Java tutorial
/******************************************************************************* * 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; } } }