001 //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/model/csct/resources/ResourceBundle.java $ 002 /*---------------- FILE HEADER ------------------------------------------ 003 004 This file is part of deegree. 005 Copyright (C) 2001-2007 by: 006 EXSE, Department of Geography, University of Bonn 007 http://www.giub.uni-bonn.de/exse/ 008 lat/lon GmbH 009 http://www.lat-lon.de 010 011 It has been implemented within SEAGIS - An OpenSource implementation of OpenGIS specification 012 (C) 2001, Institut de Recherche pour le D�veloppement (http://sourceforge.net/projects/seagis/) 013 SEAGIS Contacts: Surveillance de l'Environnement Assist�e par Satellite 014 Institut de Recherche pour le D�veloppement / US-Espace 015 mailto:seasnet@teledetection.fr 016 017 018 This library is free software; you can redistribute it and/or 019 modify it under the terms of the GNU Lesser General Public 020 License as published by the Free Software Foundation; either 021 version 2.1 of the License, or (at your option) any later version. 022 023 This library is distributed in the hope that it will be useful, 024 but WITHOUT ANY WARRANTY; without even the implied warranty of 025 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 026 Lesser General Public License for more details. 027 028 You should have received a copy of the GNU Lesser General Public 029 License along with this library; if not, write to the Free Software 030 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 031 032 Contact: 033 034 Andreas Poth 035 lat/lon GmbH 036 Aennchenstr. 19 037 53115 Bonn 038 Germany 039 E-Mail: poth@lat-lon.de 040 041 Klaus Greve 042 Department of Geography 043 University of Bonn 044 Meckenheimer Allee 166 045 53115 Bonn 046 Germany 047 E-Mail: klaus.greve@uni-bonn.de 048 049 050 ---------------------------------------------------------------------------*/ 051 package org.deegree.model.csct.resources; 052 053 // Utilities 054 import java.io.BufferedInputStream; 055 import java.io.DataInputStream; 056 import java.io.FileNotFoundException; 057 import java.io.IOException; 058 import java.io.InputStream; 059 import java.io.Writer; 060 import java.text.MessageFormat; 061 import java.util.Enumeration; 062 import java.util.Locale; 063 import java.util.MissingResourceException; 064 import java.util.NoSuchElementException; 065 066 /** 067 * {link java.util.ResourceBundle} implementation using integers instead of strings for 068 * resource keys. Because it doesn't use strings, this implementation avoid adding all 069 * those string constants to <code>.class</code> files and runtime images. Developpers 070 * still have meaningful labels in their code (e.g. <code>DIMENSION_MISMATCH</code>) 071 * through a set of constants defined in interfaces. This approach furthermore give the 072 * benefict of compile-time safety. Because integer constants are inlined right into 073 * class files at compile time, the declarative interface is never loaded at run time. 074 * <br><br> 075 * This class also provides facilities for string formatting using {@link MessageFormat}. 076 * 077 * @version 1.0 078 * @author Martin Desruisseaux 079 */ 080 public class ResourceBundle extends java.util.ResourceBundle { 081 /** 082 * Maximal string length for text inserted into an other text. This parameter 083 * is used by {@link #summarize}. Resource strings are never cut to this length. 084 * However, text replacing "{0}" in a string like "Parameter name is {0}." will 085 * be cut to this length. 086 */ 087 private static final int MAX_STRING_LENGTH = 80; 088 089 /** 090 * The resource name of the binary file containing resources. 091 * It is usually a file name, but may also be the name of an 092 * entry in a JAR file. 093 */ 094 private final String filename; 095 096 /** 097 * The array of resources. Keys are array index. For example the value for 098 * key "14" is <code>values[14]</code>. This array will be loaded only when 099 * first needed. We should not load it at construction time, because some 100 * <code>ResourceBundle</code> objects will never ask for values. This is 101 * case especially for ancestor classes of <code>Resources_fr_CA</code>, 102 * <code>Resources_en</code>, <code>Resources_de</code>, etc., which will 103 * be used only if a key has not been found in the subclasse. 104 */ 105 protected String[] values; 106 107 /** 108 * The object to use for formatting messages. This 109 * object will be constructed only when first needed. 110 */ 111 private transient MessageFormat format; 112 113 /** 114 * The key of the last resource requested. If the same resource is requested 115 * many consecutive time, knowing this fact allows to avoid invoking the costly 116 * {@link MessageFormat#applyPattern} method. 117 */ 118 private transient int lastKey; 119 120 /** 121 * Construct a new resource bundle. 122 * 123 * @param filename The resource name containing resources. It is 124 * usually a filename, but may also be an entry in a JAR file. 125 */ 126 protected ResourceBundle( final String filename ) { 127 this.filename = filename; 128 } 129 130 /** 131 * Returns the name of the logger to use. Default 132 * implementation returns the package name. 133 */ 134 protected String getLoggerName() { 135 final String name = getClass().getName(); 136 final int index = name.lastIndexOf( '.' ); 137 return ( index >= 0 ) ? name.substring( 0, index ) : "org.deegree.model"; 138 } 139 140 /** 141 * List resources to the specified stream. If a resource has 142 * more than one line, only the first line will be written. 143 * This method is used mostly for debugging purpose. 144 * 145 * @param out The destination stream. 146 * @throws IOException if an output operation failed. 147 */ 148 public final synchronized void list( final Writer out ) 149 throws IOException { 150 ensureLoaded( null ); 151 list( out, 0, values.length ); 152 } 153 154 /** 155 * List resources to the specified stream. If a resource has 156 * more than one line, only the first line will be written. 157 * This method is used mostly for debugging purpose. 158 * 159 * @param out The destination stream. 160 * @param lower The beginning index (inclusive). 161 * @param upper The ending index (exclusive). 162 * @throws IOException if an output operation failed. 163 */ 164 private void list( final Writer out, int lower, int upper ) 165 throws IOException { 166 final String lineSeparator = System.getProperty( "line.separator", "\n" ); 167 for ( int i = lower; i < upper; i++ ) { 168 String value = values[i]; 169 if ( value == null ) 170 continue; 171 int indexCR = value.indexOf( '\r' ); 172 if ( indexCR < 0 ) 173 indexCR = value.length(); 174 int indexLF = value.indexOf( '\n' ); 175 if ( indexLF < 0 ) 176 indexLF = value.length(); 177 final String number = String.valueOf( i ); 178 out.write( Utilities.spaces( 5 - number.length() ) ); 179 out.write( number ); 180 out.write( ":\t" ); 181 out.write( value.substring( 0, Math.min( indexCR, indexLF ) ) ); 182 out.write( lineSeparator ); 183 } 184 } 185 186 /** 187 * Ensure that resource values are loaded. 188 * If they are not, load them immediately. 189 * 190 * @param key Key for the requested resource, or <code>null</code> 191 * if all resources are requested. This key is used mostly 192 * for constructing messages. 193 * @throws MissingResourceException if this method failed to load resources. 194 */ 195 private void ensureLoaded( final String key ) 196 throws MissingResourceException { 197 if ( values != null ) { 198 return; 199 } 200 /* 201 * Prepare a log record. We will wait for succesfull loading before to post this 202 * record. If loading fail, the record will be changed into an error record. 203 */ 204 try { 205 final InputStream in = getClass().getClassLoader().getResourceAsStream( filename ); 206 if ( in == null ) { 207 throw new FileNotFoundException( filename ); 208 } 209 final DataInputStream input = new DataInputStream( new BufferedInputStream( in ) ); 210 values = new String[input.readInt()]; 211 for ( int i = 0; i < values.length; i++ ) { 212 values[i] = input.readUTF(); 213 if ( values[i].length() == 0 ) 214 values[i] = null; 215 } 216 input.close(); 217 218 } catch ( IOException exception ) { 219 final MissingResourceException error = new MissingResourceException( 220 exception.getLocalizedMessage(), 221 getClass().getName(), 222 key ); 223 throw error; 224 } 225 } 226 227 /** 228 * Returns an enumeration of the keys. 229 * @return enumeration of the keys 230 */ 231 public final synchronized Enumeration<String> getKeys() { 232 ensureLoaded( null ); 233 return new Enumeration<String>() { 234 private int i = 0; 235 236 public boolean hasMoreElements() { 237 while ( true ) { 238 if ( i >= values.length ) 239 return false; 240 if ( values[i] != null ) 241 return true; 242 i++; 243 } 244 } 245 246 public String nextElement() { 247 while ( true ) { 248 if ( i >= values.length ) 249 throw new NoSuchElementException(); 250 if ( values[i] != null ) 251 return String.valueOf( i++ ); 252 i++; 253 } 254 } 255 }; 256 } 257 258 /** 259 * Gets an object for the given key from this resource bundle. 260 * Returns null if this resource bundle does not contain an 261 * object for the given key. 262 * 263 * @param key the key for the desired object 264 * @exception NullPointerException if <code>key</code> is <code>null</code> 265 * @return the object for the given key, or null 266 */ 267 protected final synchronized Object handleGetObject( final String key ) { 268 ensureLoaded( key ); 269 final int keyID; 270 try { 271 keyID = Integer.parseInt( key ); 272 } catch ( NumberFormatException exception ) { 273 return null; 274 } 275 return ( keyID >= 0 && keyID < values.length ) ? values[keyID] : null; 276 } 277 278 /** 279 * Make sure that the <code>text</code> string is no longer than <code>maxLength</code> 280 * characters. If <code>text</code> is not longer, it is returned unchanged (except for 281 * trailing blancks, which are removed). If <code>text</code> is longer, it will be cut 282 * somewhere in the middle. This method try to cut between two words and replace the 283 * missing words with "(...)". For example, the following string: 284 * 285 * <blockquote> 286 * "This sentence given as an example is way too long to be included in a message." 287 * </blockquote> 288 * 289 * May be "summarized" by something like this: 290 * 291 * <blockquote> 292 * "This sentence given (...) included in a message." 293 * </blockquote> 294 * 295 * @param text The sentence to summarize if it is too long. 296 * @param maxLength The maximal length allowed for <code>text</code>. 297 * If <code>text</code> is longer, it will summarized. 298 * @return A sentence not longer than <code>text</code>. 299 */ 300 private static String summarize( String text, int maxLength ) { 301 text = text.trim(); 302 final int length = text.length(); 303 if ( length <= maxLength ) 304 return text; 305 /* 306 * Compute maximum length for one half of the string. Take in 307 * account the space needed for inserting the " (...) " string. 308 */ 309 maxLength = ( maxLength - 7 ) >> 1; 310 if ( maxLength <= 0 ) 311 return text; 312 /* 313 * We will remove characters from 'break1' to 'break2', both exclusive. 314 * We try to adjust 'break1' and 'break2' in such a way that first and 315 * last removed characters will be spaces or punctuation characters. 316 * Constants 'lower' and 'upper' are limit values. If we don't find values 317 * for 'break1' and 'break2' inside those limits, we will give it up. 318 */ 319 int break1 = maxLength; 320 int break2 = length - maxLength; 321 for ( final int lower = ( maxLength >> 1 ); break1 >= lower; break1-- ) { 322 if ( !Character.isUnicodeIdentifierPart( text.charAt( break1 ) ) ) { 323 while ( --break1 >= lower 324 && !Character.isUnicodeIdentifierPart( text.charAt( break1 ) ) ) 325 ; 326 break; 327 } 328 } 329 for ( final int upper = length - ( maxLength >> 1 ); break2 < upper; break2++ ) { 330 if ( !Character.isUnicodeIdentifierPart( text.charAt( break2 ) ) ) { 331 while ( ++break2 < upper 332 && !Character.isUnicodeIdentifierPart( text.charAt( break2 ) ) ) 333 ; 334 break; 335 } 336 } 337 return ( text.substring( 0, break1 + 1 ) + " (...) " + text.substring( break2 ) ).trim(); 338 } 339 340 /** 341 * Returns <code>arguments</code> as an array. If <code>arguments</code> is already an 342 * array, this array or a copy of this array will be returned. If <code>arguments</code> 343 * is not an array, it will be wrapped in an array of length 1. In all case, all array's 344 * elements will be checked for {@link String} objects. Any strings of length greater than 345 * {@link #MAX_STRING_LENGTH} will be reduced using the {@link #summarize} method. 346 * 347 * @param arguments The object to check. 348 * @return <code>arguments</code> as an array. 349 */ 350 private static Object[] toArray( final Object arguments ) { 351 Object[] array; 352 if ( arguments instanceof Object[] ) { 353 array = (Object[]) arguments; 354 } else { 355 array = new Object[] { arguments }; 356 } 357 for ( int i = 0; i < array.length; i++ ) { 358 { 359 final String s0 = array[i].toString(); 360 final String s1 = summarize( s0, MAX_STRING_LENGTH ); 361 if ( s0 != s1 && !s0.equals( s1 ) ) { 362 if ( array == arguments ) { 363 array = new Object[array.length]; 364 System.arraycopy( arguments, 0, array, 0, array.length ); 365 } 366 array[i] = s1; 367 } 368 } 369 } 370 return array; 371 } 372 373 /** 374 * Gets a string for the given key and append "..." to it. 375 * This is method is typically used for creating menu label. 376 * 377 * @param keyID The key for the desired string. 378 * @return The string for the given key. 379 * @throws MissingResourceException If no object for the given key can be found. 380 */ 381 public final String getMenuLabel( final int keyID ) 382 throws MissingResourceException { 383 return getString( keyID ) + "..."; 384 } 385 386 /** 387 * Gets a string for the given key and append ": " to it. 388 * This is method is typically used for creating menu label. 389 * 390 * @param keyID The key for the desired string. 391 * @return The string for the given key. 392 * @throws MissingResourceException If no object for the given key can be found. 393 */ 394 public final String getLabel( final int keyID ) 395 throws MissingResourceException { 396 return getString( keyID ) + ": "; 397 } 398 399 /** 400 * Gets a string for the given key from this resource bundle or one of its parents. 401 * 402 * @param keyID The key for the desired string. 403 * @return The string for the given key. 404 * @throws MissingResourceException If no object for the given key can be found. 405 */ 406 public final String getString( final int keyID ) 407 throws MissingResourceException { 408 return getString( String.valueOf( keyID ) ); 409 } 410 411 /** 412 * Gets a string for the given key and format it with the specified argument. 413 * The message if formatted using {@link MessageFormat}. Calling his method is 414 * approximatively equivalent to calling: 415 * 416 * <blockquote><pre> 417 * String pattern = getString(key); 418 * Format f = new MessageFormat(pattern); 419 * return f.format(arg0); 420 * </pre></blockquote> 421 * 422 * If <code>arg0</code> is not already an array, it will be wrapped into an array 423 * of length 1. Using {@link MessageFormat}, all occurence of "{0}", "{1}", "{2}" 424 * in the resource string will be replaced by <code>arg0[0]</code>, <code>arg0[1]</code>, 425 * <code>arg0[2]</code>, etc. 426 * 427 * @param keyID The key for the desired string. 428 * @param arg0 A single object or an array of objects to be formatted and substituted. 429 * @return The string for the given key. 430 * @throws MissingResourceException If no object for the given key can be found. 431 * 432 * @see MessageFormat 433 */ 434 public final synchronized String getString( final int keyID, final Object arg0 ) 435 throws MissingResourceException { 436 final Object object = getObject( String.valueOf( keyID ) ); 437 final Object[] arguments = toArray( arg0 ); 438 if ( format == null ) { 439 /* 440 * Construct a new {@link MessageFormat} for formatting the arguments. There is two 441 * possible {@link Locale} we could use: default locale or resource bundle locale. 442 * If the default locale use the same language than this <code>ResourceBundle</code> 443 * locale, then we will use the default locale. This allow formatting dates and numbers 444 * with user conventions (e.g. French Canada) even if the <code>ResourceBundle</code> 445 * locale is different (e.g. standard French). However, if languages don't match, then 446 * we will use <code>ResourceBundle</code> locale for better coherence. 447 */ 448 Locale locale = Locale.getDefault(); 449 final Locale resourceLocale = getLocale(); 450 if ( !locale.getLanguage().equalsIgnoreCase( resourceLocale.getLanguage() ) ) { 451 locale = resourceLocale; 452 } 453 } else if ( keyID != lastKey ) { 454 /* 455 * Method {@link MessageFormat#applyPattern} is costly! We will avoid 456 * calling it again if {@link #format} already has the right pattern. 457 */ 458 format.applyPattern( object.toString() ); 459 lastKey = keyID; 460 } 461 return format.format( arguments ); 462 } 463 464 /** 465 * Gets a string for the given key are replace all occurence of "{0}", 466 * "{1}", with values of <code>arg0</code>, <code>arg1</code>, etc. 467 * 468 * @param keyID The key for the desired string. 469 * @param arg0 Value to substitute to "{0}". 470 * @param arg1 Value to substitute to "{1}". 471 * @return The formatted string for the given key. 472 * @throws MissingResourceException If no object for the given key can be found. 473 */ 474 public final String getString( final int keyID, final Object arg0, final Object arg1 ) 475 throws MissingResourceException { 476 return getString( keyID, new Object[] { arg0, arg1 } ); 477 } 478 479 /** 480 * Gets a string for the given key are replace all occurence of "{0}", 481 * "{1}", with values of <code>arg0</code>, <code>arg1</code>, etc. 482 * 483 * @param keyID The key for the desired string. 484 * @param arg0 Value to substitute to "{0}". 485 * @param arg1 Value to substitute to "{1}". 486 * @param arg2 Value to substitute to "{2}". 487 * @return The formatted string for the given key. 488 * @throws MissingResourceException If no object for the given key can be found. 489 */ 490 public final String getString( final int keyID, final Object arg0, final Object arg1, 491 final Object arg2 ) 492 throws MissingResourceException { 493 return getString( keyID, new Object[] { arg0, arg1, arg2 } ); 494 } 495 496 /** 497 * Gets a string for the given key are replace all occurence of "{0}", 498 * "{1}", with values of <code>arg0</code>, <code>arg1</code>, etc. 499 * 500 * @param keyID The key for the desired string. 501 * @param arg0 Value to substitute to "{0}". 502 * @param arg1 Value to substitute to "{1}". 503 * @param arg2 Value to substitute to "{2}". 504 * @param arg3 Value to substitute to "{3}". 505 * @return The formatted string for the given key. 506 * @throws MissingResourceException If no object for the given key can be found. 507 */ 508 public final String getString( final int keyID, final Object arg0, final Object arg1, 509 final Object arg2, final Object arg3 ) 510 throws MissingResourceException { 511 return getString( keyID, new Object[] { arg0, arg1, arg2, arg3 } ); 512 } 513 514 /** 515 * Gets a string for the given key are replace all occurence of "{0}", 516 * "{1}", with values of <code>arg0</code>, <code>arg1</code>, etc. 517 * 518 * @param keyID The key for the desired string. 519 * @param arg0 Value to substitute to "{0}". 520 * @param arg1 Value to substitute to "{1}". 521 * @param arg2 Value to substitute to "{2}". 522 * @param arg3 Value to substitute to "{3}". 523 * @param arg4 Value to substitute to "{4}". 524 * @return The formatted string for the given key. 525 * @throws MissingResourceException If no object for the given key can be found. 526 */ 527 public final String getString( final int keyID, final Object arg0, final Object arg1, 528 final Object arg2, final Object arg3, final Object arg4 ) 529 throws MissingResourceException { 530 return getString( keyID, new Object[] { arg0, arg1, arg2, arg3, arg4 } ); 531 } 532 533 /** 534 * Returns a string representation of this object. 535 * This method is for debugging purpose only. 536 */ 537 public synchronized String toString() { 538 final StringBuffer buffer = new StringBuffer( Utilities.getShortClassName( this ) ); 539 buffer.append( '[' ); 540 if ( values != null ) { 541 int count = 0; 542 for ( int i = 0; i < values.length; i++ ) 543 if ( values[i] != null ) 544 count++; 545 buffer.append( count ); 546 buffer.append( " values" ); 547 } 548 buffer.append( ']' ); 549 return buffer.toString(); 550 } 551 }