View Javadoc

1   /**
2    * Licensed under the Artistic License; you may not use this file
3    * except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *      http://displaytag.sourceforge.net/license.html
7    *
8    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
9    * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10   * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11   */
12  package org.displaytag.properties;
13  
14  import java.io.IOException;
15  import java.io.InputStream;
16  import java.util.*;
17  import java.text.Collator;
18  
19  import javax.servlet.http.HttpServletRequest;
20  import javax.servlet.jsp.PageContext;
21  import javax.servlet.jsp.tagext.Tag;
22  
23  import org.apache.commons.lang.ClassUtils;
24  import org.apache.commons.lang.StringUtils;
25  import org.apache.commons.lang.UnhandledException;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.displaytag.Messages;
29  import org.displaytag.model.DefaultComparator;
30  import org.displaytag.decorator.DecoratorFactory;
31  import org.displaytag.decorator.DefaultDecoratorFactory;
32  import org.displaytag.exception.FactoryInstantiationException;
33  import org.displaytag.exception.TablePropertiesLoadException;
34  import org.displaytag.localization.I18nResourceProvider;
35  import org.displaytag.localization.LocaleResolver;
36  import org.displaytag.util.DefaultRequestHelperFactory;
37  import org.displaytag.util.ReflectHelper;
38  import org.displaytag.util.RequestHelperFactory;
39  
40  
41  /**
42   * The properties used by the Table tags. The properties are loaded in the following order, in increasing order of
43   * priority. The locale of getInstance() is used to determine the locale of the property file to use; if the key
44   * required does not exist in the specified file, the key will be loaded from a more general property file.
45   * <ol>
46   * <li>First, from the TableTag.properties included with the DisplayTag distribution.</li>
47   * <li>Then, from the file displaytag.properties, if it is present; these properties are intended to be set by the user
48   * for sitewide application. Messages are gathered according to the Locale of the property file.</li>
49   * <li>Finally, if this class has a userProperties defined, all of the properties from that Properties object are
50   * copied in as well.</li>
51   * </ol>
52   * @author Fabrizio Giustina
53   * @author rapruitt
54   * @version $Revision: 1096 $ ($Author: rapruitt $)
55   */
56  public final class TableProperties implements Cloneable
57  {
58  
59      /**
60       * name of the default properties file name ("displaytag.properties").
61       */
62      public static final String DEFAULT_FILENAME = "displaytag.properties"; //$NON-NLS-1$
63  
64      /**
65       * The name of the local properties file that is searched for on the classpath. Settings in this file will override
66       * the defaults loaded from TableTag.properties.
67       */
68      public static final String LOCAL_PROPERTIES = "displaytag"; //$NON-NLS-1$
69  
70      /**
71       * property <code>export.banner</code>.
72       */
73      public static final String PROPERTY_STRING_EXPORTBANNER = "export.banner"; //$NON-NLS-1$
74  
75      /**
76       * property <code>export.banner.sepchar</code>.
77       */
78      public static final String PROPERTY_STRING_EXPORTBANNER_SEPARATOR = "export.banner.sepchar"; //$NON-NLS-1$
79  
80      /**
81       * property <code>export.decorated</code>.
82       */
83      public static final String PROPERTY_BOOLEAN_EXPORTDECORATED = "export.decorated"; //$NON-NLS-1$
84  
85      /**
86       * property <code>export.amount</code>.
87       */
88      public static final String PROPERTY_STRING_EXPORTAMOUNT = "export.amount"; //$NON-NLS-1$
89  
90      /**
91       * property <code>sort.amount</code>.
92       */
93      public static final String PROPERTY_STRING_SORTAMOUNT = "sort.amount"; //$NON-NLS-1$
94  
95      /**
96       * property <code>basic.show.header</code>.
97       */
98      public static final String PROPERTY_BOOLEAN_SHOWHEADER = "basic.show.header"; //$NON-NLS-1$
99  
100     /**
101      * property <code>basic.msg.empty_list</code>.
102      */
103     public static final String PROPERTY_STRING_EMPTYLIST_MESSAGE = "basic.msg.empty_list"; //$NON-NLS-1$
104 
105     /**
106      * property <code>basic.msg.empty_list_row</code>.
107      */
108     public static final String PROPERTY_STRING_EMPTYLISTROW_MESSAGE = "basic.msg.empty_list_row"; //$NON-NLS-1$
109 
110     /**
111      * property <code>basic.empty.showtable</code>.
112      */
113     public static final String PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE = "basic.empty.showtable"; //$NON-NLS-1$
114 
115     /**
116      * property <code>paging.banner.placement</code>.
117      */
118     public static final String PROPERTY_STRING_BANNER_PLACEMENT = "paging.banner.placement"; //$NON-NLS-1$
119 
120     /**
121      * property <code>error.msg.invalid_page</code>.
122      */
123     public static final String PROPERTY_STRING_PAGING_INVALIDPAGE = "error.msg.invalid_page"; //$NON-NLS-1$
124 
125     /**
126      * property <code>paging.banner.item_name</code>.
127      */
128     public static final String PROPERTY_STRING_PAGING_ITEM_NAME = "paging.banner.item_name"; //$NON-NLS-1$
129 
130     /**
131      * property <code>paging.banner.items_name</code>.
132      */
133     public static final String PROPERTY_STRING_PAGING_ITEMS_NAME = "paging.banner.items_name"; //$NON-NLS-1$
134 
135     /**
136      * property <code>paging.banner.no_items_found</code>.
137      */
138     public static final String PROPERTY_STRING_PAGING_NOITEMS = "paging.banner.no_items_found"; //$NON-NLS-1$
139 
140     /**
141      * property <code>paging.banner.one_item_found</code>.
142      */
143     public static final String PROPERTY_STRING_PAGING_FOUND_ONEITEM = "paging.banner.one_item_found"; //$NON-NLS-1$
144 
145     /**
146      * property <code>paging.banner.all_items_found</code>.
147      */
148     public static final String PROPERTY_STRING_PAGING_FOUND_ALLITEMS = "paging.banner.all_items_found"; //$NON-NLS-1$
149 
150     /**
151      * property <code>paging.banner.some_items_found</code>.
152      */
153     public static final String PROPERTY_STRING_PAGING_FOUND_SOMEITEMS = "paging.banner.some_items_found"; //$NON-NLS-1$
154 
155     /**
156      * property <code>paging.banner.group_size</code>.
157      */
158     public static final String PROPERTY_INT_PAGING_GROUPSIZE = "paging.banner.group_size"; //$NON-NLS-1$
159 
160     /**
161      * property <code>paging.banner.onepage</code>.
162      */
163     public static final String PROPERTY_STRING_PAGING_BANNER_ONEPAGE = "paging.banner.onepage"; //$NON-NLS-1$
164 
165     /**
166      * property <code>paging.banner.first</code>.
167      */
168     public static final String PROPERTY_STRING_PAGING_BANNER_FIRST = "paging.banner.first"; //$NON-NLS-1$
169 
170     /**
171      * property <code>paging.banner.last</code>.
172      */
173     public static final String PROPERTY_STRING_PAGING_BANNER_LAST = "paging.banner.last"; //$NON-NLS-1$
174 
175     /**
176      * property <code>paging.banner.full</code>.
177      */
178     public static final String PROPERTY_STRING_PAGING_BANNER_FULL = "paging.banner.full"; //$NON-NLS-1$
179 
180     /**
181      * property <code>paging.banner.page.link</code>.
182      */
183     public static final String PROPERTY_STRING_PAGING_PAGE_LINK = "paging.banner.page.link"; //$NON-NLS-1$
184 
185     /**
186      * property <code>paging.banner.page.selected</code>.
187      */
188     public static final String PROPERTY_STRING_PAGING_PAGE_SELECTED = "paging.banner.page.selected"; //$NON-NLS-1$
189 
190     /**
191      * property <code>paging.banner.page.separator</code>.
192      */
193     public static final String PROPERTY_STRING_PAGING_PAGE_SPARATOR = "paging.banner.page.separator"; //$NON-NLS-1$
194 
195     /**
196      * property <code>factory.requestHelper</code>.
197      */
198     public static final String PROPERTY_CLASS_REQUESTHELPERFACTORY = "factory.requestHelper"; //$NON-NLS-1$
199 
200     /**
201      * property <code>factory.decorators</code>.
202      */
203     public static final String PROPERTY_CLASS_DECORATORFACTORY = "factory.decorator"; //$NON-NLS-1$
204 
205     /**
206      * property <code>locale.provider</code>.
207      */
208     public static final String PROPERTY_CLASS_LOCALEPROVIDER = "locale.provider"; //$NON-NLS-1$
209 
210     /**
211      * property <code>locale.resolver</code>.
212      */
213     public static final String PROPERTY_CLASS_LOCALERESOLVER = "locale.resolver"; //$NON-NLS-1$
214 
215     /**
216      * property <code>css.tr.even</code>: holds the name of the css class for even rows. Defaults to
217      * <code>even</code>.
218      */
219     public static final String PROPERTY_CSS_TR_EVEN = "css.tr.even"; //$NON-NLS-1$
220 
221     /**
222      * property <code>css.tr.odd</code>: holds the name of the css class for odd rows. Defaults to <code>odd</code>.
223      */
224     public static final String PROPERTY_CSS_TR_ODD = "css.tr.odd"; //$NON-NLS-1$
225 
226     /**
227      * property <code>css.table</code>: holds the name of the css class added to the main table tag. By default no
228      * css class is added.
229      */
230     public static final String PROPERTY_CSS_TABLE = "css.table"; //$NON-NLS-1$
231 
232     /**
233      * property <code>css.th.sortable</code>: holds the name of the css class added to the the header of a sortable
234      * column. By default no css class is added.
235      */
236     public static final String PROPERTY_CSS_TH_SORTABLE = "css.th.sortable"; //$NON-NLS-1$
237 
238     /**
239      * property <code>css.th.sorted</code>: holds the name of the css class added to the the header of a sorted
240      * column. Defaults to <code>sorted</code>.
241      */
242     public static final String PROPERTY_CSS_TH_SORTED = "css.th.sorted"; //$NON-NLS-1$
243 
244     /**
245      * property <code>css.th.ascending</code>: holds the name of the css class added to the the header of a column
246      * sorted in ascending order. Defaults to <code>order1</code>.
247      */
248     public static final String PROPERTY_CSS_TH_SORTED_ASCENDING = "css.th.ascending"; //$NON-NLS-1$
249 
250     /**
251      * property <code>css.th.descending</code>: holds the name of the css class added to the the header of a column
252      * sorted in descending order. Defaults to <code>order2</code>.
253      */
254     public static final String PROPERTY_CSS_TH_SORTED_DESCENDING = "css.th.descending"; //$NON-NLS-1$
255 
256     /**
257      * prefix used for all the properties related to export ("export"). The full property name is <code>export.</code>
258      * <em>[export type]</em><code>.</code><em>[property name]</em>
259      */
260     public static final String PROPERTY_EXPORT_PREFIX = "export"; //$NON-NLS-1$
261 
262     /**
263      * prefix used to set the media decorator property name. The full property name is 
264      * <code>decorator.media.</code><em>[export type]</em>.
265      */
266     public static final String PROPERTY_DECORATOR_SUFFIX = "decorator"; //$NON-NLS-1$
267 
268     /**
269      * used to set the media decorator property name. The full property name is 
270      * <code>decorator.media.</code><em>[export type]</em>
271      */
272     public static final String PROPERTY_DECORATOR_MEDIA = "media"; //$NON-NLS-1$
273     
274     /**
275      * property <code>export.types</code>: holds the list of export available export types.
276      */
277     public static final String PROPERTY_EXPORTTYPES = "export.types"; //$NON-NLS-1$
278 
279     /**
280      * export property <code>label</code>.
281      */
282     public static final String EXPORTPROPERTY_STRING_LABEL = "label"; //$NON-NLS-1$
283 
284     /**
285      * export property <code>class</code>.
286      */
287     public static final String EXPORTPROPERTY_STRING_CLASS = "class"; //$NON-NLS-1$
288 
289     /**
290      * export property <code>include_header</code>.
291      */
292     public static final String EXPORTPROPERTY_BOOLEAN_EXPORTHEADER = "include_header"; //$NON-NLS-1$
293 
294     /**
295      * export property <code>filename</code>.
296      */
297     public static final String EXPORTPROPERTY_STRING_FILENAME = "filename"; //$NON-NLS-1$
298 
299     /**
300      * Property <code>pagination.sort.param</code>. If external pagination and sorting is used, it holds the name of
301      * the parameter used to hold the sort criterion in generated links
302      */
303     public static final String PROPERTY_STRING_PAGINATION_SORT_PARAM = "pagination.sort.param"; //$NON-NLS-1$
304 
305     /**
306      * Property <code>pagination.sortdirection.param</code>. If external pagination and sorting is used, it holds the
307      * name of the parameter used to hold the sort direction in generated links (asc or desc)
308      */
309     public static final String PROPERTY_STRING_PAGINATION_SORT_DIRECTION_PARAM = "pagination.sortdirection.param"; //$NON-NLS-1$
310 
311     /**
312      * Property <code>pagination.pagenumber.param</code>. If external pagination and sorting is used, it holds the
313      * name of the parameter used to hold the page number in generated links
314      */
315     public static final String PROPERTY_STRING_PAGINATION_PAGE_NUMBER_PARAM = "pagination.pagenumber.param"; //$NON-NLS-1$
316 
317     /**
318      * Property <code>pagination.searchid.param</code>. If external pagination and sorting is used, it holds the name
319      * of the parameter used to hold the search ID in generated links
320      */
321     public static final String PROPERTY_STRING_PAGINATION_SEARCH_ID_PARAM = "pagination.searchid.param"; //$NON-NLS-1$
322 
323     /**
324      * Property <code>pagination.sort.asc.value</code>. If external pagination and sorting is used, it holds the
325      * value of the parameter of the sort direction parameter for "ascending"
326      */
327     public static final String PROPERTY_STRING_PAGINATION_ASC_VALUE = "pagination.sort.asc.value"; //$NON-NLS-1$
328 
329     /**
330      * Property <code>pagination.sort.desc.value</code>. If external pagination and sorting is used, it holds the
331      * value of the parameter of the sort direction parameter for "descending"
332      */
333     public static final String PROPERTY_STRING_PAGINATION_DESC_VALUE = "pagination.sort.desc.value"; //$NON-NLS-1$
334 
335     /**
336      * Property <code>pagination.sort.skippagenumber</code>. If external pagination and sorting is used, it
337      * determines if the current page number must be added in sort links or not. If this property is true, it means that
338      * each click on a generated sort link will re-sort the list, and go back to the default page number. If it is
339      * false, each click on a generated sort link will re-sort the list, and ask the current page number.
340      */
341     public static final String PROPERTY_BOOLEAN_PAGINATION_SKIP_PAGE_NUMBER_IN_SORT = "pagination.sort.skippagenumber"; //$NON-NLS-1$
342 
343     /**
344      * Property <code>comparator.default</code>.  If present, will use use as the classname of the default comparator.
345      * Will be overriden by column level comparators.
346      */
347     public static final String PROPERTY_DEFAULT_COMPARATOR = "comparator.default"; //$NON-NLS-1$
348 
349     // </JBN>
350 
351     /**
352      * Separator char used in property names.
353      */
354     private static final char SEP = '.';
355 
356     /**
357      * logger.
358      */
359     private static Log log = LogFactory.getLog(TableProperties.class);
360 
361     /**
362      * The userProperties are local, non-default properties; these settings override the defaults from
363      * displaytag.properties and TableTag.properties.
364      */
365     private static Properties userProperties = new Properties();
366 
367     /**
368      * Configured resource provider. If no ResourceProvider is configured, an no-op one is used. This instance is
369      * initialized at first use and shared.
370      */
371     private static I18nResourceProvider resourceProvider;
372 
373     /**
374      * Configured locale resolver.
375      */
376     private static LocaleResolver localeResolver;
377 
378     /**
379      * TableProperties for each locale are loaded as needed, and cloned for public usage.
380      */
381     private static Map prototypes = new HashMap();
382 
383     /**
384      * Loaded properties (defaults from defaultProperties + custom from bundle).
385      */
386     private Properties properties;
387 
388     /**
389      * The locale for these properties.
390      */
391     private Locale locale;
392 
393     /**
394      * Cache for dinamically instantiated object (request factory, decorator factory).
395      */
396     private Map objectCache = new HashMap();
397 
398     /**
399      * Setter for I18nResourceProvider. A resource provider is usually set using displaytag properties, this accessor is
400      * needed for tests.
401      * @param provider I18nResourceProvider instance
402      */
403     protected static void setResourceProvider(I18nResourceProvider provider)
404     {
405         resourceProvider = provider;
406     }
407 
408     /**
409      * Setter for LocaleResolver. A locale resolver is usually set using displaytag properties, this accessor is needed
410      * for tests.
411      * @param resolver LocaleResolver instance
412      */
413     protected static void setLocaleResolver(LocaleResolver resolver)
414     {
415         localeResolver = resolver;
416     }
417 
418     /**
419      * Loads default properties (TableTag.properties).
420      * @return loaded properties
421      * @throws TablePropertiesLoadException if default properties file can't be found
422      */
423     private static Properties loadBuiltInProperties() throws TablePropertiesLoadException
424     {
425         Properties defaultProperties = new Properties();
426 
427         try
428         {
429             InputStream is = TableProperties.class.getResourceAsStream(DEFAULT_FILENAME);
430             if (is == null)
431             {
432                 throw new TablePropertiesLoadException(TableProperties.class, DEFAULT_FILENAME, null);
433             }
434             defaultProperties.load(is);
435         }
436         catch (IOException e)
437         {
438             throw new TablePropertiesLoadException(TableProperties.class, DEFAULT_FILENAME, e);
439         }
440 
441         return defaultProperties;
442     }
443 
444     /**
445      * Loads user properties (displaytag.properties) according to the given locale. User properties are not guarantee to
446      * exist, so the method can return <code>null</code> (no exception will be thrown).
447      * @param locale requested Locale
448      * @return loaded properties
449      */
450     private static ResourceBundle loadUserProperties(Locale locale)
451     {
452         ResourceBundle bundle = null;
453         try
454         {
455             bundle = ResourceBundle.getBundle(LOCAL_PROPERTIES, locale);
456         }
457         catch (MissingResourceException e)
458         {
459             // if no resource bundle is found, try using the context classloader
460             try
461             {
462                 bundle = ResourceBundle.getBundle(LOCAL_PROPERTIES, locale, Thread
463                     .currentThread()
464                     .getContextClassLoader());
465             }
466             catch (MissingResourceException mre)
467             {
468                 if (log.isDebugEnabled())
469                 {
470                     log.debug(Messages.getString("TableProperties.propertiesnotfound", //$NON-NLS-1$
471                         new Object[]{mre.getMessage()}));
472                 }
473             }
474         }
475 
476         return bundle;
477     }
478 
479     /**
480      * Returns the configured Locale Resolver. This method is called before the loading of localized properties.
481      * @return LocaleResolver instance.
482      * @throws TablePropertiesLoadException if the default <code>TableTag.properties</code> file is not found.
483      */
484     public static LocaleResolver getLocaleResolverInstance() throws TablePropertiesLoadException
485     {
486 
487         if (localeResolver == null)
488         {
489 
490             // special handling, table properties is not yet instantiated
491             String className = null;
492 
493             ResourceBundle defaultUserProperties = loadUserProperties(Locale.getDefault());
494 
495             // if available, user properties have higher precedence
496             if (defaultUserProperties != null)
497             {
498                 try
499                 {
500                     className = defaultUserProperties.getString(PROPERTY_CLASS_LOCALERESOLVER);
501                 }
502                 catch (MissingResourceException e)
503                 {
504                     // no problem
505                 }
506             }
507 
508             // still null? load defaults
509             if (className == null)
510             {
511                 Properties defaults = loadBuiltInProperties();
512                 className = defaults.getProperty(PROPERTY_CLASS_LOCALERESOLVER);
513             }
514 
515             if (className != null)
516             {
517                 try
518                 {
519                     Class classProperty = ReflectHelper.classForName(className);
520                     localeResolver = (LocaleResolver) classProperty.newInstance();
521 
522                     log.info(Messages.getString("TableProperties.classinitializedto", //$NON-NLS-1$
523                         new Object[]{ClassUtils.getShortClassName(LocaleResolver.class), className}));
524                 }
525                 catch (Throwable e)
526                 {
527                     log.warn(Messages.getString("TableProperties.errorloading", //$NON-NLS-1$
528                         new Object[]{
529                             ClassUtils.getShortClassName(LocaleResolver.class),
530                             e.getClass().getName(),
531                             e.getMessage()}));
532                 }
533             }
534             else
535             {
536                 log.info(Messages.getString("TableProperties.noconfigured", //$NON-NLS-1$
537                     new Object[]{ClassUtils.getShortClassName(LocaleResolver.class)}));
538             }
539 
540             // still null?
541             if (localeResolver == null)
542             {
543                 // fallback locale resolver
544                 localeResolver = new LocaleResolver()
545                 {
546 
547                     public Locale resolveLocale(HttpServletRequest request)
548                     {
549                         return request.getLocale();
550                     }
551                 };
552             }
553         }
554 
555         return localeResolver;
556     }
557 
558     /**
559      * Initialize a new TableProperties loading the default properties file and the user defined one. There is no
560      * caching used here, caching is assumed to occur in the getInstance factory method.
561      * @param myLocale the locale we are in
562      * @throws TablePropertiesLoadException for errors during loading of properties files
563      */
564     private TableProperties(Locale myLocale) throws TablePropertiesLoadException
565     {
566         this.locale = myLocale;
567         // default properties will not change unless this class is reloaded
568         Properties defaultProperties = loadBuiltInProperties();
569 
570         properties = new Properties(defaultProperties);
571         addProperties(myLocale);
572 
573         // Now copy in the user properties (properties file set by calling setUserProperties()).
574         // note setUserProperties() MUST BE CALLED before the first TableProperties instantation
575         Enumeration keys = userProperties.keys();
576         while (keys.hasMoreElements())
577         {
578             String key = (String) keys.nextElement();
579             if (key != null)
580             {
581                 properties.setProperty(key, (String) userProperties.get(key));
582             }
583         }
584     }
585 
586     /**
587      * Try to load the properties from the local properties file, displaytag.properties, and merge them into the
588      * existing properties.
589      * @param userLocale the locale from which the properties are to be loaded
590      */
591     private void addProperties(Locale userLocale)
592     {
593         ResourceBundle bundle = loadUserProperties(userLocale);
594 
595         if (bundle != null)
596         {
597             Enumeration keys = bundle.getKeys();
598             while (keys.hasMoreElements())
599             {
600                 String key = (String) keys.nextElement();
601                 properties.setProperty(key, bundle.getString(key));
602             }
603         }
604     }
605 
606     /**
607      * Clones the properties as well.
608      * @return a new clone of oneself
609      */
610     protected Object clone()
611     {
612         TableProperties twin;
613         try
614         {
615             twin = (TableProperties) super.clone();
616         }
617         catch (CloneNotSupportedException e)
618         {
619             // should never happen
620             throw new UnhandledException(e);
621         }
622         twin.properties = (Properties) this.properties.clone();
623         return twin;
624     }
625 
626     /**
627      * Returns a new TableProperties instance for the given locale.
628      * @param request HttpServletRequest needed to extract the locale to use. If null the default locale will be used.
629      * @return TableProperties instance
630      */
631     public static TableProperties getInstance(HttpServletRequest request)
632     {
633         Locale locale;
634         if (request != null)
635         {
636             locale = getLocaleResolverInstance().resolveLocale(request);
637         }
638         else
639         {
640             // for some configuration parameters locale doesn't matter
641             locale = Locale.getDefault();
642         }
643 
644         TableProperties props = (TableProperties) prototypes.get(locale);
645         if (props == null)
646         {
647             TableProperties lprops = new TableProperties(locale);
648             prototypes.put(locale, lprops);
649             props = lprops;
650         }
651         return (TableProperties) props.clone();
652     }
653 
654     /**
655      * Unload all cached properties. This will not clear properties set by by setUserProperties; you must clear those
656      * manually.
657      */
658     public static void clearProperties()
659     {
660         prototypes.clear();
661     }
662 
663     /**
664      * Local, non-default properties; these settings override the defaults from displaytag.properties and
665      * TableTag.properties. Please note that the values are copied in, so that multiple calls with non-overlapping
666      * properties will be merged, not overwritten. Note: setUserProperties() MUST BE CALLED before the first
667      * TableProperties instantation.
668      * @param overrideProperties - The local, non-default properties
669      */
670     public static void setUserProperties(Properties overrideProperties)
671     {
672         // copy keys here, so that this can be invoked more than once from different sources.
673         // if default properties are not yet loaded they will be copied in constructor
674         Enumeration keys = overrideProperties.keys();
675         while (keys.hasMoreElements())
676         {
677             String key = (String) keys.nextElement();
678             if (key != null)
679             {
680                 userProperties.setProperty(key, (String) overrideProperties.get(key));
681             }
682         }
683     }
684 
685     /**
686      * The locale for which these properties are intended.
687      * @return the locale
688      */
689     public Locale getLocale()
690     {
691         return locale;
692     }
693 
694     /**
695      * Getter for the <code>PROPERTY_STRING_PAGING_INVALIDPAGE</code> property.
696      * @return String
697      */
698     public String getPagingInvalidPage()
699     {
700         return getProperty(PROPERTY_STRING_PAGING_INVALIDPAGE);
701     }
702 
703     /**
704      * Getter for the <code>PROPERTY_STRING_PAGING_ITEM_NAME</code> property.
705      * @return String
706      */
707     public String getPagingItemName()
708     {
709         return getProperty(PROPERTY_STRING_PAGING_ITEM_NAME);
710     }
711 
712     /**
713      * Getter for the <code>PROPERTY_STRING_PAGING_ITEMS_NAME</code> property.
714      * @return String
715      */
716     public String getPagingItemsName()
717     {
718         return getProperty(PROPERTY_STRING_PAGING_ITEMS_NAME);
719     }
720 
721     /**
722      * Getter for the <code>PROPERTY_STRING_PAGING_NOITEMS</code> property.
723      * @return String
724      */
725     public String getPagingFoundNoItems()
726     {
727         return getProperty(PROPERTY_STRING_PAGING_NOITEMS);
728     }
729 
730     /**
731      * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_ONEITEM</code> property.
732      * @return String
733      */
734     public String getPagingFoundOneItem()
735     {
736         return getProperty(PROPERTY_STRING_PAGING_FOUND_ONEITEM);
737     }
738 
739     /**
740      * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_ALLITEMS</code> property.
741      * @return String
742      */
743     public String getPagingFoundAllItems()
744     {
745         return getProperty(PROPERTY_STRING_PAGING_FOUND_ALLITEMS);
746     }
747 
748     /**
749      * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_SOMEITEMS</code> property.
750      * @return String
751      */
752     public String getPagingFoundSomeItems()
753     {
754         return getProperty(PROPERTY_STRING_PAGING_FOUND_SOMEITEMS);
755     }
756 
757     /**
758      * Getter for the <code>PROPERTY_INT_PAGING_GROUPSIZE</code> property.
759      * @return int
760      */
761     public int getPagingGroupSize()
762     {
763         // default size is 8
764         return getIntProperty(PROPERTY_INT_PAGING_GROUPSIZE, 8);
765     }
766 
767     /**
768      * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_ONEPAGE</code> property.
769      * @return String
770      */
771     public String getPagingBannerOnePage()
772     {
773         return getProperty(PROPERTY_STRING_PAGING_BANNER_ONEPAGE);
774     }
775 
776     /**
777      * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_FIRST</code> property.
778      * @return String
779      */
780     public String getPagingBannerFirst()
781     {
782         return getProperty(PROPERTY_STRING_PAGING_BANNER_FIRST);
783     }
784 
785     /**
786      * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_LAST</code> property.
787      * @return String
788      */
789     public String getPagingBannerLast()
790     {
791         return getProperty(PROPERTY_STRING_PAGING_BANNER_LAST);
792     }
793 
794     /**
795      * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_FULL</code> property.
796      * @return String
797      */
798     public String getPagingBannerFull()
799     {
800         return getProperty(PROPERTY_STRING_PAGING_BANNER_FULL);
801     }
802 
803     /**
804      * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_LINK</code> property.
805      * @return String
806      */
807     public String getPagingPageLink()
808     {
809         return getProperty(PROPERTY_STRING_PAGING_PAGE_LINK);
810     }
811 
812     /**
813      * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_SELECTED</code> property.
814      * @return String
815      */
816     public String getPagingPageSelected()
817     {
818         return getProperty(PROPERTY_STRING_PAGING_PAGE_SELECTED);
819     }
820 
821     /**
822      * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_SPARATOR</code> property.
823      * @return String
824      */
825     public String getPagingPageSeparator()
826     {
827         return getProperty(PROPERTY_STRING_PAGING_PAGE_SPARATOR);
828     }
829 
830     /**
831      * Is the given export option enabled?
832      * @param exportType instance of MediaTypeEnum
833      * @return boolean true if export is enabled
834      */
835     public boolean getAddExport(MediaTypeEnum exportType)
836     {
837         return getBooleanProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName());
838     }
839 
840     /**
841      * Should headers be included in given export type?
842      * @param exportType instance of MediaTypeEnum
843      * @return boolean true if export should include headers
844      */
845     public boolean getExportHeader(MediaTypeEnum exportType)
846     {
847         return getBooleanProperty(PROPERTY_EXPORT_PREFIX
848             + SEP
849             + exportType.getName()
850             + SEP
851             + EXPORTPROPERTY_BOOLEAN_EXPORTHEADER);
852     }
853 
854     /**
855      * Returns the label for the given export option.
856      * @param exportType instance of MediaTypeEnum
857      * @return String label
858      */
859     public String getExportLabel(MediaTypeEnum exportType)
860     {
861         return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName() + SEP + EXPORTPROPERTY_STRING_LABEL);
862     }
863 
864     /**
865      * Returns the file name for the given media. Can be null
866      * @param exportType instance of MediaTypeEnum
867      * @return String filename
868      */
869     public String getExportFileName(MediaTypeEnum exportType)
870     {
871         return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName() + SEP + EXPORTPROPERTY_STRING_FILENAME);
872     }
873 
874     /**
875      * Getter for the <code>PROPERTY_BOOLEAN_EXPORTDECORATED</code> property.
876      * @return boolean <code>true</code> if decorators should be used in exporting
877      */
878     public boolean getExportDecorated()
879     {
880         return getBooleanProperty(PROPERTY_BOOLEAN_EXPORTDECORATED);
881     }
882 
883     /**
884      * Getter for the <code>PROPERTY_STRING_EXPORTBANNER</code> property.
885      * @return String
886      */
887     public String getExportBanner()
888     {
889         return getProperty(PROPERTY_STRING_EXPORTBANNER);
890     }
891 
892     /**
893      * Getter for the <code>PROPERTY_STRING_EXPORTBANNER_SEPARATOR</code> property.
894      * @return String
895      */
896     public String getExportBannerSeparator()
897     {
898         return getProperty(PROPERTY_STRING_EXPORTBANNER_SEPARATOR);
899     }
900 
901     /**
902      * Getter for the <code>PROPERTY_BOOLEAN_SHOWHEADER</code> property.
903      * @return boolean
904      */
905     public boolean getShowHeader()
906     {
907         return getBooleanProperty(PROPERTY_BOOLEAN_SHOWHEADER);
908     }
909 
910     /**
911      * Getter for the <code>PROPERTY_STRING_EMPTYLIST_MESSAGE</code> property.
912      * @return String
913      */
914     public String getEmptyListMessage()
915     {
916         return getProperty(PROPERTY_STRING_EMPTYLIST_MESSAGE);
917     }
918 
919     /**
920      * Getter for the <code>PROPERTY_STRING_EMPTYLISTROW_MESSAGE</code> property.
921      * @return String
922      */
923     public String getEmptyListRowMessage()
924     {
925         return getProperty(PROPERTY_STRING_EMPTYLISTROW_MESSAGE);
926     }
927 
928     /**
929      * Getter for the <code>PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE</code> property.
930      * @return boolean <code>true</code> if table should be displayed also if no items are found
931      */
932     public boolean getEmptyListShowTable()
933     {
934         return getBooleanProperty(PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE);
935     }
936 
937     /**
938      * Getter for the <code>PROPERTY_STRING_EXPORTAMOUNT</code> property.
939      * @return boolean <code>true</code> if <code>export.amount</code> is <code>list</code>
940      */
941     public boolean getExportFullList()
942     {
943         return "list".equals(getProperty(PROPERTY_STRING_EXPORTAMOUNT)); //$NON-NLS-1$
944     }
945 
946     /**
947      * Getter for the <code>PROPERTY_STRING_SORTAMOUNT</code> property.
948      * @return boolean <code>true</code> if <code>sort.amount</code> is <code>list</code>
949      */
950     public boolean getSortFullList()
951     {
952         return "list".equals(getProperty(PROPERTY_STRING_SORTAMOUNT)); //$NON-NLS-1$
953     }
954 
955     /**
956      * Should paging banner be added before the table?
957      * @return boolean
958      */
959     public boolean getAddPagingBannerTop()
960     {
961         String placement = getProperty(PROPERTY_STRING_BANNER_PLACEMENT);
962         return "top".equals(placement) || "both".equals(placement); //$NON-NLS-1$ //$NON-NLS-2$
963     }
964 
965     /**
966      * Should paging banner be added after the table?
967      * @return boolean
968      */
969     public boolean getAddPagingBannerBottom()
970     {
971         String placement = getProperty(PROPERTY_STRING_BANNER_PLACEMENT);
972         return "bottom".equals(placement) || "both".equals(placement); //$NON-NLS-1$ //$NON-NLS-2$
973     }
974 
975     /**
976      * Returns the appropriate css class for a table row.
977      * @param rowNumber row number
978      * @return the value of <code>PROPERTY_CSS_TR_EVEN</code> if rowNumber is even or <code>PROPERTY_CSS_TR_ODD</code>
979      * if rowNumber is odd.
980      */
981     public String getCssRow(int rowNumber)
982     {
983         return getProperty((rowNumber % 2 == 0) ? PROPERTY_CSS_TR_ODD : PROPERTY_CSS_TR_EVEN);
984     }
985 
986     /**
987      * Returns the appropriate css class for a sorted column header.
988      * @param ascending <code>true</code> if column is sorded in ascending order.
989      * @return the value of <code>PROPERTY_CSS_TH_SORTED_ASCENDING</code> if column is sorded in ascending order or
990      * <code>PROPERTY_CSS_TH_SORTED_DESCENDING</code> if column is sorded in descending order.
991      */
992     public String getCssOrder(boolean ascending)
993     {
994         return getProperty(ascending ? PROPERTY_CSS_TH_SORTED_ASCENDING : PROPERTY_CSS_TH_SORTED_DESCENDING);
995     }
996 
997     /**
998      * Returns the configured css class for a sorted column header.
999      * @return the value of <code>PROPERTY_CSS_TH_SORTED</code>
1000      */
1001     public String getCssSorted()
1002     {
1003         return getProperty(PROPERTY_CSS_TH_SORTED);
1004     }
1005 
1006     /**
1007      * Returns the configured css class for the main table tag.
1008      * @return the value of <code>PROPERTY_CSS_TABLE</code>
1009      */
1010     public String getCssTable()
1011     {
1012         return getProperty(PROPERTY_CSS_TABLE);
1013     }
1014 
1015     /**
1016      * Returns the configured css class for a sortable column header.
1017      * @return the value of <code>PROPERTY_CSS_TH_SORTABLE</code>
1018      */
1019     public String getCssSortable()
1020     {
1021         return getProperty(PROPERTY_CSS_TH_SORTABLE);
1022     }
1023 
1024     /**
1025      * Returns the configured list of media.
1026      * @return the value of <code>PROPERTY_EXPORTTYPES</code>
1027      */
1028     public String[] getExportTypes()
1029     {
1030         String list = getProperty(PROPERTY_EXPORTTYPES);
1031         if (list == null)
1032         {
1033             return new String[0];
1034         }
1035 
1036         return StringUtils.split(list);
1037     }
1038 
1039     /**
1040      * Returns the class responsible for the given export.
1041      * @param exportName export name
1042      * @return String classname
1043      */
1044     public String getExportClass(String exportName)
1045     {
1046         return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportName + SEP + EXPORTPROPERTY_STRING_CLASS);
1047     }
1048 
1049     /**
1050      * Returns an instance of configured requestHelperFactory.
1051      * @return RequestHelperFactory instance.
1052      * @throws FactoryInstantiationException if unable to load or instantiate the configurated class.
1053      */
1054     public RequestHelperFactory getRequestHelperFactoryInstance() throws FactoryInstantiationException
1055     {
1056         Object loadedObject = getClassPropertyInstance(PROPERTY_CLASS_REQUESTHELPERFACTORY);
1057 
1058         // should not be null, but avoid errors just in case... see DISPL-148
1059         if (loadedObject == null)
1060         {
1061             return new DefaultRequestHelperFactory();
1062         }
1063 
1064         try
1065         {
1066             return (RequestHelperFactory) loadedObject;
1067         }
1068         catch (ClassCastException e)
1069         {
1070             throw new FactoryInstantiationException(getClass(), PROPERTY_CLASS_REQUESTHELPERFACTORY, loadedObject
1071                 .getClass()
1072                 .getName(), e);
1073         }
1074     }
1075 
1076     /**
1077      * Returns an instance of configured DecoratorFactory.
1078      * @return DecoratorFactory instance.
1079      * @throws FactoryInstantiationException if unable to load or instantiate the configurated class.
1080      */
1081     public DecoratorFactory getDecoratorFactoryInstance() throws FactoryInstantiationException
1082     {
1083         Object loadedObject = getClassPropertyInstance(PROPERTY_CLASS_DECORATORFACTORY);
1084 
1085         if (loadedObject == null)
1086         {
1087             return new DefaultDecoratorFactory();
1088         }
1089 
1090         try
1091         {
1092             return (DecoratorFactory) loadedObject;
1093         }
1094         catch (ClassCastException e)
1095         {
1096             throw new FactoryInstantiationException(getClass(), PROPERTY_CLASS_DECORATORFACTORY, loadedObject
1097                 .getClass()
1098                 .getName(), e);
1099         }
1100     }
1101 
1102     public String getPaginationSortParam()
1103     {
1104         String result = getProperty(PROPERTY_STRING_PAGINATION_SORT_PARAM);
1105         if (result == null)
1106         {
1107             result = "sort";
1108         }
1109         return result;
1110     }
1111 
1112     public String getPaginationPageNumberParam()
1113     {
1114         String result = getProperty(PROPERTY_STRING_PAGINATION_PAGE_NUMBER_PARAM);
1115         if (result == null)
1116         {
1117             result = "page";
1118         }
1119         return result;
1120     }
1121 
1122     public String getPaginationSortDirectionParam()
1123     {
1124         String result = getProperty(PROPERTY_STRING_PAGINATION_SORT_DIRECTION_PARAM);
1125         if (result == null)
1126         {
1127             result = "dir";
1128         }
1129         return result;
1130     }
1131 
1132     public String getPaginationSearchIdParam()
1133     {
1134         String result = getProperty(PROPERTY_STRING_PAGINATION_SEARCH_ID_PARAM);
1135         if (result == null)
1136         {
1137             result = "searchId";
1138         }
1139         return result;
1140     }
1141 
1142     public String getPaginationAscValue()
1143     {
1144         String result = getProperty(PROPERTY_STRING_PAGINATION_ASC_VALUE);
1145         if (result == null)
1146         {
1147             result = "asc";
1148         }
1149         return result;
1150     }
1151 
1152     public String getPaginationDescValue()
1153     {
1154         String result = getProperty(PROPERTY_STRING_PAGINATION_DESC_VALUE);
1155         if (result == null)
1156         {
1157             result = "desc";
1158         }
1159         return result;
1160     }
1161 
1162     public boolean getPaginationSkipPageNumberInSort()
1163     {
1164         String s = getProperty(PROPERTY_BOOLEAN_PAGINATION_SKIP_PAGE_NUMBER_IN_SORT);
1165         if (s == null)
1166         {
1167             return true;
1168         }
1169         else
1170         {
1171             return getBooleanProperty(PROPERTY_BOOLEAN_PAGINATION_SKIP_PAGE_NUMBER_IN_SORT);
1172         }
1173     }
1174 
1175     // </JBN>
1176 
1177     /**
1178      * Returns the configured resource provider instance. If necessary instantiate the resource provider from config and
1179      * then keep a cached instance.
1180      * @return I18nResourceProvider instance.
1181      * @see I18nResourceProvider
1182      */
1183     public I18nResourceProvider geResourceProvider()
1184     {
1185         String className = getProperty(PROPERTY_CLASS_LOCALEPROVIDER);
1186 
1187         if (resourceProvider == null)
1188         {
1189             if (className != null)
1190             {
1191                 try
1192                 {
1193                     Class classProperty = ReflectHelper.classForName(className);
1194                     resourceProvider = (I18nResourceProvider) classProperty.newInstance();
1195 
1196                     log.info(Messages.getString("TableProperties.classinitializedto", //$NON-NLS-1$
1197                         new Object[]{ClassUtils.getShortClassName(I18nResourceProvider.class), className}));
1198                 }
1199                 catch (Throwable e)
1200                 {
1201                     log.warn(Messages.getString("TableProperties.errorloading", //$NON-NLS-1$
1202                         new Object[]{
1203                             ClassUtils.getShortClassName(I18nResourceProvider.class),
1204                             e.getClass().getName(),
1205                             e.getMessage()}));
1206                 }
1207             }
1208             else
1209             {
1210                 log.info(Messages.getString("TableProperties.noconfigured", //$NON-NLS-1$
1211                     new Object[]{ClassUtils.getShortClassName(I18nResourceProvider.class)}));
1212             }
1213 
1214             // still null?
1215             if (resourceProvider == null)
1216             {
1217                 // fallback provider, no i18n
1218                 resourceProvider = new I18nResourceProvider()
1219                 {
1220 
1221                     // Always returns null
1222                     public String getResource(String titleKey, String property, Tag tag, PageContext context)
1223                     {
1224                         return null;
1225                     }
1226                 };
1227             }
1228         }
1229 
1230         return resourceProvider;
1231     }
1232 
1233     /**
1234      * Reads a String property.
1235      * @param key property name
1236      * @return property value or <code>null</code> if property is not found
1237      */
1238     private String getProperty(String key)
1239     {
1240         return this.properties.getProperty(key);
1241     }
1242 
1243     /**
1244      * Sets a property.
1245      * @param key property name
1246      * @param value property value
1247      */
1248     public void setProperty(String key, String value)
1249     {
1250         this.properties.setProperty(key, value);
1251     }
1252 
1253     /**
1254      * Reads a boolean property.
1255      * @param key property name
1256      * @return boolean <code>true</code> if the property value is "true", <code>false</code> for any other value.
1257      */
1258     private boolean getBooleanProperty(String key)
1259     {
1260         return Boolean.TRUE.toString().equals(getProperty(key));
1261     }
1262 
1263     /**
1264      * Returns an instance of a configured Class. Returns a configured Class instantiated
1265      * callingClass.forName([configuration value]).
1266      * @param key configuration key
1267      * @return instance of configured class
1268      * @throws FactoryInstantiationException if unable to load or instantiate the configurated class.
1269      */
1270     private Object getClassPropertyInstance(String key) throws FactoryInstantiationException
1271     {
1272         Object instance = objectCache.get(key);
1273         if (instance != null)
1274         {
1275             return instance;
1276         }
1277 
1278         String className = getProperty(key);
1279 
1280         // shouldn't be null, but better check it
1281         if (className == null)
1282         {
1283             return null;
1284         }
1285 
1286         try
1287         {
1288             Class classProperty = ReflectHelper.classForName(className);
1289             instance = classProperty.newInstance();
1290             objectCache.put(key, instance);
1291             return instance;
1292         }
1293         catch (Exception e)
1294         {
1295             throw new FactoryInstantiationException(getClass(), key, className, e);
1296         }
1297     }
1298 
1299     /**
1300      * Reads an int property.
1301      * @param key property name
1302      * @param defaultValue default value returned if property is not found or not a valid int value
1303      * @return property value
1304      */
1305     private int getIntProperty(String key, int defaultValue)
1306     {
1307         try
1308         {
1309             return Integer.parseInt(getProperty(key));
1310         }
1311         catch (NumberFormatException e)
1312         {
1313             // Don't care, use default
1314             log.warn(Messages.getString("TableProperties.invalidvalue", //$NON-NLS-1$
1315                 new Object[]{key, getProperty(key), new Integer(defaultValue)}));
1316         }
1317 
1318         return defaultValue;
1319     }
1320 
1321     /**
1322      * Obtain the name of the decorator configured for a given media type.
1323      * @param thatEnum A media type
1324      * @return The name of the decorator configured for a given media type.
1325      * @deprecated Use getMediaTypeDecoratorName instead.
1326      */
1327     public String getExportDecoratorName(MediaTypeEnum thatEnum)
1328     {
1329         return getProperty(PROPERTY_EXPORT_PREFIX + SEP + thatEnum + SEP + PROPERTY_DECORATOR_SUFFIX);
1330     }
1331 
1332     /**
1333      * Obtain the name of the decorator configured for a given media type.
1334      * @param thatEnum A media type
1335      * @return The name of the decorator configured for a given media type.
1336      */
1337 	public String getMediaTypeDecoratorName(MediaTypeEnum thatEnum)
1338 	{
1339         return getProperty(PROPERTY_DECORATOR_SUFFIX + SEP + PROPERTY_DECORATOR_MEDIA + SEP + thatEnum);
1340 	}
1341 
1342     public Comparator getDefaultComparator()
1343     {
1344         String className = getProperty(PROPERTY_DEFAULT_COMPARATOR);  
1345         if (className != null)
1346         {
1347             try
1348             {
1349                 Class classProperty = ReflectHelper.classForName(className);
1350                 return (Comparator) classProperty.newInstance();
1351             }
1352             catch (Throwable e)
1353             {
1354                 log.warn(Messages.getString("TableProperties.errorloading", //$NON-NLS-1$
1355                     new Object[]{
1356                         ClassUtils.getShortClassName(Comparator.class),
1357                         e.getClass().getName(),
1358                         e.getMessage()}));
1359             }
1360         }
1361         return new DefaultComparator(Collator.getInstance(getLocale()));
1362     }
1363 }