001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.resources;
018:
019: import java.io.BufferedInputStream;
020: import java.io.DataInputStream;
021: import java.io.FileNotFoundException;
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.io.Writer;
025: import java.text.MessageFormat;
026: import java.util.Enumeration;
027: import java.util.Locale;
028: import java.util.MissingResourceException;
029: import java.util.NoSuchElementException;
030: import java.util.ResourceBundle;
031: import java.util.logging.Level;
032: import java.util.logging.LogRecord;
033: import java.util.logging.Logger;
034:
035: import org.opengis.util.InternationalString;
036: import org.geotools.util.logging.Logging;
037:
038: /**
039: * {@link ResourceBundle} implementation using integers instead of strings for resource
040: * keys. Because it doesn't use strings, this implementation avoids adding all those string
041: * constants to {@code .class} files and runtime images. Developers still have meaningful labels
042: * in their code (e.g. {@code DIMENSION_MISMATCH}) through a set of constants defined in interfaces.
043: * This approach furthermore gives the benefit of compile-time safety. Because integer constants are
044: * inlined right into class files at compile time, the declarative interface is never loaded at run
045: * time. This class also provides facilities for string formatting using {@link MessageFormat}.
046: *
047: * @since 2.4
048: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/resources/IndexedResourceBundle.java $
049: * @version $Id: IndexedResourceBundle.java 27862 2007-11-12 19:51:19Z desruisseaux $
050: * @author Martin Desruisseaux
051: */
052: public class IndexedResourceBundle extends ResourceBundle {
053: /**
054: * The logger for reporting resources loading.
055: */
056: private static final Logger LOGGER = Logging
057: .getLogger("org.geotools.resources");
058:
059: /**
060: * Maximum string length for text inserted into another text. This parameter is used by
061: * {@link #summarize}. Resource strings are never cut to this length. However, text replacing
062: * "{0}" in a string like "Parameter name is {0}." will be cut to this length.
063: */
064: private static final int MAX_STRING_LENGTH = 80;
065:
066: /**
067: * The resource name of the binary file containing resources.
068: * It may be a file name or an entry in a JAR file.
069: */
070: private final String filename;
071:
072: /**
073: * The array of resources. Keys are an array index. For example, the value for key "14" is
074: * {@code values[14]}. This array will be loaded only when first needed. We should not load
075: * it at construction time, because some {@code ResourceBundle} objects will never ask for
076: * values. This is particularly the case for ancestor classes of {@code Resources_fr_CA},
077: * {@code Resources_en}, {@code Resources_de}, etc., which will only be used if a key has
078: * not been found in the subclass.
079: */
080: private String[] values;
081:
082: /**
083: * The locale for formatting objects like number, date, etc. There are two possible Locales
084: * we could use: default locale or resource bundle locale. If the default locale uses the same
085: * language as this ResourceBundle's locale, then we will use the default locale. This allows
086: * dates and numbers to be formatted according to user conventions (e.g. French Canada) even
087: * if the ResourceBundle locale is different (e.g. standard French). However, if languages
088: * don't match, then we will use ResourceBundle locale for better coherence.
089: */
090: private transient Locale locale;
091:
092: /**
093: * The object to use for formatting messages. This object
094: * will be constructed only when first needed.
095: */
096: private transient MessageFormat format;
097:
098: /**
099: * The key of the last resource requested. If the same resource is requested multiple times,
100: * knowing its key allows us to avoid invoking the costly {@link MessageFormat#applyPattern}
101: * method.
102: */
103: private transient int lastKey;
104:
105: /**
106: * Constructs a new resource bundle. The resource filename will be inferred from
107: * the fully qualified classname of this {@code IndexedResourceBundle} subclass.
108: */
109: protected IndexedResourceBundle() {
110: String classname = getClass().getName();
111: if (classname.endsWith("_en")) {
112: /*
113: * In Geotools implementation, the English language resources are the default ones.
114: */
115: classname = classname.substring(0, classname.length() - 3);
116: }
117: filename = classname.substring(classname.lastIndexOf('.') + 1)
118: + ".utf";
119: }
120:
121: /**
122: * Constructs a new resource bundle.
123: *
124: * @param filename The resource name containing resources.
125: * It may be a filename or an entry in a JAR file.
126: */
127: protected IndexedResourceBundle(final String filename) {
128: this .filename = filename;
129: }
130:
131: /**
132: * Returns the locale to use for formatters.
133: */
134: private Locale getFormatLocale() {
135: if (locale == null) {
136: locale = Locale.getDefault();
137: final Locale resourceLocale = getLocale();
138: if (!locale.getLanguage().equalsIgnoreCase(
139: resourceLocale.getLanguage())) {
140: locale = resourceLocale;
141: }
142: }
143: return locale;
144: }
145:
146: /**
147: * Returns the name of the package.
148: */
149: private String getPackageName() {
150: final String name = getClass().getName();
151: final int index = name.lastIndexOf('.');
152: return (index >= 0) ? name.substring(0, index) : "org.geotools";
153: }
154:
155: /**
156: * Lists resources to the specified stream. If a resource has more than one line, only
157: * the first line will be written. This method is used mostly for debugging purposes.
158: *
159: * @param out The destination stream.
160: * @throws IOException if an output operation failed.
161: */
162: public final void list(final Writer out) throws IOException {
163: // Synchronization performed by 'ensureLoaded'
164: list(out, ensureLoaded(null));
165: }
166:
167: /**
168: * Lists resources to the specified stream. If a resource has more than one line, only
169: * the first line will be written. This method is used mostly for debugging purposes.
170: *
171: * @param out The destination stream.
172: * @param values The resources to list.
173: * @throws IOException if an output operation failed.
174: */
175: private static void list(final Writer out, final String[] values)
176: throws IOException {
177: final String lineSeparator = System.getProperty(
178: "line.separator", "\n");
179: for (int i = 0; i < values.length; i++) {
180: String value = values[i];
181: if (value == null) {
182: continue;
183: }
184: int indexCR = value.indexOf('\r');
185: if (indexCR < 0)
186: indexCR = value.length();
187: int indexLF = value.indexOf('\n');
188: if (indexLF < 0)
189: indexLF = value.length();
190: final String number = String.valueOf(i);
191: out.write(Utilities.spaces(5 - number.length()));
192: out.write(number);
193: out.write(":\t");
194: out.write(value.substring(0, Math.min(indexCR, indexLF)));
195: out.write(lineSeparator);
196: }
197: }
198:
199: /**
200: * Ensures that resource values are loaded. If they are not, load them immediately.
201: *
202: * @param key Key for the requested resource, or {@code null} if all resources
203: * are requested. This key is used mostly for constructing messages.
204: * @return The resources.
205: * @throws MissingResourceException if this method failed to load resources.
206: */
207: private String[] ensureLoaded(final String key)
208: throws MissingResourceException {
209: LogRecord record = null;
210: try {
211: String[] values;
212: synchronized (this ) {
213: values = this .values;
214: if (values != null) {
215: return values;
216: }
217: /*
218: * Prepares a log record. We will wait for successful loading before
219: * posting this record. If loading fails, the record will be changed
220: * into an error record. Note that the message must be logged outside
221: * the synchronized block, otherwise there is dead locks!
222: */
223: record = new LogRecord(Level.FINER,
224: "Loaded resources for {0} from bundle \"{1}\".");
225: record.setSourceClassName(getClass().getName());
226: record.setSourceMethodName((key != null) ? "getObject"
227: : "getKeys");
228: /*
229: * Load resources from the UTF file.
230: */
231: final InputStream in = getClass().getResourceAsStream(
232: filename);
233: if (in == null) {
234: throw new FileNotFoundException(filename);
235: }
236: final DataInputStream input = new DataInputStream(
237: new BufferedInputStream(in));
238: this .values = values = new String[input.readInt()];
239: for (int i = 0; i < values.length; i++) {
240: values[i] = input.readUTF();
241: if (values[i].length() == 0)
242: values[i] = null;
243: }
244: input.close();
245: /*
246: * Now, log the message. This message is not localized.
247: */
248: String language = getLocale().getDisplayName(Locale.US);
249: if (language == null || language.length() == 0) {
250: language = "<default>";
251: }
252: record.setParameters(new String[] { language,
253: getPackageName() });
254: }
255: LOGGER.log(record);
256: return values;
257: } catch (IOException exception) {
258: record.setLevel(Level.WARNING);
259: record.setMessage(exception.getLocalizedMessage());
260: record.setThrown(exception);
261: LOGGER.log(record);
262: final MissingResourceException error = new MissingResourceException(
263: exception.getLocalizedMessage(), getClass()
264: .getName(), key);
265: error.initCause(exception);
266: throw error;
267: }
268: }
269:
270: /**
271: * Returns an enumeration of the keys.
272: */
273: public final Enumeration getKeys() {
274: // Synchronization performed by 'ensureLoaded'
275: final String[] values = ensureLoaded(null);
276: return new Enumeration() {
277: private int i = 0;
278:
279: public boolean hasMoreElements() {
280: while (true) {
281: if (i >= values.length)
282: return false;
283: if (values[i] != null)
284: return true;
285: i++;
286: }
287: }
288:
289: public Object nextElement() {
290: while (true) {
291: if (i >= values.length)
292: throw new NoSuchElementException();
293: if (values[i] != null)
294: return String.valueOf(i++);
295: i++;
296: }
297: }
298: };
299: }
300:
301: /**
302: * Gets an object for the given key from this resource bundle.
303: * Returns null if this resource bundle does not contain an
304: * object for the given key.
305: *
306: * @param key the key for the desired object
307: * @exception NullPointerException if {@code key} is {@code null}
308: * @return the object for the given key, or null
309: */
310: protected final Object handleGetObject(final String key) {
311: // Synchronization performed by 'ensureLoaded'
312: final String[] values = ensureLoaded(key);
313: final int keyID;
314: try {
315: keyID = Integer.parseInt(key);
316: } catch (NumberFormatException exception) {
317: return null;
318: }
319: return (keyID >= 0 && keyID < values.length) ? values[keyID]
320: : null;
321: }
322:
323: /**
324: * Makes sure that the {@code text} string is not longer than {@code maxLength} characters.
325: * If {@code text} is not longer, it is returned unchanged (except for trailing blanks,
326: * which are removed). If {@code text} is longer, it will be cut somewhere in the middle.
327: * This method tries to cut between two words and replace the missing words with "(...)".
328: * For example, the following string:
329: *
330: * <blockquote>
331: * "This sentence given as an example is way too long to be
332: * included in a message."
333: * </blockquote>
334: *
335: * May be "summarized" by something like this:
336: *
337: * <blockquote>
338: * "This sentence given (...) included in a message."
339: * </blockquote>
340: *
341: * @param text The sentence to summarize if it is too long.
342: * @param maxLength The maximum length allowed for {@code text}.
343: * If {@code text} is longer, it will be summarized.
344: * @return A sentence not longer than {@code maxLength}.
345: */
346: private static String summarize(String text, int maxLength) {
347: text = text.trim();
348: final int length = text.length();
349: if (length <= maxLength) {
350: return text;
351: }
352: /*
353: * Computes maximum length for one half of the string. Take into
354: * account the space needed for inserting the " (...) " string.
355: */
356: maxLength = (maxLength - 7) >> 1;
357: if (maxLength <= 0) {
358: return text;
359: }
360: /*
361: * We will remove characters from 'break1' to 'break2', both exclusive.
362: * We try to adjust 'break1' and 'break2' in such a way that the first
363: * and last characters to be removed will be spaces or punctuation
364: * characters.
365: * Constants 'lower' and 'upper' are limit values. If we don't find
366: * values for 'break1' and 'break2' inside those limits, we will give
367: * up.
368: */
369: int break1 = maxLength;
370: int break2 = length - maxLength;
371: for (final int lower = (maxLength >> 1); break1 >= lower; break1--) {
372: if (!Character.isUnicodeIdentifierPart(text.charAt(break1))) {
373: while (--break1 >= lower
374: && !Character.isUnicodeIdentifierPart(text
375: .charAt(break1)))
376: ;
377: break;
378: }
379: }
380: for (final int upper = length - (maxLength >> 1); break2 < upper; break2++) {
381: if (!Character.isUnicodeIdentifierPart(text.charAt(break2))) {
382: while (++break2 < upper
383: && !Character.isUnicodeIdentifierPart(text
384: .charAt(break2)))
385: ;
386: break;
387: }
388: }
389: return (text.substring(0, break1 + 1) + " (...) " + text
390: .substring(break2)).trim();
391: }
392:
393: /**
394: * Returns {@code arguments} as an array. If {@code arguments} is already an array, this array
395: * or a copy of this array will be returned. If {@code arguments} is not an array, it will be
396: * placed in an array of length 1. In any case, all the array's elements will be checked for
397: * {@link String} objects. Any strings of length greater than {@link #MAX_STRING_LENGTH} will
398: * be reduced using the {@link #summarize} method.
399: *
400: * @param arguments The object to check.
401: * @return {@code arguments} as an array.
402: */
403: private Object[] toArray(final Object arguments) {
404: Object[] array;
405: if (arguments instanceof Object[]) {
406: array = (Object[]) arguments;
407: } else {
408: array = new Object[] { arguments };
409: }
410: for (int i = 0; i < array.length; i++) {
411: final Object element = array[i];
412: if (element instanceof CharSequence) {
413: final String s0;
414: if (element instanceof InternationalString) {
415: s0 = ((InternationalString) element)
416: .toString(getFormatLocale());
417: } else {
418: s0 = element.toString();
419: }
420: final String s1 = summarize(s0, MAX_STRING_LENGTH);
421: if (s0 != s1 && !s0.equals(s1)) {
422: if (array == arguments) {
423: array = new Object[array.length];
424: System.arraycopy(arguments, 0, array, 0,
425: array.length);
426: }
427: array[i] = s1;
428: }
429: } else if (element instanceof Throwable) {
430: String message = ((Throwable) element)
431: .getLocalizedMessage();
432: if (message == null) {
433: message = Utilities.getShortClassName(element);
434: }
435: array[i] = message;
436: }
437: }
438: return array;
439: }
440:
441: /**
442: * Gets a string for the given key and appends "..." to it.
443: * This method is typically used for creating menu items.
444: *
445: * @param key The key for the desired string.
446: * @return The string for the given key.
447: * @throws MissingResourceException If no object for the given key can be found.
448: */
449: public final String getMenuLabel(final int key)
450: throws MissingResourceException {
451: return getString(key) + "...";
452: }
453:
454: /**
455: * Gets a string for the given key and appends ": " to it.
456: * This method is typically used for creating labels.
457: *
458: * @param key The key for the desired string.
459: * @return The string for the given key.
460: * @throws MissingResourceException If no object for the given key can be found.
461: */
462: public final String getLabel(final int key)
463: throws MissingResourceException {
464: return getString(key) + ": ";
465: }
466:
467: /**
468: * Gets a string for the given key from this resource bundle or one of its parents.
469: *
470: * @param key The key for the desired string.
471: * @return The 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 key)
475: throws MissingResourceException {
476: return getString(String.valueOf(key));
477: }
478:
479: /**
480: * Gets a string for the given key and formats it with the specified argument. The message is
481: * formatted using {@link MessageFormat}. Calling this method is approximately equivalent to
482: * calling:
483: *
484: * <blockquote><pre>
485: * String pattern = getString(key);
486: * Format f = new MessageFormat(pattern);
487: * return f.format(arg0);
488: * </pre></blockquote>
489: *
490: * If {@code arg0} is not already an array, it will be placed into an array of length 1. Using
491: * {@link MessageFormat}, all occurrences of "{0}", "{1}", "{2}" in the resource string will be
492: * replaced by {@code arg0[0]}, {@code arg0[1]}, {@code arg0[2]}, etc.
493: *
494: * @param key The key for the desired string.
495: * @param arg0 A single object or an array of objects to be formatted and substituted.
496: * @return The string for the given key.
497: * @throws MissingResourceException If no object for the given key can be found.
498: *
499: * @see #getString(String)
500: * @see #getString(int,Object,Object)
501: * @see #getString(int,Object,Object,Object)
502: * @see MessageFormat
503: */
504: public final String getString(final int key, final Object arg0)
505: throws MissingResourceException {
506: final String pattern = getString(key);
507: final Object[] arguments = toArray(arg0);
508: synchronized (this ) {
509: if (format == null) {
510: /*
511: * Constructs a new MessageFormat for formatting the arguments.
512: */
513: format = new MessageFormat(pattern, getFormatLocale());
514: } else if (key != lastKey) {
515: /*
516: * Method MessageFormat.applyPattern(...) is costly! We will avoid
517: * calling it again if the format already has the right pattern.
518: */
519: format.applyPattern(pattern);
520: lastKey = key;
521: }
522: return format.format(arguments);
523: }
524: }
525:
526: /**
527: * Gets a string for the given key and replaces all occurrences of "{0}",
528: * "{1}", with values of {@code arg0}, {@code arg1}, etc.
529: *
530: * @param key The key for the desired string.
531: * @param arg0 Value to substitute for "{0}".
532: * @param arg1 Value to substitute for "{1}".
533: * @return The formatted string for the given key.
534: * @throws MissingResourceException If no object for the given key can be found.
535: */
536: public final String getString(final int key, final Object arg0,
537: final Object arg1) throws MissingResourceException {
538: return getString(key, new Object[] { arg0, arg1 });
539: }
540:
541: /**
542: * Gets a string for the given key and replaces all occurrences of "{0}",
543: * "{1}", with values of {@code arg0}, {@code arg1}, etc.
544: *
545: * @param key The key for the desired string.
546: * @param arg0 Value to substitute for "{0}".
547: * @param arg1 Value to substitute for "{1}".
548: * @param arg2 Value to substitute for "{2}".
549: * @return The formatted string for the given key.
550: * @throws MissingResourceException If no object for the given key can be found.
551: */
552: public final String getString(final int key, final Object arg0,
553: final Object arg1, final Object arg2)
554: throws MissingResourceException {
555: return getString(key, new Object[] { arg0, arg1, arg2 });
556: }
557:
558: /**
559: * Gets a string for the given key and replaces all occurrences of "{0}",
560: * "{1}", with values of {@code arg0}, {@code arg1}, etc.
561: *
562: * @param key The key for the desired string.
563: * @param arg0 Value to substitute for "{0}".
564: * @param arg1 Value to substitute for "{1}".
565: * @param arg2 Value to substitute for "{2}".
566: * @param arg3 Value to substitute for "{3}".
567: * @return The formatted string for the given key.
568: * @throws MissingResourceException If no object for the given key can be found.
569: */
570: public final String getString(final int key, final Object arg0,
571: final Object arg1, final Object arg2, final Object arg3)
572: throws MissingResourceException {
573: return getString(key, new Object[] { arg0, arg1, arg2, arg3 });
574: }
575:
576: /**
577: * Gets a string for the given key and replaces all occurrences of "{0}",
578: * "{1}", with values of {@code arg0}, {@code arg1}, etc.
579: *
580: * @param key The key for the desired string.
581: * @param arg0 Value to substitute for "{0}".
582: * @param arg1 Value to substitute for "{1}".
583: * @param arg2 Value to substitute for "{2}".
584: * @param arg3 Value to substitute for "{3}".
585: * @param arg4 Value to substitute for "{4}".
586: * @return The formatted string for the given key.
587: * @throws MissingResourceException If no object for the given key can be found.
588: */
589: public final String getString(final int key, final Object arg0,
590: final Object arg1, final Object arg2, final Object arg3,
591: final Object arg4) throws MissingResourceException {
592: return getString(key, new Object[] { arg0, arg1, arg2, arg3,
593: arg4 });
594: }
595:
596: /**
597: * Gets a localized log record.
598: *
599: * @param level The log record level.
600: * @param key The resource key.
601: * @return The log record.
602: */
603: public LogRecord getLogRecord(final Level level, final int key) {
604: return getLogRecord(level, key, null);
605: }
606:
607: /**
608: * Gets a localized log record.
609: *
610: * @param level The log record level.
611: * @param key The resource key.
612: * @param arg0 The parameter for the log message, or {@code null}.
613: * @return The log record.
614: */
615: public LogRecord getLogRecord(final Level level, final int key,
616: final Object arg0) {
617: final LogRecord record = new LogRecord(level, String
618: .valueOf(key));
619: record.setResourceBundle(this );
620: if (arg0 != null) {
621: record.setParameters(toArray(arg0));
622: }
623: return record;
624: }
625:
626: /**
627: * Gets a localized log record.
628: *
629: * @param level The log record level.
630: * @param key The resource key.
631: * @param arg0 The first parameter.
632: * @param arg1 The second parameter.
633: * @return The log record.
634: */
635: public LogRecord getLogRecord(final Level level, final int key,
636: final Object arg0, final Object arg1) {
637: return getLogRecord(level, key, new Object[] { arg0, arg1 });
638: }
639:
640: /**
641: * Gets a localized log record.
642: *
643: * @param level The log record level.
644: * @param key The resource key.
645: * @param arg0 The first parameter.
646: * @param arg1 The second parameter.
647: * @param arg2 The third parameter.
648: * @return The log record.
649: */
650: public LogRecord getLogRecord(final Level level, final int key,
651: final Object arg0, final Object arg1, final Object arg2) {
652: return getLogRecord(level, key,
653: new Object[] { arg0, arg1, arg2 });
654: }
655:
656: /**
657: * Gets a localized log record.
658: *
659: * @param level The log record level.
660: * @param key The resource key.
661: * @param arg0 The first parameter.
662: * @param arg1 The second parameter.
663: * @param arg2 The third parameter.
664: * @param arg3 The fourth parameter.
665: * @return The log record.
666: */
667: public LogRecord getLogRecord(final Level level, final int key,
668: final Object arg0, final Object arg1, final Object arg2,
669: final Object arg3) {
670: return getLogRecord(level, key, new Object[] { arg0, arg1,
671: arg2, arg3 });
672: }
673:
674: /**
675: * Localize and format the message string from a log record. This method performs a work
676: * similar to {@link java.util.logging.Formatter#formatMessage}, except that the work will be
677: * delegated to {@link #getString(int, Object)} if the {@linkplain LogRecord#getResourceBundle
678: * record resource bundle} is an instance of {@code IndexedResourceBundle}.
679: *
680: * @param record The log record to format.
681: * @return The formatted message.
682: */
683: public static String format(final LogRecord record) {
684: String message = record.getMessage();
685: final ResourceBundle resources = record.getResourceBundle();
686: if (resources instanceof IndexedResourceBundle) {
687: int key = -1;
688: try {
689: key = Integer.parseInt(message);
690: } catch (NumberFormatException e) {
691: unexpectedException(e);
692: }
693: if (key >= 0) {
694: final Object[] parameters = record.getParameters();
695: return ((IndexedResourceBundle) resources).getString(
696: key, parameters);
697: }
698: }
699: if (resources != null) {
700: try {
701: message = resources.getString(message);
702: } catch (MissingResourceException e) {
703: unexpectedException(e);
704: }
705: final Object[] parameters = record.getParameters();
706: if (parameters != null && parameters.length != 0) {
707: final int offset = message.indexOf('{');
708: if (offset >= 0 && offset < message.length() - 1) {
709: // Uses a more restrictive check than Character.isDigit(char)
710: final char c = message.charAt(offset);
711: if (c >= '0' && c <= '9')
712: try {
713: return MessageFormat.format(message,
714: parameters);
715: } catch (IllegalArgumentException e) {
716: unexpectedException(e);
717: }
718: }
719: }
720: }
721: return message;
722: }
723:
724: /**
725: * Invoked when an unexpected exception occured in the {@link #format} method.
726: */
727: private static void unexpectedException(
728: final RuntimeException exception) {
729: Logging.unexpectedException(LOGGER,
730: IndexedResourceBundle.class, "format", exception);
731: }
732:
733: /**
734: * Returns a string representation of this object.
735: * This method is for debugging purposes only.
736: */
737: public synchronized String toString() {
738: final StringBuffer buffer = new StringBuffer(Utilities
739: .getShortClassName(this ));
740: buffer.append('[');
741: if (values != null) {
742: int count = 0;
743: for (int i = 0; i < values.length; i++) {
744: if (values[i] != null)
745: count++;
746: }
747: buffer.append(count);
748: buffer.append(" values");
749: }
750: buffer.append(']');
751: return buffer.toString();
752: }
753: }
|