From 017da40e2c6e8c7a3e48d94eff9c64a729a4705c Mon Sep 17 00:00:00 2001 From: Dirk Fauth Date: Sat, 18 May 2013 17:41:24 +0200 Subject: Bug 406632 - Add support for Locale changes at runtime --- .../e4/tools/services/IMessageFactoryService.java | 26 +++- .../services/impl/MessageFactoryServiceImpl.java | 66 ++++++--- .../services/impl/TranslationObjectSupplier.java | 152 ++++++++++++++++++--- 3 files changed, 205 insertions(+), 39 deletions(-) diff --git a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/IMessageFactoryService.java b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/IMessageFactoryService.java index b5512df7..b93d9b2c 100644 --- a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/IMessageFactoryService.java +++ b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/IMessageFactoryService.java @@ -1,8 +1,32 @@ package org.eclipse.e4.tools.services; +import java.util.Locale; +import java.util.ResourceBundle; + import org.eclipse.osgi.service.localization.BundleLocalization; +/** + * Service that is responsible for creating and managing message class instances. + */ public interface IMessageFactoryService { - public M createInstance(final String locale, final Class messages, BundleLocalization localization) + + /** + * Returns an instance of the of a given messages class for the given {@link Locale}. + * If configured it caches the created instances and return the already created instances. + * Otherwise a new instance will be created. + * + * @param locale The {@link Locale} for which the message class instance is requested. + * @param messages The type of the message class whose instance is requested. + * @param localization The service that is needed to retrieve {@link ResourceBundle} objects from a bundle + * with a given locale. + * @return An instance of the given messages class and {@link Locale}. + * + * @throws InstantiationException if the requested message class represents an abstract class, an interface, + * an array class, a primitive type, or void; + * or if the class has no nullary constructor; + * or if the instantiation fails for some other reason. + * @throws IllegalAccessException if the requested message class or its nullary constructor is not accessible. + */ + public M getMessageInstance(final Locale locale, final Class messages, BundleLocalization localization) throws InstantiationException, IllegalAccessException; } diff --git a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/MessageFactoryServiceImpl.java b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/MessageFactoryServiceImpl.java index e09b23b6..27a23467 100644 --- a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/MessageFactoryServiceImpl.java +++ b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/MessageFactoryServiceImpl.java @@ -7,6 +7,7 @@ * * Contributors: * Tom Schindl - initial API and implementation + * Dirk Fauth - modifications to instance creation ******************************************************************************/ package org.eclipse.e4.tools.services.impl; @@ -28,16 +29,12 @@ import java.util.ResourceBundle; import org.eclipse.e4.tools.services.IMessageFactoryService; import org.eclipse.e4.tools.services.Message; import org.eclipse.e4.tools.services.Message.ReferenceType; -import org.eclipse.e4.tools.services.ToolsServicesActivator; import org.eclipse.osgi.service.localization.BundleLocalization; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; -import org.osgi.service.log.LogService; public class MessageFactoryServiceImpl implements IMessageFactoryService { - private static LogService logService = ToolsServicesActivator.getDefault().getLogService(); - // Cache so when multiple instance use the same message class private Map> SOFT_CACHE = Collections .synchronizedMap(new HashMap>()); @@ -47,7 +44,8 @@ public class MessageFactoryServiceImpl implements IMessageFactoryService { private int CLEANUPCOUNT = 0; - public M createInstance(final String locale, final Class messages, final BundleLocalization localization) + @Override + public M getMessageInstance(final Locale locale, final Class messages, final BundleLocalization localization) throws InstantiationException, IllegalAccessException { String key = messages.getName() + "_" + locale; //$NON-NLS-1$ @@ -95,13 +93,13 @@ public class MessageFactoryServiceImpl implements IMessageFactoryService { M instance; if (System.getSecurityManager() == null) { - instance = doCreateInstance(locale, messages, annotation, localization); + instance = createInstance(locale, messages, annotation, localization); } else { instance = AccessController.doPrivileged(new PrivilegedAction() { public M run() { try { - return doCreateInstance(locale, messages, annotation, localization); + return createInstance(locale, messages, annotation, localization); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { @@ -124,24 +122,48 @@ public class MessageFactoryServiceImpl implements IMessageFactoryService { return instance; } - private static M doCreateInstance(String locale, Class messages, + /** + * Creates and returns an instance of the of a given messages class for the given {@link Locale}. + * The message class gets instantiated and the fields are initialized with values out of a {@link ResourceBundle}. + * As there are several options to specify the location of the {@link ResourceBundle} to load, the + * following search order is used: + *
    + *
  1. URI location
    + * If the message class is annotated with @Message and the contributorURI + * attribute is set, the {@link ResourceBundle} is searched at the specified location
  2. + *
  3. Relative location
    + * If the message class is not annotated with @Message and a contributorURI + * attribute value or there is no {@link ResourceBundle} found at the specified location, a + * {@link ResourceBundle} with the same name in the same package as the message class is searched.
  4. + *
  5. Bundle localization
    + * If there is no {@link ResourceBundle} found by URI or relative location, the OSGi {@link ResourceBundle} + * configured in the MANIFEST.MF is tried to load.
  6. + *
+ * Note: Even if there is no {@link ResourceBundle} found in any of the mentioned locations, this method will + * not break. In this case the fields of the message class will get initialized with values that look + * like !key! to indicate that there is no translation value found for that key. + * + * @param locale The {@link Locale} for which the message class instance is requested. + * @param messages The type of the message class whose instance is requested. + * @param annotation The annotation that is used in the message class. If specified it is needed + * to retrieve the URI of the location to search for the {@link ResourceBundle}. + * @param localization The service that is needed to retrieve {@link ResourceBundle} objects from a bundle + * with a given locale. + * @return The created instance of the given messages class and {@link Locale}. + * + * @throws InstantiationException if the requested message class represents an abstract class, an interface, + * an array class, a primitive type, or void; + * or if the class has no nullary constructor; + * or if the instantiation fails for some other reason. + * @throws IllegalAccessException if the requested message class or its nullary constructor is not accessible. + */ + private static M createInstance(Locale locale, Class messages, Message annotation, BundleLocalization localization) throws InstantiationException, IllegalAccessException { - Locale loc = null; - try { - loc = locale == null ? Locale.getDefault() : ResourceBundleHelper.toLocale(locale); - } - catch (Exception e) { - //parsing the locale String to a Locale failed, so we use the default Locale - if (logService != null) - logService.log(LogService.LOG_ERROR, "Invalid locale", e); //$NON-NLS-1$ - loc = Locale.getDefault(); - } - ResourceBundle resourceBundle = null; if (annotation != null && annotation.contributorURI().length() > 0) { - resourceBundle = ResourceBundleHelper.getResourceBundleForUri(annotation.contributorURI(), loc, localization); + resourceBundle = ResourceBundleHelper.getResourceBundleForUri(annotation.contributorURI(), locale, localization); } if (resourceBundle == null) { @@ -149,7 +171,7 @@ public class MessageFactoryServiceImpl implements IMessageFactoryService { String baseName = messages.getName().replace('.', '/'); try { - resourceBundle = ResourceBundleHelper.getEquinoxResourceBundle(baseName, loc, messages.getClassLoader()); + resourceBundle = ResourceBundleHelper.getEquinoxResourceBundle(baseName, locale, messages.getClassLoader()); } catch (MissingResourceException e) { //do nothing as this just means there is no resource bundle named @@ -161,7 +183,7 @@ public class MessageFactoryServiceImpl implements IMessageFactoryService { if (resourceBundle == null) { //retrieve the OSGi resource bundle Bundle bundle = FrameworkUtil.getBundle(messages); - resourceBundle = localization.getLocalization(bundle, locale); + resourceBundle = localization.getLocalization(bundle, locale.toString()); } //always create a provider, if there is no resource bundle found, simply the modified keys will diff --git a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/TranslationObjectSupplier.java b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/TranslationObjectSupplier.java index 31c06b87..1350a0bf 100644 --- a/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/TranslationObjectSupplier.java +++ b/bundles/org.eclipse.e4.tools.services/src/org/eclipse/e4/tools/services/impl/TranslationObjectSupplier.java @@ -7,49 +7,170 @@ * * Contributors: * Tom Schindl - initial API and implementation + * Dirk Fauth - modifications to support locale changes at runtime ******************************************************************************/ package org.eclipse.e4.tools.services.impl; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Named; + +import org.eclipse.e4.core.di.annotations.Optional; import org.eclipse.e4.core.di.suppliers.ExtendedObjectSupplier; import org.eclipse.e4.core.di.suppliers.IObjectDescriptor; import org.eclipse.e4.core.di.suppliers.IRequestor; -import org.eclipse.e4.core.internal.contexts.ContextObjectSupplier; -import org.eclipse.e4.core.internal.di.Requestor; import org.eclipse.e4.core.services.translation.TranslationService; import org.eclipse.e4.tools.services.IMessageFactoryService; +import org.eclipse.e4.tools.services.Message; +import org.eclipse.e4.tools.services.ToolsServicesActivator; import org.eclipse.osgi.service.localization.BundleLocalization; +import org.osgi.service.log.LogService; +@SuppressWarnings("rawtypes") public class TranslationObjectSupplier extends ExtendedObjectSupplier { + private static LogService logService = ToolsServicesActivator.getDefault().getLogService(); + + /** + * The current active locale that gets injected for updating the message instances. + */ + private Locale locale; + + /** + * The service that gets {@link ResourceBundle} objects from a bundle with a given locale. + */ + @Inject + private BundleLocalization localization; + + /** + * The service that creates instances of message classes based on the current active locale, + * the {@link BundleLocalization} and the configuration used in the {@link Message} annotation. + */ + @Inject + private IMessageFactoryService factoryService; + + /** + * Map that contains all {@link IRequestor} that requested an instance of a messages class. + * Used to inform all requestor if the instances have changed due to a locale change. + */ + private Map> listeners = new HashMap>(); + @Override public Object get(IObjectDescriptor descriptor, IRequestor requestor, boolean track, boolean group) { + Class descriptorsClass = getDesiredClass(descriptor.getDesiredType()); - Requestor req = (Requestor) requestor; - ContextObjectSupplier sub = (ContextObjectSupplier) req - .getPrimarySupplier(); - - String locale = (String) sub.getContext().get(TranslationService.LOCALE); - locale = locale == null ? Locale.getDefault().toString() : locale; - BundleLocalization localization = sub.getContext().get(BundleLocalization.class); - IMessageFactoryService factoryService = sub.getContext().get(IMessageFactoryService.class); + if (track) + addListener(descriptorsClass, requestor); + + return getMessageInstance(descriptorsClass); + } + + /** + * Setting the {@link Locale} by using this method will cause to create new instances for all + * message classes that were requested before. It also notifys all {@link IRequestor} that requested + * those messages instance which causes dynamic reinjection. + * @param locale The {@link Locale} to use for creating the message instances. + */ + @Inject + public void setLocale(@Optional @Named(TranslationService.LOCALE) String locale) { + try { + this.locale = locale == null ? Locale.getDefault() : ResourceBundleHelper.toLocale(locale); + } + catch (IllegalArgumentException e) { + //parsing the locale String to a Locale failed because of invalid String, use the default locale + if (logService != null) + logService.log(LogService.LOG_ERROR, e.getMessage() + " - Default Locale will be used instead."); //$NON-NLS-1$ + this.locale = Locale.getDefault(); + } + catch (Exception e) { + //parsing the locale String to a Locale failed, so we use the default Locale + if (logService != null) + logService.log(LogService.LOG_ERROR, "Invalid locale", e); //$NON-NLS-1$ + this.locale = Locale.getDefault(); + } + //update listener + updateMessages(); + } + + /** + * Notify the {@link IRequestor}s of those instances that they need to update their message class instances. + */ + private void updateMessages() { + for (Map.Entry> entry : this.listeners.entrySet()) { + notifyRequestor(entry.getValue()); + } + } + + /** + * Checks if for the specified descriptor class there is already an instance in the local + * cache. If not a new instance is created using the local configuration on {@link Locale}, + * {@link BundleLocalization} and given descriptor class. + * @param descriptorsClass The class for which an instance is requested. + * @return The instance of the requested message class + */ + private Object getMessageInstance(Class descriptorsClass) { try { - return factoryService.createInstance(locale, descriptorsClass, localization); + return this.factoryService.getMessageInstance( + this.locale, descriptorsClass, this.localization); } catch (InstantiationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + if (logService != null) + logService.log(LogService.LOG_ERROR, + "Instantiation of messages class failed", e); //$NON-NLS-1$ } catch (IllegalAccessException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + if (logService != null) + logService.log(LogService.LOG_ERROR, + "Failed to access messages class", e); //$NON-NLS-1$ } return null; } + + /** + * Remember the {@link IRequestor} that requested an instance of the given descriptor class. + * This is needed to be able to inform all {@link IRequestor} if the {@link Locale} changes + * at runtime. + * @param descriptorsClass The class for which an instance was requested. + * @param requestor The {@link IRequestor} that requested the instance. + */ + private void addListener(Class descriptorsClass, IRequestor requestor) { + Set registered = this.listeners.get(descriptorsClass); + if (registered == null) { + registered = new HashSet(); + this.listeners.put(descriptorsClass, registered); + } + registered.add(requestor); + } + + /** + * Notify all given {@link IRequestor} about changes for their injected values. + * This way the dynamic injection is performed. + * @param requestors The {@link IRequestor} to inform about the instance changes. + */ + private void notifyRequestor(Collection requestors) { + if (requestors != null) { + for (Iterator it = requestors.iterator(); it.hasNext();) { + IRequestor requestor = it.next(); + if (!requestor.isValid()) { + it.remove(); + continue; + } + requestor.resolveArguments(false); + requestor.execute(); + } + } + } private Class getDesiredClass(Type desiredType) { if (desiredType instanceof Class) @@ -61,5 +182,4 @@ public class TranslationObjectSupplier extends ExtendedObjectSupplier { } return null; } - } \ No newline at end of file -- cgit v1.2.3