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 }