org.eclipse.e4.tools.services.impl.ResourceBundleHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.e4.tools.services.impl.ResourceBundleHelper.java

Source

/*******************************************************************************
 * Copyright (c) 2012 Dirk Fauth and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Dirk Fauth <dirk.fauth@gmail.com> - initial API and implementation
 ******************************************************************************/
package org.eclipse.e4.tools.services.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

import org.eclipse.e4.tools.services.ToolsServicesActivator;
import org.eclipse.osgi.service.localization.BundleLocalization;
import org.osgi.framework.Bundle;
import org.osgi.service.log.LogService;
import org.osgi.service.packageadmin.PackageAdmin;

/**
 * Helper class for retrieving {@link ResourceBundle}s out of OSGi {@link Bundle}s.
 * 
 * @author Dirk Fauth
 */
// There is no replacement for PackageAdmin#getBundles()
@SuppressWarnings("deprecation")
public class ResourceBundleHelper {

    /**
     * The schema identifier used for Eclipse platform references
     */
    private static final String PLATFORM_SCHEMA = "platform"; //$NON-NLS-1$
    /**
     * The schema identifier used for Eclipse bundle class references
     */
    private static final String BUNDLECLASS_SCHEMA = "bundleclass"; //$NON-NLS-1$
    /**
     * Identifier part of the Eclipse platform schema to point to a plugin
     */
    private static final String PLUGIN_SEGMENT = "/plugin/"; //$NON-NLS-1$
    /**
     * Identifier part of the Eclipse platform schema to point to a fragment
     */
    private static final String FRAGMENT_SEGMENT = "/fragment/"; //$NON-NLS-1$
    /**
     * The separator character for paths in the platform schema
     */
    private static final String PATH_SEPARATOR = "/"; //$NON-NLS-1$

    /**
     * Parses the specified contributor URI and loads the {@link ResourceBundle} for the specified {@link Locale}
     * out of an OSGi {@link Bundle}.
     * <p>Following URIs are supported:
     * <ul>
     * <li>platform:/[plugin|fragment]/[Bundle-SymbolicName]<br>
     * Load the OSGi resource bundle out of the bundle/fragment named [Bundle-SymbolicName]</li>
     * <li>platform:/[plugin|fragment]/[Bundle-SymbolicName]/[Path]/[Basename]<br>
     * Load the resource bundle specified by [Path] and [Basename] out of the bundle/fragment named [Bundle-SymbolicName].</li>
     * <li>bundleclass://[plugin|fragment]/[Full-Qualified-Classname]<br>
     * Instantiate the class specified by [Full-Qualified-Classname] out of the bundle/fragment named [Bundle-SymbolicName].
     * Note that the class needs to be a subtype of {@link ResourceBundle}.</li>
     * </ul>
     * </p>
     * @param contributorURI The URI that points to a {@link ResourceBundle}
     * @param locale The {@link Locale} to use for loading the {@link ResourceBundle}
     * @param localization The service for retrieving a {@link ResourceBundle} for a given {@link Locale} out of
     *          the given {@link Bundle} which is specified by URI.
     * @return
     */
    public static ResourceBundle getResourceBundleForUri(String contributorURI, Locale locale,
            BundleLocalization localization) {
        if (contributorURI == null)
            return null;

        LogService logService = ToolsServicesActivator.getDefault().getLogService();

        URI uri;
        try {
            uri = new URI(contributorURI);
        } catch (URISyntaxException e) {
            if (logService != null)
                logService.log(LogService.LOG_ERROR, "Invalid contributor URI: " + contributorURI); //$NON-NLS-1$
            return null;
        }

        String bundleName = null;
        Bundle bundle = null;
        String resourcePath = null;
        String classPath = null;

        //the uri follows the platform schema, so we search for .properties files in the bundle
        if (PLATFORM_SCHEMA.equals(uri.getScheme())) {
            bundleName = uri.getPath();
            if (bundleName.startsWith(PLUGIN_SEGMENT))
                bundleName = bundleName.substring(PLUGIN_SEGMENT.length());
            else if (bundleName.startsWith(FRAGMENT_SEGMENT))
                bundleName = bundleName.substring(FRAGMENT_SEGMENT.length());

            resourcePath = ""; //$NON-NLS-1$
            if (bundleName.contains(PATH_SEPARATOR)) {
                resourcePath = bundleName.substring(bundleName.indexOf(PATH_SEPARATOR) + 1);
                bundleName = bundleName.substring(0, bundleName.indexOf(PATH_SEPARATOR));
            }
        } else if (BUNDLECLASS_SCHEMA.equals(uri.getScheme())) {
            if (uri.getAuthority() == null) {
                if (logService != null)
                    logService.log(LogService.LOG_ERROR, "Failed to get bundle for: " + contributorURI); //$NON-NLS-1$
            }
            bundleName = uri.getAuthority();
            //remove the leading /
            classPath = uri.getPath().substring(1);
        }

        ResourceBundle result = null;

        if (bundleName != null) {
            bundle = getBundleForName(bundleName);

            if (bundle != null) {
                if (resourcePath == null && classPath != null) {
                    //the URI points to a class within the bundle classpath
                    //therefore we are trying to instantiate the class
                    try {
                        Class<?> resourceBundleClass = bundle.loadClass(classPath);
                        result = getEquinoxResourceBundle(classPath, locale, resourceBundleClass.getClassLoader());
                    } catch (Exception e) {
                        if (logService != null)
                            logService.log(LogService.LOG_ERROR,
                                    "Failed to load specified ResourceBundle: " + contributorURI, e); //$NON-NLS-1$
                    }
                } else if (resourcePath.length() > 0) {
                    //the specified URI points to a resource 
                    //therefore we try to load the .properties files into a ResourceBundle
                    result = getEquinoxResourceBundle(resourcePath.replace('.', '/'), locale, bundle);
                } else {
                    //there is no class and no special resource specified within the URI 
                    //therefore we load the OSGi resource bundle out of the specified Bundle
                    //for the current Locale
                    result = localization.getLocalization(bundle, locale.toString());
                }
            }
        }

        return result;
    }

    /**
     * This method searches for the {@link ResourceBundle} in a modified way by inspecting the configuration option 
     * <code>equinox.root.locale</code>. 
     * <p>
     * If the value for this system property is set to an empty String the default search order for ResourceBundles is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * Where bn is this bundle's localization basename, Ls, Cs and Vs are the specified locale (language, country, variant) and 
     * Ld, Cd and Vd are the default locale (language, country, variant).
     * </p>
     * <p>
     * If Ls equals the value of <code>equinox.root.locale</code> then the following search order is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * </p>
     * If <code>equinox.root.locale=en</code> and en_XX or en is asked for then this allows the root file to be used instead of 
     * falling back to the default locale.
     * 
      * @param baseName the base name of the resource bundle, a fully qualified class name
      * @param locale the locale for which a resource bundle is desired
      * @param loader the class loader from which to load the resource bundle
      * @return a resource bundle for the given base name and locale
     * 
     * @see ResourceBundle#getBundle(String, Locale, ClassLoader)
     */
    public static ResourceBundle getEquinoxResourceBundle(String baseName, Locale locale, ClassLoader loader) {
        ResourceBundle resourceBundle = null;

        String equinoxLocale = getEquinoxRootLocale();
        //if the equinox.root.locale is not empty and the specified locale equals the equinox.root.locale
        // -> use the special search order
        if (equinoxLocale.length() > 0 && locale.toString().startsWith(equinoxLocale)) {
            //there is a equinox.root.locale configured that matches the specified locale 
            //so the special search order is used
            //to achieve this we first search without a fallback to the default locale
            try {
                resourceBundle = ResourceBundle.getBundle(baseName, locale, loader,
                        ResourceBundle.Control.getNoFallbackControl(Control.FORMAT_DEFAULT));
            } catch (MissingResourceException e) {
                //do nothing
            }
            //if there is no ResourceBundle found for that path, we will now search for the default locale ResourceBundle
            if (resourceBundle == null) {
                try {
                    resourceBundle = ResourceBundle.getBundle(baseName, Locale.getDefault(), loader,
                            ResourceBundle.Control.getNoFallbackControl(Control.FORMAT_DEFAULT));
                } catch (MissingResourceException e) {
                    //do nothing
                }
            }
        } else {
            //there is either no equinox.root.locale configured or it does not match the specified locale
            // -> use the default search order
            try {
                resourceBundle = ResourceBundle.getBundle(baseName, locale, loader);
            } catch (MissingResourceException e) {
                //do nothing
            }
        }

        return resourceBundle;
    }

    /**
     * This method searches for the {@link ResourceBundle} in a modified way by inspecting the configuration option 
     * <code>equinox.root.locale</code>. It uses the {@link BundleResourceBundleControl} to load the resources out
     * of a {@link Bundle}.
     * <p><b>Note: This method will only search for ResourceBundles based on properties files.</b></p>
     * <p>
     * If the value for this system property is set to an empty String the default search order for ResourceBundles is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * Where bn is this bundle's localization basename, Ls, Cs and Vs are the specified locale (language, country, variant) and 
     * Ld, Cd and Vd are the default locale (language, country, variant).
     * </p>
     * <p>
     * If Ls equals the value of <code>equinox.root.locale</code> then the following search order is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * </p>
     * If <code>equinox.root.locale=en</code> and en_XX or en is asked for then this allows the root file to be used instead of 
     * falling back to the default locale.
     * 
      * @param baseName the base name of the resource bundle, a fully qualified class name
      * @param locale the locale for which a resource bundle is desired
     * @param bundle The OSGi {@link Bundle} to lookup the {@link ResourceBundle}
      * @return a resource bundle for the given base name and locale
     * 
     * @see ResourceBundle#getBundle(String, Locale, Control)
     */
    public static ResourceBundle getEquinoxResourceBundle(String baseName, Locale locale, Bundle bundle) {
        return getEquinoxResourceBundle(baseName, locale, new BundleResourceBundleControl(bundle, true),
                new BundleResourceBundleControl(bundle, false));
    }

    /**
     * This method searches for the {@link ResourceBundle} in a modified way by inspecting the configuration option 
     * <code>equinox.root.locale</code>.
     * <p><b>Note: This method will only search for ResourceBundles based on properties files.</b></p>
     * <p>
     * If the value for this system property is set to an empty String the default search order for ResourceBundles is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * Where bn is this bundle's localization basename, Ls, Cs and Vs are the specified locale (language, country, variant) and 
     * Ld, Cd and Vd are the default locale (language, country, variant).
     * </p>
     * <p>
     * If Ls equals the value of <code>equinox.root.locale</code> then the following search order is used:
     * <ul>
     * <li>bn + Ls + "_" + Cs + "_" + Vs</li>
     * <li>bn + Ls + "_" + Cs</li>
     * <li>bn + Ls</li>
     * <li>bn</li>
     * <li>bn + Ld + "_" + Cd + "_" + Vd</li>
     * <li>bn + Ld + "_" + Cd</li>
     * <li>bn + Ld</li>
     * <li>bn</li>
     * </ul>
     * </p>
     * If <code>equinox.root.locale=en</code> and en_XX or en is asked for then this allows the root file to be used instead of 
     * falling back to the default locale.
     * 
      * @param baseName the base name of the resource bundle, a fully qualified class name
      * @param locale the locale for which a resource bundle is desired
     * @param withFallback The {@link Control} that uses the default locale fallback on searching for resource bundles.
     * @param withoutFallback The {@link Control} that doesn't use the default locale fallback on searching for resource bundles.
      * @return a resource bundle for the given base name and locale
     * 
     * @see ResourceBundle#getBundle(String, Locale, Control)
     */
    public static ResourceBundle getEquinoxResourceBundle(String baseName, Locale locale, Control withFallback,
            Control withoutFallback) {
        ResourceBundle resourceBundle = null;

        String equinoxLocale = getEquinoxRootLocale();
        //if the equinox.root.locale is not empty and the specified locale equals the equinox.root.locale
        // -> use the special search order
        if (equinoxLocale.length() > 0 && locale.toString().startsWith(equinoxLocale)) {
            //there is a equinox.root.locale configured that matches the specified locale 
            //so the special search order is used
            //to achieve this we first search without a fallback to the default locale
            try {
                resourceBundle = ResourceBundle.getBundle(baseName, locale, withoutFallback);
            } catch (MissingResourceException e) {
                //do nothing
            }
            //if there is no ResourceBundle found for that path, we will now search for the default locale ResourceBundle
            if (resourceBundle == null) {
                try {
                    resourceBundle = ResourceBundle.getBundle(baseName, Locale.getDefault(), withoutFallback);
                } catch (MissingResourceException e) {
                    //do nothing
                }
            }
        } else {
            //there is either no equinox.root.locale configured or it does not match the specified locale
            // -> use the default search order
            try {
                resourceBundle = ResourceBundle.getBundle(baseName, locale, withFallback);
            } catch (MissingResourceException e) {
                //do nothing
            }
        }

        return resourceBundle;
    }

    /**
     * @return The value for the system property for key <code>equinox.root.locale</code>.
     *          If none is specified than <b>en</b> will be returned as default.
     */
    private static String getEquinoxRootLocale() {
        // Logic from FrameworkProperties.getProperty("equinox.root.locale", "en")
        String root = System.getProperties().getProperty("equinox.root.locale"); //$NON-NLS-1$
        if (root == null) {
            root = "en"; //$NON-NLS-1$
        }
        return root;
    }

    /**
     * This method is copied out of org.eclipse.e4.ui.internal.workbench.Activator
     * because as it is a internal resource, it is not accessible for us.
     * 
     * @param bundleName
     *            the bundle id
     * @return A bundle if found, or <code>null</code>
     */
    public static Bundle getBundleForName(String bundleName) {
        PackageAdmin packageAdmin = ToolsServicesActivator.getDefault().getPackageAdmin();
        Bundle[] bundles = packageAdmin.getBundles(bundleName, null);
        if (bundles == null)
            return null;
        // Return the first bundle that is not installed or uninstalled
        for (int i = 0; i < bundles.length; i++) {
            if ((bundles[i].getState() & (Bundle.INSTALLED | Bundle.UNINSTALLED)) == 0) {
                return bundles[i];
            }
        }
        return null;
    }

    /**
     * <p>Converts a String to a Locale.</p>
     *
     * <p>This method takes the string format of a locale and creates the
     * locale object from it.</p>
     *
     * <pre>
     *   MessageFactoryServiceImpl.toLocale("en")         = new Locale("en", "")
     *   MessageFactoryServiceImpl.toLocale("en_GB")      = new Locale("en", "GB")
     *   MessageFactoryServiceImpl.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")
     * </pre>
     *
     * <p>This method validates the input strictly.
     * The language code must be lowercase.
     * The country code must be uppercase.
     * The separator must be an underscore.
     * The length must be correct.
     * </p>
     *
     * <p>This method is inspired by <code>org.apache.commons.lang.LocaleUtils.toLocale(String)</code> by
     * fixing the parsing error for uncommon Locales like having a language and a variant code but
     * no country code, or a Locale that only consists of a country code.
     * </p>
     *  
     * @param str the locale String to convert 
     * @return a Locale that matches the specified locale String or <code>null</code> 
     *          if the specified String is <code>null</code>
     * @throws IllegalArgumentException if the String is an invalid format
     */
    public static Locale toLocale(String str) {
        if (str == null) {
            return null;
        }

        String language = ""; //$NON-NLS-1$
        String country = ""; //$NON-NLS-1$
        String variant = ""; //$NON-NLS-1$

        String[] localeParts = str.split("_"); //$NON-NLS-1$
        if (localeParts.length == 0 || localeParts.length > 3
                || (localeParts.length == 1 && localeParts[0].length() == 0)) {
            throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
        } else {
            if (localeParts[0].length() == 1 || localeParts[0].length() > 2) {
                throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
            } else if (localeParts[0].length() == 2) {
                char ch0 = localeParts[0].charAt(0);
                char ch1 = localeParts[0].charAt(1);
                if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
                    throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
                }
            }

            language = localeParts[0];

            if (localeParts.length > 1) {
                if (localeParts[1].length() == 1 || localeParts[1].length() > 2) {
                    throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
                } else if (localeParts[1].length() == 2) {
                    char ch3 = localeParts[1].charAt(0);
                    char ch4 = localeParts[1].charAt(1);
                    if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
                        throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
                    }
                }

                country = localeParts[1];
            }

            if (localeParts.length == 3) {
                if (localeParts[0].length() == 0 && localeParts[1].length() == 0) {
                    throw new IllegalArgumentException("Invalid locale format: " + str); //$NON-NLS-1$
                }
                variant = localeParts[2];
            }
        }

        return new Locale(language, country, variant);
    }

    /**
     * Specialization of {@link Control} which loads the {@link ResourceBundle} out of an
     * OSGi {@link Bundle} instead of using a classloader.
     * 
     * <p>It only supports properties based {@link ResourceBundle}s. If you want to use 
     * source based {@link ResourceBundle}s you have to use the bundleclass URI with the
     * Message annotation.
     * 
     * @author Dirk Fauth
     *
     */
    static class BundleResourceBundleControl extends ResourceBundle.Control {

        /**
         * Flag to determine whether the default locale should be used as fallback locale
         * in case there is no {@link ResourceBundle} found for the specified locale.
         */
        private final boolean useFallback;

        /**
         * The OSGi {@link Bundle} to lookup the {@link ResourceBundle}
         */
        private final Bundle osgiBundle;

        /**
         * 
         * @param osgiBundle The OSGi {@link Bundle} to lookup the {@link ResourceBundle}
         * @param useFallback <code>true</code> if the default locale should be used as fallback
         *          locale in the search path or <code>false</code> if there should be no fallback.
         */
        public BundleResourceBundleControl(Bundle osgiBundle, boolean useFallback) {
            this.osgiBundle = osgiBundle;
            this.useFallback = useFallback;
        }

        @Override
        public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader,
                boolean reload) throws IllegalAccessException, InstantiationException, IOException {

            String bundleName = toBundleName(baseName, locale);
            ResourceBundle bundle = null;
            if (format.equals("java.properties")) { //$NON-NLS-1$
                final String resourceName = toResourceName(bundleName, "properties"); //$NON-NLS-1$
                InputStream stream = null;
                try {
                    stream = AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
                        public InputStream run() throws IOException {
                            InputStream is = null;
                            URL url = osgiBundle.getEntry(resourceName);
                            if (url != null) {
                                URLConnection connection = url.openConnection();
                                if (connection != null) {
                                    // Disable caches to get fresh data for
                                    // reloading.
                                    connection.setUseCaches(false);
                                    is = connection.getInputStream();
                                }
                            }
                            return is;
                        }
                    });
                } catch (PrivilegedActionException e) {
                    throw (IOException) e.getException();
                }
                if (stream != null) {
                    try {
                        bundle = new PropertyResourceBundle(stream);
                    } finally {
                        stream.close();
                    }
                }
            } else {
                throw new IllegalArgumentException("unknown format: " + format); //$NON-NLS-1$
            }
            return bundle;
        }

        @Override
        public List<String> getFormats(String baseName) {
            return FORMAT_PROPERTIES;
        }

        @Override
        public Locale getFallbackLocale(String baseName, Locale locale) {
            return this.useFallback ? super.getFallbackLocale(baseName, locale) : null;
        }
    }
}