/*
 * File   : $Source: /usr/local/cvs/opencms/test/org/opencms/i18n/TestCmsMessageBundles.java,v $
 * Date   : $Date: 2008-06-12 10:47:39 $
 * Version: $Revision: 1.18 $
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) 2002 - 2008 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.i18n;

import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsStringUtil;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import junit.framework.TestCase;

/**
 * Tests for the CmsMessageBundles.<p>
 * 
 * @author Alexander Kandzior 
 * 
 * @version $Revision: 1.18 $
 * 
 * @since 6.0.0
 */
public abstract class TestCmsMessageBundles extends TestCase {

    /** Prefix for the error messages in the bundles. */
    private static final String KEY_PREFIX_ERR = "ERR_";

    /** Prefix for the gui messages in the bundles. */
    private static final String KEY_PREFIX_GUI = "GUI_";

    /** Prefix for the initialization messages in the bundles. */
    private static final String KEY_PREFIX_INIT = "INIT_";

    /** Prefix for the log messages in the bundles. */
    private static final String KEY_PREFIX_LOG = "LOG_";

    /** Prefix for the report messages in the bundles. */
    private static final String KEY_PREFIX_RPT = "RPT_";

    /** The source folder to copy the resource bundles from. */
    private static final String SOURCE_FOLDER_INFIX = "/resources/system/workplace/locales/";

    /** The source folder to copy the resource bundles from. */
    private static final String SOURCE_FOLDER_PREFIX = "modules/org.opencms.locale.";

    /** The source folder to copy the resource bundles from. */
    private static final String SOURCE_FOLDER_SUFFIX = "/messages/";

    /** The taget folder to copy the resource bundles to. */
    private static final String TARGET_FOLDER = "bin/";

    /** Cache the resource bundle to exclude from additional locales tests. */
    private Map m_excludedBundles = new HashMap();

    /**
     * Checks all message bundles for the DE locale.<p>
     * 
     * @throws Exception if the test fails
     */
    public void testLocale_DE_MessagesBundles() throws Exception {

        messagesBundleConstantTest(Locale.GERMAN);
    }

    /**
     * Checks all message bundles for the EN locale.<p>
     * 
     * @throws Exception if the test fails
     */
    public void testLocale_EN_MessagesBundles() throws Exception {

        messagesBundleConstantTest(Locale.ENGLISH);
    }

    /**
     * Checks all message bundles for the ES locale.<p>
     * 
     * @throws Exception if the test fails
     */
    public void testLocale_ES_MessagesBundles() throws Exception {

        messagesBundleConstantTest(new Locale("es"));
    }

    /**
     * Performs some key and language independent tests.<p>
     * 
     * @param bundle the bundle to test
     * @param locale the locale to test
     * 
     * @return a description of all errors found
     */
    protected String doPreTestBundle(I_CmsMessageBundle bundle, Locale locale) {

        String className = bundle.getClass().getName();
        if (!className.endsWith(".Messages")) {
            return "Bundle '" + className + "' is not a 'Messages' class.\n";
        }
        if (!className.toLowerCase().equals(bundle.getBundleName())) {
            return "Bundle '" + bundle.getBundleName() + "' has not the form: packagename.messages.\n";
        }
        if (!bundle.getBundleName().endsWith(".messages")) {
            return "The Message bundle '"
                + bundle.getBundleName()
                + "' does not ends with: '.messages'. \n "
                + "Change the constant literal ('private static final String BUNDLE_NAME')\n";
        }
        String fileName = getMessageBundleSourceName(bundle, locale);
        if (!Locale.ENGLISH.equals(locale)) {
            if (!(new File(fileName)).canRead()) {
                return "Bundle '" + className + "' has no input for locale '" + locale + "'.\n";
            }
        }
        // in case of no errors, return the emtpy String 
        return "";
    }

    /**
     * Tests an individual message bundle.<p>
     * 
     * @param bundle the bundle to test
     * @param locale the locale to test
     * 
     * @return a description of all errors found
     * 
     * @throws Exception if the test fails
     */
    protected String doTestBundle(I_CmsMessageBundle bundle, Locale locale) throws Exception {

        List keys = new ArrayList();

        System.out.println("\nValidating all keys in bundle " + bundle.getBundleName() + " for locale " + locale + ":");

        CmsMessages messages = getMessageBundle(bundle, locale);

        List errorMessages = new ArrayList();
        // use reflection on all member constants
        Field[] fields = bundle.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            if (field.getType().equals(String.class)) {
                // check all String fields
                String key = field.getName();

                String value;
                try {
                    value = (String)field.get(bundle);
                } catch (IllegalAccessException e) {
                    continue;
                }

                System.out.println("Validating key '" + key + "': '" + messages.key(key) + "'");

                // ensure the name id identical to the value
                if (!key.equals(value)) {
                    errorMessages.add("Key '" + key + "' has bad value '" + value + "'.");
                }
                // check if key exists in bundle for constant
                String message = messages.key(key);
                boolean isPresent = !CmsMessages.isUnknownKey(message);
                boolean testKeyValue = false;
                if (Locale.ENGLISH.equals(locale)) {
                    testKeyValue = true;
                } else {
                    boolean isAdditionalKey = !key.toUpperCase().equals(key);
                    testKeyValue = (isAdditionalKey || key.startsWith(KEY_PREFIX_ERR) || key.startsWith(KEY_PREFIX_GUI) || key.startsWith(KEY_PREFIX_RPT));
                }
                if (testKeyValue && !isPresent) {
                    errorMessages.add("No message for '" + key + "' in bundle.");
                }

                // ensure key has the form
                // "{ERR|LOG|INIT|GUI|RPT}_KEYNAME_{0-9}";
                if (key.length() < 7) {
                    errorMessages.add("Key '" + key + "' is too short (length must be at least 7).");
                }
                if (!key.equals(key.toUpperCase())) {
                    errorMessages.add("Key '" + key + "' must be all upper case.");
                }
                if ((key.charAt(key.length() - 2) != '_')
                    || (!key.startsWith(KEY_PREFIX_ERR)
                        && !key.startsWith(KEY_PREFIX_GUI)
                        && !key.startsWith(KEY_PREFIX_INIT)
                        && !key.startsWith(KEY_PREFIX_LOG) && !key.startsWith(KEY_PREFIX_RPT))) {
                    errorMessages.add("Key '" + key + "' must have the form {ERR|LOG|INIT|GUI|RPT}_KEYNAME_{0-9}.");
                }
                int argCount = Integer.valueOf(key.substring(key.length() - 1)).intValue();

                if (testKeyValue && isPresent) {
                    for (int j = 0; j < argCount; j++) {
                        String arg = "{" + j;
                        int pos = message.indexOf(arg);
                        if (pos < 0) {
                            errorMessages.add("Message '"
                                + message
                                + "' for key '"
                                + key
                                + "' misses argument {"
                                + j
                                + "}.");
                        }
                    }
                    for (int j = argCount; j < 10; j++) {
                        String arg = "{" + j;
                        int pos = message.indexOf(arg);
                        if (pos >= 0) {
                            errorMessages.add("Message '"
                                + message
                                + "' for key '"
                                + key
                                + "' contains unused argument {"
                                + j
                                + "}.");
                        }
                    }
                }
                // store this key for later check against all properties in the bundle
                keys.add(key);
            }
        }

        Enumeration bundleKeys = messages.getResourceBundle().getKeys();
        while (bundleKeys.hasMoreElements()) {
            String bundleKey = (String)bundleKeys.nextElement();
            if (bundleKey.toUpperCase().equals(bundleKey)) {
                // only check keys which are all upper case
                if (!keys.contains(bundleKey)) {
                    errorMessages.add("Bundle contains unreferenced message '" + bundleKey + "'.");
                }
            } else {
                System.out.println("Additional key '" + bundleKey + "' in bundle.");
            }
        }

        // for locales other than ENGLISH
        if (!Locale.ENGLISH.equals(locale)) {
            // compare the additional key names and params with the ENGLISH ones
            ResourceBundle resBundle = messages.getResourceBundle();
            ResourceBundle enResBundle = bundle.getBundle().getResourceBundle();
            bundleKeys = enResBundle.getKeys();
            while (bundleKeys.hasMoreElements()) {
                String bundleKey = (String)bundleKeys.nextElement();
                if (!bundleKey.toUpperCase().equals(bundleKey)) {
                    // additional key found
                    String keyValue;
                    try {
                        // try to retrieve the key for the given locale
                        keyValue = resBundle.getString(bundleKey);
                    } catch (MissingResourceException e) {
                        errorMessages.add("Additional key '" + bundleKey + "' missing.");
                        continue;
                    }
                    String enKeyValue = enResBundle.getString(bundleKey);
                    boolean[] args = new boolean[10];
                    for (int j = 0; j < args.length; j++) {
                        String arg = "{" + j;
                        int pos = enKeyValue.indexOf(arg);
                        args[j] = pos >= 0;
                    }
                    for (int j = 0; j < args.length; j++) {
                        String arg = "{" + j;
                        int pos = keyValue.indexOf(arg);
                        if ((pos < 0) && (args[j])) {
                            // not in locale bundle but in master bundle
                            errorMessages.add("Additional message '"
                                + keyValue
                                + "' for key '"
                                + bundleKey
                                + "' misses argument {"
                                + j
                                + "} from master bundle.");
                        } else if ((pos >= 0) && (!args[j])) {
                            // in locale bundle but not in master bundle
                            errorMessages.add("Additional message '"
                                + keyValue
                                + "' for key '"
                                + bundleKey
                                + "' contains argument {"
                                + j
                                + "} not used in master bundle.");
                        }
                    }
                }
            }
        }

        if (!errorMessages.isEmpty()) {
            String msg = "Errors for bundle '" + bundle.getBundleName() + "' and Locale '" + locale + "':";
            Iterator it = errorMessages.iterator();
            while (it.hasNext()) {
                msg += "\n     - ";
                msg += it.next();
            }
            return msg + "\n";
        }
        // in case there was no error, return an emtpy String
        return "";
    }

    /**
     * Returns the resource bundles to be excluded from additional locales tests.<p>
     * 
     * @param locale the locale to get the excluded bundles for
     * 
     * @return the resource bundles to be excluded from additional locales tests
     */
    protected List getExcludedLocalizedBundles(Locale locale) {

        if (Locale.ENGLISH.equals(locale)) {
            return new ArrayList();
        }
        if (m_excludedBundles.get(locale) == null) {
            List excludedBundles;
            List notLocalized = getNotLocalizedBundles(locale);
            if (notLocalized == null) {
                excludedBundles = new ArrayList();
            } else {
                excludedBundles = new ArrayList(notLocalized);
            }
            for (int i = 0; i < getTestMessageBundles().length; i++) {
                I_CmsMessageBundle bundle = getTestMessageBundles()[i];
                if (excludedBundles.contains(bundle)) {
                    continue;
                }
                boolean exclude = true;
                // test if the bundle contains keys prefixed by GUI_ or RPT_
                Field[] fields = bundle.getClass().getDeclaredFields();
                for (int j = 0; j < fields.length; j++) {
                    Field field = fields[j];
                    if (field.getType().equals(String.class)) {
                        // check all String fields
                        String key = field.getName();
                        if (key.startsWith(KEY_PREFIX_ERR)
                            || key.startsWith(KEY_PREFIX_GUI)
                            || key.startsWith(KEY_PREFIX_RPT)) {
                            exclude = false;
                            break;
                        }
                    }
                }
                if (!exclude) {
                    continue;
                }
                // test if the bundle contains additional keys
                CmsMessages messages = new CmsMessages(bundle.getBundleName(), Locale.ENGLISH);
                Enumeration bundleKeys = messages.getResourceBundle().getKeys();
                while (bundleKeys.hasMoreElements()) {
                    String bundleKey = (String)bundleKeys.nextElement();
                    if (!bundleKey.toUpperCase().equals(bundleKey)) {
                        exclude = false;
                        break;
                    }
                }
                if (exclude) {
                    excludedBundles.add(bundle);
                }
            }
            m_excludedBundles.put(locale, excludedBundles);
        }
        return (List)m_excludedBundles.get(locale);
    }

    /**
     * Prepares the test for the given bundle and locale and
     * returns a message bundle that DOES NOT include the default keys.<p>
     * 
     * @param bundle the resource bundle to prepare
     * @param locale the locale to prepare the resource bundle for
     * 
     * @return a message bundle that DOES NOT include the default keys
     * 
     * @throws IOException if something goes wrong
     */
    protected CmsMessages getMessageBundle(I_CmsMessageBundle bundle, Locale locale) throws IOException {

        if (Locale.ENGLISH.equals(locale)) {
            return bundle.getBundle(locale);
        }
        String source = getMessageBundleSourceName(bundle, locale);
        String fileName = CmsStringUtil.substitute(bundle.getBundleName(), ".", "/")
            + "_"
            + locale.toString()
            + ".properties";
        String target = TARGET_FOLDER + fileName;
        CmsFileUtil.copy(source, target);
        return new CmsMessages(bundle.getBundleName() + "_" + locale.toString(), locale);
    }

    /**
     * Returns the file name of the source message bundle.<p>
     * 
     * @param bundle the resource bundle to get the file name for
     * @param locale the locale to get the file name for
     * 
     * @return the file name of the source message bundle
     */
    protected String getMessageBundleSourceName(I_CmsMessageBundle bundle, Locale locale) {

        if (Locale.ENGLISH.equals(locale)) {
            return bundle.getBundleName();
        }
        String fileName = CmsStringUtil.substitute(bundle.getBundleName(), ".", "/")
            + "_"
            + locale.toString()
            + ".properties";
        String source = SOURCE_FOLDER_PREFIX
            + locale.toString()
            + SOURCE_FOLDER_INFIX
            + locale.toString()
            + SOURCE_FOLDER_SUFFIX
            + fileName;

        // if file from the localized folder is not readable take the file from the original module
        if (!new File(source).canRead()) {
            source = getModuleMessagesBundleSourceName(bundle, locale);
        }
        return source;
    }

    /**
     * Returns the file name of the source message bundle from the module.<p>
     * 
     * @param bundle the resource bundle to get the file name for
     * @param locale the locale to get the file name for
     * 
     * @return the file name of the source message bundle of the module
     */
    protected String getModuleMessagesBundleSourceName(I_CmsMessageBundle bundle, Locale locale) {

        if (Locale.ENGLISH.equals(locale)) {
            return bundle.getBundleName();
        }
        // substitute the last occuring "." of the bundle name with "/" to build the correct filename
        String packageName = bundle.getBundleName().substring(0, bundle.getBundleName().lastIndexOf('.'));
        packageName += "/";
        String source = "modules/"
            + packageName
            + "resources/system/modules/"
            + packageName
            + "classes/"
            + CmsStringUtil.substitute(bundle.getBundleName(), ".", "/")
            + "_"
            + locale.toString()
            + ".properties";
        return source;
    }

    /**
     * Returns a list of bundles not to be localized.<p>
     * 
     * @param locale the locale to get the not localized bundles for 
     * 
     * @return a list of bundles not to be localized
     */
    protected abstract List getNotLocalizedBundles(Locale locale);

    /**
     * Template method that has to be overwritten to return the <code>I_CmsMessageBundle</code> 
     * instances that will be tested.<p> 
     * 
     * @return the <code>I_CmsMessageBundle</code> instances to test: these will be the 
     *         singleton instances of the <code>Messages</code> classes residing in every localized package. 
     */
    protected abstract I_CmsMessageBundle[] getTestMessageBundles();

    /**
     * Checks all OpenCms internal message bundles if the are correctly build.<p>
     * 
     * @param locale the locale to test
     * 
     * @throws Exception if the test fails
     */
    protected void messagesBundleConstantTest(Locale locale) throws Exception {

        // the default locale MUST be ENGLISH (this call will also set the default locale to ENGLISH if required)
        assertEquals(CmsLocaleManager.getDefaultLocale(), Locale.ENGLISH);

        StringBuffer errors = new StringBuffer();
        I_CmsMessageBundle[] bundles = getTestMessageBundles();
        for (int i = 0; i < bundles.length; i++) {
            I_CmsMessageBundle bundle = bundles[i];
            String tmp = bundle.getBundleName();
            tmp = doPreTestBundle(bundle, locale);
            errors.append(tmp);
            if (CmsStringUtil.isEmpty(tmp)) {
                if (!getExcludedLocalizedBundles(locale).contains(bundle)) {
                    errors.append(doTestBundle(bundle, locale));
                }
            }
        }
        if (errors.length() > 0) {
            fail("\n" + errors);
        }
    }
}