/**
 * Licensed under the Artistic License; you may not use this file
 * except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://displaytag.sourceforge.net/license.html
 *
 * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */
package org.displaytag.properties;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.Tag;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.displaytag.Messages;
import org.displaytag.exception.FactoryInstantiationException;
import org.displaytag.exception.TablePropertiesLoadException;
import org.displaytag.exception.WrappedRuntimeException;
import org.displaytag.localization.I18nResourceProvider;
import org.displaytag.localization.LocaleResolver;
import org.displaytag.util.DefaultRequestHelperFactory;
import org.displaytag.util.ReflectHelper;
import org.displaytag.util.RequestHelperFactory;


/**
 * The properties used by the Table tags. The properties are loaded in the following order, in increasing order of
 * priority. The locale of getInstance() is used to determine the locale of the property file to use; if the key
 * required does not exist in the specified file, the key will be loaded from a more general property file.
 * <ol>
 * <li>First, from the TableTag.properties included with the DisplayTag distribution.</li>
 * <li>Then, from the file displaytag.properties, if it is present; these properties are intended to be set by the user
 * for sitewide application. Messages are gathered according to the Locale of the property file.</li>
 * <li>Finally, if this class has a userProperties defined, all of the properties from that Properties object are
 * copied in as well.</li>
 * </ol>
 * @author Fabrizio Giustina
 * @author rapruitt
 * @version $Revision: 1.1 $ ($Author: clmg $)
 */
public final class TableProperties implements Cloneable
{

    /**
     * name of the default properties file name ("TableTag.properties").
     */
    public static final String DEFAULT_FILENAME = "TableTag.properties"; //$NON-NLS-1$

    /**
     * The name of the local properties file that is searched for on the classpath. Settings in this file will override
     * the defaults loaded from TableTag.properties.
     */
    public static final String LOCAL_PROPERTIES = "displaytag"; //$NON-NLS-1$

    /**
     * property <code>export.banner</code>.
     */
    public static final String PROPERTY_STRING_EXPORTBANNER = "export.banner"; //$NON-NLS-1$

    /**
     * property <code>export.banner.sepchar</code>.
     */
    public static final String PROPERTY_STRING_EXPORTBANNER_SEPARATOR = "export.banner.sepchar"; //$NON-NLS-1$

    /**
     * property <code>export.decorated</code>.
     */
    public static final String PROPERTY_BOOLEAN_EXPORTDECORATED = "export.decorated"; //$NON-NLS-1$

    /**
     * property <code>export.amount</code>.
     */
    public static final String PROPERTY_STRING_EXPORTAMOUNT = "export.amount"; //$NON-NLS-1$

    /**
     * property <code>sort.amount</code>.
     */
    public static final String PROPERTY_STRING_SORTAMOUNT = "sort.amount"; //$NON-NLS-1$

    /**
     * property <code>basic.show.header</code>.
     */
    public static final String PROPERTY_BOOLEAN_SHOWHEADER = "basic.show.header"; //$NON-NLS-1$

    /**
     * property <code>basic.msg.empty_list</code>.
     */
    public static final String PROPERTY_STRING_EMPTYLIST_MESSAGE = "basic.msg.empty_list"; //$NON-NLS-1$

    /**
     * property <code>basic.msg.empty_list_row</code>.
     */
    public static final String PROPERTY_STRING_EMPTYLISTROW_MESSAGE = "basic.msg.empty_list_row"; //$NON-NLS-1$

    /**
     * property <code>basic.empty.showtable</code>.
     */
    public static final String PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE = "basic.empty.showtable"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.placement</code>.
     */
    public static final String PROPERTY_STRING_BANNER_PLACEMENT = "paging.banner.placement"; //$NON-NLS-1$

    /**
     * property <code>error.msg.invalid_page</code>.
     */
    public static final String PROPERTY_STRING_PAGING_INVALIDPAGE = "error.msg.invalid_page"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.item_name</code>.
     */
    public static final String PROPERTY_STRING_PAGING_ITEM_NAME = "paging.banner.item_name"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.items_name</code>.
     */
    public static final String PROPERTY_STRING_PAGING_ITEMS_NAME = "paging.banner.items_name"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.no_items_found</code>.
     */
    public static final String PROPERTY_STRING_PAGING_NOITEMS = "paging.banner.no_items_found"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.one_item_found</code>.
     */
    public static final String PROPERTY_STRING_PAGING_FOUND_ONEITEM = "paging.banner.one_item_found"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.all_items_found</code>.
     */
    public static final String PROPERTY_STRING_PAGING_FOUND_ALLITEMS = "paging.banner.all_items_found"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.some_items_found</code>.
     */
    public static final String PROPERTY_STRING_PAGING_FOUND_SOMEITEMS = "paging.banner.some_items_found"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.group_size</code>.
     */
    public static final String PROPERTY_INT_PAGING_GROUPSIZE = "paging.banner.group_size"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.onepage</code>.
     */
    public static final String PROPERTY_STRING_PAGING_BANNER_ONEPAGE = "paging.banner.onepage"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.first</code>.
     */
    public static final String PROPERTY_STRING_PAGING_BANNER_FIRST = "paging.banner.first"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.last</code>.
     */
    public static final String PROPERTY_STRING_PAGING_BANNER_LAST = "paging.banner.last"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.full</code>.
     */
    public static final String PROPERTY_STRING_PAGING_BANNER_FULL = "paging.banner.full"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.page.link</code>.
     */
    public static final String PROPERTY_STRING_PAGING_PAGE_LINK = "paging.banner.page.link"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.page.selected</code>.
     */
    public static final String PROPERTY_STRING_PAGING_PAGE_SELECTED = "paging.banner.page.selected"; //$NON-NLS-1$

    /**
     * property <code>paging.banner.page.separator</code>.
     */
    public static final String PROPERTY_STRING_PAGING_PAGE_SPARATOR = "paging.banner.page.separator"; //$NON-NLS-1$

    /**
     * property <code>factory.requestHelper</code>.
     */
    public static final String PROPERTY_CLASS_REQUESTHELPERFACTORY = "factory.requestHelper"; //$NON-NLS-1$

    /**
     * property <code>locale.provider</code>.
     */
    public static final String PROPERTY_CLASS_LOCALEPROVIDER = "locale.provider"; //$NON-NLS-1$

    /**
     * property <code>locale.resolver</code>.
     */
    public static final String PROPERTY_CLASS_LOCALERESOLVER = "locale.resolver"; //$NON-NLS-1$

    /**
     * property <code>css.tr.even</code>: holds the name of the css class for even rows. Defaults to
     * <code>even</code>.
     */
    public static final String PROPERTY_CSS_TR_EVEN = "css.tr.even"; //$NON-NLS-1$

    /**
     * property <code>css.tr.odd</code>: holds the name of the css class for odd rows. Defaults to <code>odd</code>.
     */
    public static final String PROPERTY_CSS_TR_ODD = "css.tr.odd"; //$NON-NLS-1$

    /**
     * property <code>css.table</code>: holds the name of the css class added to the main table tag. By default no
     * css class is added.
     */
    public static final String PROPERTY_CSS_TABLE = "css.table"; //$NON-NLS-1$

    /**
     * property <code>css.th.sortable</code>: holds the name of the css class added to the the header of a sortable
     * column. By default no css class is added.
     */
    public static final String PROPERTY_CSS_TH_SORTABLE = "css.th.sortable"; //$NON-NLS-1$

    /**
     * property <code>css.th.sorted</code>: holds the name of the css class added to the the header of a sorted
     * column. Defaults to <code>sorted</code>.
     */
    public static final String PROPERTY_CSS_TH_SORTED = "css.th.sorted"; //$NON-NLS-1$

    /**
     * property <code>css.th.ascending</code>: holds the name of the css class added to the the header of a column
     * sorted in ascending order. Defaults to <code>order1</code>.
     */
    public static final String PROPERTY_CSS_TH_SORTED_ASCENDING = "css.th.ascending"; //$NON-NLS-1$

    /**
     * property <code>css.th.descending</code>: holds the name of the css class added to the the header of a column
     * sorted in descending order. Defaults to <code>order2</code>.
     */
    public static final String PROPERTY_CSS_TH_SORTED_DESCENDING = "css.th.descending"; //$NON-NLS-1$

    /**
     * prefix used for all the properties related to export ("export"). The full property name is <code>export.</code>
     * <em>[export type]</em><code>.</code><em>[property name]</em>
     */
    public static final String PROPERTY_EXPORT_PREFIX = "export"; //$NON-NLS-1$

    /**
     * property <code>export.types</code>: holds the list of export available export types.
     */
    public static final String PROPERTY_EXPORTTYPES = "export.types"; //$NON-NLS-1$

    /**
     * export property <code>label</code>.
     */
    public static final String EXPORTPROPERTY_STRING_LABEL = "label"; //$NON-NLS-1$

    /**
     * export property <code>class</code>.
     */
    public static final String EXPORTPROPERTY_STRING_CLASS = "class"; //$NON-NLS-1$

    /**
     * export property <code>include_header</code>.
     */
    public static final String EXPORTPROPERTY_BOOLEAN_EXPORTHEADER = "include_header"; //$NON-NLS-1$

    /**
     * export property <code>filename</code>.
     */
    public static final String EXPORTPROPERTY_STRING_FILENAME = "filename"; //$NON-NLS-1$

    /**
     * Separator char used in property names.
     */
    private static final char SEP = '.';

    /**
     * logger.
     */
    private static Log log = LogFactory.getLog(TableProperties.class);

    /**
     * The userProperties are local, non-default properties; these settings override the defaults from
     * displaytag.properties and TableTag.properties.
     */
    private static Properties userProperties = new Properties();

    /**
     * Configured resource provider. If no ResourceProvider is configured, an no-op one is used. This instance is
     * initialized at first use and shared.
     */
    private static I18nResourceProvider resourceProvider;

    /**
     * Configured locale resolver.
     */
    private static LocaleResolver localeResolver;

    /**
     * TableProperties for each locale are loaded as needed, and cloned for public usage.
     */
    private static Map prototypes = new HashMap();

    /**
     * Loaded properties (defaults from defaultProperties + custom from bundle).
     */
    private Properties properties;

    /**
     * The locale for these properties.
     */
    private Locale locale;

    /**
     * Setter for I18nResourceProvider. A resource provider is usually set using displaytag properties, this accessor is
     * needed for tests.
     * @param provider I18nResourceProvider instance
     */
    protected static void setResourceProvider(I18nResourceProvider provider)
    {
        resourceProvider = provider;
    }

    /**
     * Setter for LocaleResolver. A locale resolver is usually set using displaytag properties, this accessor is needed
     * for tests.
     * @param resolver LocaleResolver instance
     */
    protected static void setLocaleResolver(LocaleResolver resolver)
    {
        localeResolver = resolver;
    }

    /**
     * Loads default properties (TableTag.properties).
     * @return loaded properties
     * @throws TablePropertiesLoadException if default properties file can't be found
     */
    private static Properties loadBuiltInProperties() throws TablePropertiesLoadException
    {
        Properties defaultProperties = new Properties();

        try
        {
            defaultProperties.load(TableProperties.class.getResourceAsStream(DEFAULT_FILENAME));
        }
        catch (IOException e)
        {
            throw new TablePropertiesLoadException(TableProperties.class, DEFAULT_FILENAME, e);
        }

        return defaultProperties;
    }

    /**
     * Loads user properties (displaytag.properties) according to the given locale. User properties are not guarantee to
     * exist, so the method can return <code>null</code> (no exception will be thrown).
     * @param locale requested Locale
     * @return loaded properties
     */
    private static ResourceBundle loadUserProperties(Locale locale)
    {
        ResourceBundle bundle = null;
        try
        {
            bundle = ResourceBundle.getBundle(LOCAL_PROPERTIES, locale);
        }
        catch (MissingResourceException e)
        {
            // if no resource bundle is found, try using the context classloader
            try
            {
                bundle = ResourceBundle.getBundle(LOCAL_PROPERTIES, locale, Thread
                    .currentThread()
                    .getContextClassLoader());
            }
            catch (MissingResourceException mre)
            {
                if (log.isDebugEnabled())
                {
                    log.debug(Messages.getString("TableProperties.propertiesnotfound", //$NON-NLS-1$
                        new Object[]{mre.getMessage()}));
                }
            }
        }

        return bundle;
    }

    /**
     * Returns the configured Locale Resolver. This method is called before the loading of localized properties.
     * @return LocaleResolver instance.
     * @throws TablePropertiesLoadException if the default <code>TableTag.properties</code> file is not found.
     */
    public static LocaleResolver getLocaleResolverInstance() throws TablePropertiesLoadException
    {
        // special handling, table properties is not yet instantiated
        String className = null;

        ResourceBundle defaultUserProperties = loadUserProperties(Locale.getDefault());

        // if available, user properties have higher precedence
        if (defaultUserProperties != null)
        {
            try
            {
                className = defaultUserProperties.getString(PROPERTY_CLASS_LOCALERESOLVER);
            }
            catch (MissingResourceException e)
            {
                // no problem
            }
        }

        // still null? load defaults
        if (className == null)
        {
            Properties defaults = loadBuiltInProperties();
            className = defaults.getProperty(PROPERTY_CLASS_LOCALERESOLVER);
        }

        if (localeResolver == null)
        {
            if (className != null)
            {
                try
                {
                    Class classProperty = ReflectHelper.classForName(className);
                    localeResolver = (LocaleResolver) classProperty.newInstance();

                    log.info(Messages.getString("TableProperties.classinitializedto", //$NON-NLS-1$
                        new Object[]{ClassUtils.getShortClassName(LocaleResolver.class), className}));
                }
                catch (Throwable e)
                {
                    log.warn(Messages.getString("TableProperties.errorloading", //$NON-NLS-1$
                        new Object[]{
                            ClassUtils.getShortClassName(LocaleResolver.class),
                            e.getClass().getName(),
                            e.getMessage()}));
                }
            }
            else
            {
                log.info(Messages.getString("TableProperties.noconfigured", //$NON-NLS-1$
                    new Object[]{ClassUtils.getShortClassName(LocaleResolver.class)}));
            }

            // still null?
            if (localeResolver == null)
            {
                // fallback locale resolver
                localeResolver = new LocaleResolver()
                {

                    public Locale resolveLocale(HttpServletRequest request)
                    {
                        return request.getLocale();
                    }
                };
            }
        }

        return localeResolver;
    }

    /**
     * Initialize a new TableProperties loading the default properties file and the user defined one. There is no
     * caching used here, caching is assumed to occur in the getInstance factory method.
     * @param myLocale the locale we are in
     * @throws TablePropertiesLoadException for errors during loading of properties files
     */
    private TableProperties(Locale myLocale) throws TablePropertiesLoadException
    {
        this.locale = myLocale;
        // default properties will not change unless this class is reloaded
        Properties defaultProperties = loadBuiltInProperties();

        properties = new Properties(defaultProperties);
        addProperties(myLocale);

        // Now copy in the user properties (properties file set by calling setUserProperties()).
        // note setUserProperties() MUST BE CALLED before the first TableProperties instantation
        Enumeration keys = userProperties.keys();
        while (keys.hasMoreElements())
        {
            String key = (String) keys.nextElement();
            if (key != null)
            {
                properties.setProperty(key, (String) userProperties.get(key));
            }
        }
    }

    /**
     * Try to load the properties from the local properties file, displaytag.properties, and merge them into the
     * existing properties.
     * @param userLocale the locale from which the properties are to be loaded
     */
    private void addProperties(Locale userLocale)
    {
        ResourceBundle bundle = loadUserProperties(userLocale);

        if (bundle != null)
        {
            Enumeration keys = bundle.getKeys();
            while (keys.hasMoreElements())
            {
                String key = (String) keys.nextElement();
                properties.setProperty(key, bundle.getString(key));
            }
        }
    }

    /**
     * Clones the properties as well.
     * @return a new clone of oneself
     */
    protected Object clone()
    {
        TableProperties twin;
        try
        {
            twin = (TableProperties) super.clone();
        }
        catch (CloneNotSupportedException e)
        {
            // should never happen
            throw new WrappedRuntimeException(getClass(), e);
        }
        twin.properties = (Properties) this.properties.clone();
        return twin;
    }

    /**
     * Returns a new TableProperties instance for the given locale.
     * @param request HttpServletRequest needed to extract the locale to use. If null the default locale will be used.
     * @return TableProperties instance
     */
    public static TableProperties getInstance(HttpServletRequest request)
    {
        Locale locale;
        if (request != null)
        {
            locale = getLocaleResolverInstance().resolveLocale(request);
        }
        else
        {
            // for some configuration parameters locale doesn't matter
            locale = Locale.getDefault();
        }

        TableProperties props = (TableProperties) prototypes.get(locale);
        if (props == null)
        {
            TableProperties lprops = new TableProperties(locale);
            prototypes.put(locale, lprops);
            props = lprops;
        }
        return (TableProperties) props.clone();
    }

    /**
     * Unload all cached properties. This will not clear properties set by by setUserProperties; you must clear those
     * manually.
     */
    public static void clearProperties()
    {
        prototypes.clear();
    }

    /**
     * Local, non-default properties; these settings override the defaults from displaytag.properties and
     * TableTag.properties. Please note that the values are copied in, so that multiple calls with non-overlapping
     * properties will be merged, not overwritten. Note: setUserProperties() MUST BE CALLED before the first
     * TableProperties instantation.
     * @param overrideProperties - The local, non-default properties
     */
    public static void setUserProperties(Properties overrideProperties)
    {
        // copy keys here, so that this can be invoked more than once from different sources.
        // if default properties are not yet loaded they will be copied in constructor
        Enumeration keys = overrideProperties.keys();
        while (keys.hasMoreElements())
        {
            String key = (String) keys.nextElement();
            if (key != null)
            {
                userProperties.setProperty(key, (String) overrideProperties.get(key));
            }
        }
    }

    /**
     * The locale for which these properties are intended.
     * @return the locale
     */
    public Locale getLocale()
    {
        return locale;
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_INVALIDPAGE</code> property.
     * @return String
     */
    public String getPagingInvalidPage()
    {
        return getProperty(PROPERTY_STRING_PAGING_INVALIDPAGE);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_ITEM_NAME</code> property.
     * @return String
     */
    public String getPagingItemName()
    {
        return getProperty(PROPERTY_STRING_PAGING_ITEM_NAME);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_ITEMS_NAME</code> property.
     * @return String
     */
    public String getPagingItemsName()
    {
        return getProperty(PROPERTY_STRING_PAGING_ITEMS_NAME);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_NOITEMS</code> property.
     * @return String
     */
    public String getPagingFoundNoItems()
    {
        return getProperty(PROPERTY_STRING_PAGING_NOITEMS);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_ONEITEM</code> property.
     * @return String
     */
    public String getPagingFoundOneItem()
    {
        return getProperty(PROPERTY_STRING_PAGING_FOUND_ONEITEM);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_ALLITEMS</code> property.
     * @return String
     */
    public String getPagingFoundAllItems()
    {
        return getProperty(PROPERTY_STRING_PAGING_FOUND_ALLITEMS);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_FOUND_SOMEITEMS</code> property.
     * @return String
     */
    public String getPagingFoundSomeItems()
    {
        return getProperty(PROPERTY_STRING_PAGING_FOUND_SOMEITEMS);
    }

    /**
     * Getter for the <code>PROPERTY_INT_PAGING_GROUPSIZE</code> property.
     * @return int
     */
    public int getPagingGroupSize()
    {
        // default size is 8
        return getIntProperty(PROPERTY_INT_PAGING_GROUPSIZE, 8);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_ONEPAGE</code> property.
     * @return String
     */
    public String getPagingBannerOnePage()
    {
        return getProperty(PROPERTY_STRING_PAGING_BANNER_ONEPAGE);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_FIRST</code> property.
     * @return String
     */
    public String getPagingBannerFirst()
    {
        return getProperty(PROPERTY_STRING_PAGING_BANNER_FIRST);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_LAST</code> property.
     * @return String
     */
    public String getPagingBannerLast()
    {
        return getProperty(PROPERTY_STRING_PAGING_BANNER_LAST);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_BANNER_FULL</code> property.
     * @return String
     */
    public String getPagingBannerFull()
    {
        return getProperty(PROPERTY_STRING_PAGING_BANNER_FULL);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_LINK</code> property.
     * @return String
     */
    public String getPagingPageLink()
    {
        return getProperty(PROPERTY_STRING_PAGING_PAGE_LINK);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_SELECTED</code> property.
     * @return String
     */
    public String getPagingPageSelected()
    {
        return getProperty(PROPERTY_STRING_PAGING_PAGE_SELECTED);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_PAGING_PAGE_SPARATOR</code> property.
     * @return String
     */
    public String getPagingPageSeparator()
    {
        return getProperty(PROPERTY_STRING_PAGING_PAGE_SPARATOR);
    }

    /**
     * Is the given export option enabled?
     * @param exportType instance of MediaTypeEnum
     * @return boolean true if export is enabled
     */
    public boolean getAddExport(MediaTypeEnum exportType)
    {
        return getBooleanProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName());
    }

    /**
     * Should headers be included in given export type?
     * @param exportType instance of MediaTypeEnum
     * @return boolean true if export should include headers
     */
    public boolean getExportHeader(MediaTypeEnum exportType)
    {
        return getBooleanProperty(PROPERTY_EXPORT_PREFIX
            + SEP
            + exportType.getName()
            + SEP
            + EXPORTPROPERTY_BOOLEAN_EXPORTHEADER);
    }

    /**
     * Returns the label for the given export option.
     * @param exportType instance of MediaTypeEnum
     * @return String label
     */
    public String getExportLabel(MediaTypeEnum exportType)
    {
        return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName() + SEP + EXPORTPROPERTY_STRING_LABEL);
    }

    /**
     * Returns the file name for the given media. Can be null
     * @param exportType instance of MediaTypeEnum
     * @return String filename
     */
    public String getExportFileName(MediaTypeEnum exportType)
    {
        return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportType.getName() + SEP + EXPORTPROPERTY_STRING_FILENAME);
    }

    /**
     * Getter for the <code>PROPERTY_BOOLEAN_EXPORTDECORATED</code> property.
     * @return boolean <code>true</code> if decorators should be used in exporting
     */
    public boolean getExportDecorated()
    {
        return getBooleanProperty(PROPERTY_BOOLEAN_EXPORTDECORATED);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_EXPORTBANNER</code> property.
     * @return String
     */
    public String getExportBanner()
    {
        return getProperty(PROPERTY_STRING_EXPORTBANNER);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_EXPORTBANNER_SEPARATOR</code> property.
     * @return String
     */
    public String getExportBannerSeparator()
    {
        return getProperty(PROPERTY_STRING_EXPORTBANNER_SEPARATOR);
    }

    /**
     * Getter for the <code>PROPERTY_BOOLEAN_SHOWHEADER</code> property.
     * @return boolean
     */
    public boolean getShowHeader()
    {
        return getBooleanProperty(PROPERTY_BOOLEAN_SHOWHEADER);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_EMPTYLIST_MESSAGE</code> property.
     * @return String
     */
    public String getEmptyListMessage()
    {
        return getProperty(PROPERTY_STRING_EMPTYLIST_MESSAGE);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_EMPTYLISTROW_MESSAGE</code> property.
     * @return String
     */
    public String getEmptyListRowMessage()
    {
        return getProperty(PROPERTY_STRING_EMPTYLISTROW_MESSAGE);
    }

    /**
     * Getter for the <code>PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE</code> property.
     * @return boolean <code>true</code> if table should be displayed also if no items are found
     */
    public boolean getEmptyListShowTable()
    {
        return getBooleanProperty(PROPERTY_BOOLEAN_EMPTYLIST_SHOWTABLE);
    }

    /**
     * Getter for the <code>PROPERTY_STRING_EXPORTAMOUNT</code> property.
     * @return boolean <code>true</code> if <code>export.amount</code> is <code>list</code>
     */
    public boolean getExportFullList()
    {
        return "list".equals(getProperty(PROPERTY_STRING_EXPORTAMOUNT)); //$NON-NLS-1$
    }

    /**
     * Getter for the <code>PROPERTY_STRING_SORTAMOUNT</code> property.
     * @return boolean <code>true</code> if <code>sort.amount</code> is <code>list</code>
     */
    public boolean getSortFullList()
    {
        return "list".equals(getProperty(PROPERTY_STRING_SORTAMOUNT)); //$NON-NLS-1$
    }

    /**
     * Should paging banner be added before the table?
     * @return boolean
     */
    public boolean getAddPagingBannerTop()
    {
        String placement = getProperty(PROPERTY_STRING_BANNER_PLACEMENT);
        return "top".equals(placement) || "both".equals(placement); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Should paging banner be added after the table?
     * @return boolean
     */
    public boolean getAddPagingBannerBottom()
    {
        String placement = getProperty(PROPERTY_STRING_BANNER_PLACEMENT);
        return "bottom".equals(placement) || "both".equals(placement); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Returns the appropriate css class for a table row.
     * @param rowNumber row number
     * @return the value of <code>PROPERTY_CSS_TR_EVEN</code> if rowNumber is even or <code>PROPERTY_CSS_TR_ODD</code>
     * if rowNumber is odd.
     */
    public String getCssRow(int rowNumber)
    {
        return getProperty((rowNumber % 2 == 0) ? PROPERTY_CSS_TR_ODD : PROPERTY_CSS_TR_EVEN);
    }

    /**
     * Returns the appropriate css class for a sorted column header.
     * @param ascending <code>true</code> if column is sorded in ascending order.
     * @return the value of <code>PROPERTY_CSS_TH_SORTED_ASCENDING</code> if column is sorded in ascending order or
     * <code>PROPERTY_CSS_TH_SORTED_DESCENDING</code> if column is sorded in descending order.
     */
    public String getCssOrder(boolean ascending)
    {
        return getProperty(ascending ? PROPERTY_CSS_TH_SORTED_ASCENDING : PROPERTY_CSS_TH_SORTED_DESCENDING);
    }

    /**
     * Returns the configured css class for a sorted column header.
     * @return the value of <code>PROPERTY_CSS_TH_SORTED</code>
     */
    public String getCssSorted()
    {
        return getProperty(PROPERTY_CSS_TH_SORTED);
    }

    /**
     * Returns the configured css class for the main table tag.
     * @return the value of <code>PROPERTY_CSS_TABLE</code>
     */
    public String getCssTable()
    {
        return getProperty(PROPERTY_CSS_TABLE);
    }

    /**
     * Returns the configured css class for a sortable column header.
     * @return the value of <code>PROPERTY_CSS_TH_SORTABLE</code>
     */
    public String getCssSortable()
    {
        return getProperty(PROPERTY_CSS_TH_SORTABLE);
    }

    /**
     * Returns the configured list of media.
     * @return the value of <code>PROPERTY_EXPORTTYPES</code>
     */
    public String[] getExportTypes()
    {
        String list = getProperty(PROPERTY_EXPORTTYPES);
        if (list == null)
        {
            return new String[0];
        }

        return StringUtils.split(list);
    }

    /**
     * Returns the class responsible for the given export.
     * @param exportName export name
     * @return String classname
     */
    public String getExportClass(String exportName)
    {
        return getProperty(PROPERTY_EXPORT_PREFIX + SEP + exportName + SEP + EXPORTPROPERTY_STRING_CLASS);
    }

    /**
     * Returns an instance of configured requestHelperFactory.
     * @return RequestHelperFactory instance.
     * @throws FactoryInstantiationException if unable to load or instantiate the configurated class.
     */
    public RequestHelperFactory getRequestHelperFactoryInstance() throws FactoryInstantiationException
    {
        Object loadedObject = getClassPropertyInstance(PROPERTY_CLASS_REQUESTHELPERFACTORY);

        // should not be null, but avoid errors just in case... see DISPL-148
        if (loadedObject == null)
        {
            return new DefaultRequestHelperFactory();
        }

        try
        {
            return (RequestHelperFactory) loadedObject;
        }
        catch (ClassCastException e)
        {
            throw new FactoryInstantiationException(getClass(), PROPERTY_CLASS_REQUESTHELPERFACTORY, loadedObject
                .getClass()
                .getName(), e);
        }
    }

    /**
     * Returns the configured resource provider instance. If necessary instantiate the resource provider from config and
     * then keep a cached instance.
     * @return I18nResourceProvider instance.
     * @see I18nResourceProvider
     */
    public I18nResourceProvider geResourceProvider()
    {
        String className = getProperty(PROPERTY_CLASS_LOCALEPROVIDER);

        if (resourceProvider == null)
        {
            if (className != null)
            {
                try
                {
                    Class classProperty = ReflectHelper.classForName(className);
                    resourceProvider = (I18nResourceProvider) classProperty.newInstance();

                    log.info(Messages.getString("TableProperties.classinitializedto", //$NON-NLS-1$
                        new Object[]{ClassUtils.getShortClassName(I18nResourceProvider.class), className}));
                }
                catch (Throwable e)
                {
                    log.warn(Messages.getString("TableProperties.errorloading", //$NON-NLS-1$
                        new Object[]{
                            ClassUtils.getShortClassName(I18nResourceProvider.class),
                            e.getClass().getName(),
                            e.getMessage()}));
                }
            }
            else
            {
                log.info(Messages.getString("TableProperties.noconfigured", //$NON-NLS-1$
                    new Object[]{ClassUtils.getShortClassName(I18nResourceProvider.class)}));
            }

            // still null?
            if (resourceProvider == null)
            {
                // fallback provider, no i18n
                resourceProvider = new I18nResourceProvider()
                {

                    // Always returns null
                    public String getResource(String titleKey, String property, Tag tag, PageContext context)
                    {
                        return null;
                    }
                };
            }
        }

        return resourceProvider;
    }

    /**
     * Reads a String property.
     * @param key property name
     * @return property value or <code>null</code> if property is not found
     */
    private String getProperty(String key)
    {
        return this.properties.getProperty(key);
    }

    /**
     * Sets a property.
     * @param key property name
     * @param value property value
     */
    public void setProperty(String key, String value)
    {
        this.properties.setProperty(key, value);
    }

    /**
     * Reads a boolean property.
     * @param key property name
     * @return boolean <code>true</code> if the property value is "true", <code>false</code> for any other value.
     */
    private boolean getBooleanProperty(String key)
    {
        return Boolean.TRUE.toString().equals(getProperty(key));
    }

    /**
     * Returns an instance of a configured Class. Returns a configured Class instantiated
     * callingClass.forName([configuration value]).
     * @param key configuration key
     * @return instance of configured class
     * @throws FactoryInstantiationException if unable to load or instantiate the configurated class.
     */
    private Object getClassPropertyInstance(String key) throws FactoryInstantiationException
    {
        String className = getProperty(key);

        // shouldn't be null, but better check it
        if (className == null)
        {
            return null;
        }

        try
        {
            Class classProperty = ReflectHelper.classForName(className);
            return classProperty.newInstance();
        }
        catch (Exception e)
        {
            throw new FactoryInstantiationException(getClass(), key, className, e);
        }
    }

    /**
     * Reads an int property.
     * @param key property name
     * @param defaultValue default value returned if property is not found or not a valid int value
     * @return property value
     */
    private int getIntProperty(String key, int defaultValue)
    {
        try
        {
            return Integer.parseInt(getProperty(key));
        }
        catch (NumberFormatException e)
        {
            // Don't care, use default
            log.warn(Messages.getString("TableProperties.invalidvalue", //$NON-NLS-1$
                new Object[]{key, getProperty(key), new Integer(defaultValue)}));
        }

        return defaultValue;
    }
}