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    }