Bug 428621 - Created the BaseMessageRegistry to be able to bind
localizable properties of objects to a field in a messages instance
Change-Id: If839b011341a610d9a11135e401a7e9eaa6a7138
Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/BaseMessageRegistry.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/BaseMessageRegistry.java
new file mode 100644
index 0000000..1e86a46
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/BaseMessageRegistry.java
@@ -0,0 +1,340 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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@googlemail.com> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.services.nls;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.PreDestroy;
+import org.eclipse.e4.core.internal.services.ServicesActivator;
+import org.osgi.service.log.LogService;
+
+/**
+ * Using this MessageRegistry allows to register controls for attributes in a
+ * Messages class. These controls will automatically get updated in case of
+ * Locale changes.
+ * <p>
+ * When updating the dependencies from Java 7 to Java 8, this class can be
+ * replaced by a more modern variant that makes use of functional interfaces and
+ * method references as shown in the above linked blog post.
+ * </p>
+ *
+ * <p>
+ * To use the registry you need to implement a subclass of
+ * <code>BaseMessageRegistry</code> that is typed to the messages class that it
+ * is related to. The main thing to do is to override
+ * <code>updateMessages(M)</code> while getting the messages instance injected.
+ * </p>
+ *
+ * <pre>
+ * @Creatable
+ * public class ExampleMessageRegistry
+ * extends
+ * BaseMessageRegistry<ExampleMessages> {
+ *
+ * @Override
+ * @Inject
+ * public void updateMessages(@Translation ExampleMessages messages) {
+ * super.updateMessages(messages);
+ * }
+ * }
+ * </pre>
+ *
+ * <p>
+ * Note that the registry instance is annotated with @Creatable so it is
+ * created per requestor and is making use of DI.
+ * </p>
+ *
+ * @param <M>
+ * the message class type
+ * @since 2.0
+ */
+public class BaseMessageRegistry<M> {
+
+ private static LogService logService = ServicesActivator.getDefault().getLogService();
+
+ private M messages;
+
+ private final Map<MessageConsumer, MessageSupplier> bindings = new HashMap<MessageConsumer, MessageSupplier>();
+
+ /**
+ * Register a consumer and a function that is acting as the supplier of the translation value.
+ * <p>
+ * This method allows to register a binding using method references and lambdas if used in an
+ * environment that already uses Java 8.
+ * </p>
+ *
+ * <pre>
+ * @Inject
+ * ExampleMessageRegistry registry;
+ *
+ * Label myFirstLabel = new Label(parent, SWT.WRAP);
+ * registry.register(myFirstLabel::setText, (m) -> m.firstLabelMessage);
+ * </pre>
+ *
+ * @param consumer
+ * The consumer of the message.
+ * @param function
+ * The function that supplies the message.
+ */
+ public void register(MessageConsumer consumer, final MessageFunction<M> function) {
+ register(consumer, new MessageSupplier() {
+
+ @Override
+ public String get() {
+ return function.apply(messages);
+ }
+ });
+ }
+
+ /**
+ * Register a binding for the given consumer and supplier.
+ *
+ * <p>
+ * Unless you don't want to anonymously implement the consumer and supplier interfaces yourself,
+ * use the register methods that take the Control instance and String(s) as parameters.
+ * </p>
+ *
+ * @param consumer
+ * The consumer of the message.
+ * @param supplier
+ * The supplier of the message.
+ *
+ * @see BaseMessageRegistry#register(Object, String, String)
+ * @see BaseMessageRegistry#registerProperty(Object, String, String)
+ */
+ public void register(MessageConsumer consumer, MessageSupplier supplier) {
+ //set the value to the control
+ consumer.accept(supplier.get());
+ //remember the control and the supplier
+ bindings.put(consumer, supplier);
+ }
+
+ /**
+ * Binds a method of an object to a message. Doing this the specified method will be called on
+ * the instance with the message String as parameter that is retrieved via message key out of
+ * the local Messages instance.
+ *
+ * @param control
+ * The control for which a message binding should be created
+ * @param method
+ * The method that should be bound. Methods that can be bound need to accept one
+ * String parameter.
+ * @param messageKey
+ * The key of the message property that should be bound
+ *
+ * @see BaseMessageRegistry#registerProperty(Object, String, String)
+ */
+ public void register(final Object control, final String method, final String messageKey) {
+ MessageConsumer consumer = createConsumer(control, method);
+ MessageSupplier supplier = createSupplier(messageKey);
+ //only register if consumer and supplier were created
+ if (consumer != null && supplier != null)
+ register(consumer, supplier);
+ }
+
+ /**
+ * Binds the setter of a property of an object to a message. Doing this the setter of the given
+ * property will be called on the instance with the message String as parameter that is
+ * retrieved via message key out of the local Messages instance.
+ *
+ * @param control
+ * The control for which a message binding should be created
+ * @param property
+ * The property of the control which should be bound
+ * @param messageKey
+ * The key of the message property that should be bound
+ *
+ * @see BaseMessageRegistry#register(Object, String, String)
+ */
+ public void registerProperty(final Object control, final String property, final String messageKey) {
+ MessageConsumer consumer = createConsumer(control, "set" + Character.toUpperCase(property.charAt(0)) + property.substring(1));
+ MessageSupplier supplier = createSupplier(messageKey);
+ //only register if consumer and supplier were created
+ if (consumer != null && supplier != null)
+ register(consumer, supplier);
+ }
+
+ /**
+ * This method performs the localization update for all bound objects.
+ * <p>
+ * Typically this method is overriden by a concrete implementation where the Messages instance
+ * is injected via @Inject and @Translation.
+ * </p>
+ *
+ * @param messages
+ * The new Messages instance that should be used to update the localization.
+ */
+ public void updateMessages(M messages) {
+ //remember the current message instance
+ this.messages = messages;
+ //iterate over all registered consumer
+ for (Map.Entry<MessageConsumer, MessageSupplier> entry : bindings.entrySet()) {
+ entry.getKey().accept(entry.getValue().get());
+ }
+ }
+
+ /**
+ *
+ * @param control
+ * The control on which the created consumer should operate
+ * @param method
+ * The method the created consumer should call to set the new value
+ * @return A MessageConsumer that sets a value to the property of the given control
+ */
+ protected MessageConsumer createConsumer(final Object control, final String method) {
+ MessageConsumer consumer = null;
+
+ try {
+ final Method m = control.getClass().getMethod(method, String.class);
+ if (m != null) {
+
+ consumer = new MessageConsumer() {
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ @Override
+ public void accept(final String value) {
+ try {
+ // ensure the method is accessible so the registry
+ // also works well with protected or package
+ // protected classes
+ if (System.getSecurityManager() == null) {
+ m.setAccessible(true);
+ m.invoke(control, value);
+ } else {
+ AccessController
+ .doPrivileged(new PrivilegedAction() {
+
+ @Override
+ public Object run() {
+ m.setAccessible(true);
+ try {
+ m.invoke(control, value);
+ } catch (Exception e) {
+ // if anything fails on
+ // invoke we unregister the
+ // binding to avoid further
+ // issues e.g. this can
+ // happen in case of
+ // disposed SWT controls
+ bindings.remove(this);
+ if (logService != null)
+ logService
+ .log(LogService.LOG_INFO,
+ "Error on invoke '"
+ + m.getName()
+ + "' on '"
+ + control
+ .getClass()
+ + "' with error message '"
+ + e.getMessage()
+ + "'. Binding is removed.");
+ }
+ return null;
+ }
+
+ });
+ }
+ } catch (Exception e) {
+ //if anything fails on invoke we unregister the binding
+ //to avoid further issues
+ //e.g. this can happen in case of disposed SWT controls
+ bindings.remove(this);
+ if (logService != null)
+ logService.log(LogService.LOG_INFO,
+ "Error on invoke '" + m.getName() + "' on '"
+ + control.getClass()
+ + "' with error message '" + e.getMessage()
+ + "'. Binding is removed.");
+ }
+ }
+ };
+
+ }
+ } catch (NoSuchMethodException e) {
+ if (logService != null)
+ logService.log(LogService.LOG_WARNING,
+ "The method '" + e.getMessage()
+ + "' does not exist. Binding is not created!");
+ } catch (SecurityException e) {
+ if (logService != null)
+ logService.log(
+ LogService.LOG_WARNING,
+ "Error on accessing method '" + method + "' on class '"
+ + control.getClass() + "' with error message '" + e.getMessage()
+ + "'. Binding is not created!");
+ }
+
+ return consumer;
+ }
+
+ /**
+ *
+ * @param messageKey
+ * The name of the field that should be accessed
+ * @return A MessageSupplier that returns the message value for the given message key
+ */
+ protected MessageSupplier createSupplier(final String messageKey) {
+ MessageSupplier supplier = null;
+
+ try {
+ final Field f = messages.getClass().getField(messageKey);
+ if (f != null) {
+ supplier = new MessageSupplier() {
+
+ @Override
+ public String get() {
+ String message = null;
+ try {
+ message = (String) f.get(messages);
+ } catch (Exception e) {
+ // if anything fails on invoke we unregister the binding
+ // to avoid further issues
+ // e.g. this can happen in case of disposed SWT controls
+ bindings.remove(this);
+ if (logService != null)
+ logService.log(
+ LogService.LOG_INFO,
+ "Error on invoke '" + f.getName() + "' on '"
+ + messages.getClass() + "' with error message '"
+ + e.getMessage() + "'. Binding is removed.");
+ }
+
+ return message;
+ }
+ };
+ }
+ } catch (NoSuchFieldException e) {
+ if (logService != null)
+ logService.log(LogService.LOG_WARNING, "The class '"
+ + this.messages.getClass().getName()
+ + "' does not contain a field with name '" + e.getMessage()
+ + "'. Binding is not created!");
+ } catch (SecurityException e) {
+ if (logService != null)
+ logService.log(
+ LogService.LOG_WARNING,
+ "Error on accessing field '" + messageKey + "' on class '"
+ + messages.getClass() + "' with error message '" + e.getMessage()
+ + "'. Binding is not created!");
+ }
+
+ return supplier;
+ }
+
+ @PreDestroy
+ void unregister() {
+ this.bindings.clear();
+ }
+}
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageConsumer.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageConsumer.java
new file mode 100644
index 0000000..f7fd622
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageConsumer.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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@googlemail.com> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.services.nls;
+
+/**
+ * Consumer interface that is used to bind a method of an object (e.g. a SWT
+ * control) to a field of a Messages instance. Typically the setter of a
+ * localizable property will be called with the given value as parameter (e.g.
+ * <code>label.setText(value)</code>)
+ * <p>
+ * This is a functional interface whose functional method is
+ * {@link #accept(String)}.
+ * </p>
+ * <p>
+ * When updating to Java 8 this interface can be removed and replaced with the
+ * general <code>java.util.function.Consumer</code>
+ * </p>
+ *
+ * @since 2.0
+ *
+ */
+public interface MessageConsumer {
+
+ /**
+ *
+ * @param value
+ * The localization value that should be set to the control.
+ */
+ void accept(String value);
+}
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageFunction.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageFunction.java
new file mode 100644
index 0000000..710451c
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageFunction.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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@googlemail.com> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.services.nls;
+
+/**
+ * Function interface that is used to bind a property of an object (e.g. a SWT
+ * control) to a field of a Messages instance. This interface is intended to be
+ * used with implementations that use Java 8, so it is possible to operate using
+ * method references and lambdas.
+ * <p>
+ * This is a functional interface whose functional method is {@link #apply(M)}.
+ * </p>
+ * <p>
+ * When updating to Java 8 this interface can be removed and replaced with the
+ * general <code>java.util.function.Function</code>
+ * </p>
+ *
+ * @since 2.0
+ *
+ * @param <M>
+ * the message class type
+ */
+public interface MessageFunction<M> {
+
+ /**
+ *
+ * @param m
+ * The message instance from which the value should be retrieved.
+ * @return The message value out of the given messages instance.
+ */
+ String apply(M m);
+}
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageSupplier.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageSupplier.java
new file mode 100644
index 0000000..2f93a84
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/nls/MessageSupplier.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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@googlemail.com> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.services.nls;
+
+/**
+ * Supplier interface that is used to bind a method of an object (e.g. a SWT
+ * control) to a field of a Messages instance. Typically the instance field that
+ * contains the localization value will be returned.
+ * <p>
+ * This is a functional interface whose functional method is {@link #get()}.
+ * </p>
+ * <p>
+ * When updating to Java 8 this interface can be removed and replaced with the
+ * general <code>java.util.function.Supplier</code>
+ * </p>
+ *
+ * @since 2.0
+ *
+ */
+public interface MessageSupplier {
+
+ /**
+ *
+ * @return The value this {@link MessageSupplier} holds.
+ */
+ String get();
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/BundleMessagesRegistry.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/BundleMessagesRegistry.java
new file mode 100644
index 0000000..d21c613
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/BundleMessagesRegistry.java
@@ -0,0 +1,18 @@
+package org.eclipse.e4.core.internal.tests.nls;
+
+import javax.inject.Inject;
+
+import org.eclipse.e4.core.di.annotations.Creatable;
+import org.eclipse.e4.core.services.nls.BaseMessageRegistry;
+import org.eclipse.e4.core.services.nls.Translation;
+
+@Creatable
+public class BundleMessagesRegistry extends BaseMessageRegistry<BundleMessages> {
+
+ @Override
+ @Inject
+ public void updateMessages(@Translation BundleMessages messages) {
+ super.updateMessages(messages);
+ }
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/MessageRegistryTest.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/MessageRegistryTest.java
new file mode 100644
index 0000000..0905375
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/nls/MessageRegistryTest.java
@@ -0,0 +1,113 @@
+package org.eclipse.e4.core.internal.tests.nls;
+
+import java.util.Locale;
+
+import javax.inject.Inject;
+
+import junit.framework.TestCase;
+
+import org.eclipse.e4.core.contexts.ContextInjectionFactory;
+import org.eclipse.e4.core.contexts.EclipseContextFactory;
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.core.internal.tests.CoreTestsActivator;
+import org.eclipse.e4.core.services.translation.TranslationService;
+
+public class MessageRegistryTest extends TestCase {
+
+ static class TestObject {
+ @Inject
+ BundleMessagesRegistry registry;
+ }
+
+ class TestLocalizableObject {
+ private String localizableValue;
+
+ public String getLocalizableValue() {
+ return localizableValue;
+ }
+
+ public void setLocalizableValue(String localizableValue) {
+ this.localizableValue = localizableValue;
+ }
+ }
+
+ private IEclipseContext context;
+
+ @Override
+ public void setUp() {
+ this.context = EclipseContextFactory.getServiceContext(CoreTestsActivator.getDefault().getBundleContext());
+ ContextInjectionFactory.setDefault(context);
+ }
+
+ public void testRegisterLocalizationByProperty() {
+ // ensure the en Locale is set for this test
+ this.context.set(TranslationService.LOCALE, Locale.ENGLISH);
+ TestObject o = ContextInjectionFactory.make(TestObject.class, this.context);
+
+ TestLocalizableObject control = new TestLocalizableObject();
+ o.registry.registerProperty(control, "localizableValue", "message");
+
+ // test value is set
+ assertNotNull(control.getLocalizableValue());
+
+ // test the set value
+ assertEquals("BundleMessage", control.getLocalizableValue());
+ }
+
+ public void testRegisterLocalizationByMethod() {
+ // ensure the en Locale is set for this test
+ this.context.set(TranslationService.LOCALE, Locale.ENGLISH);
+ TestObject o = ContextInjectionFactory.make(TestObject.class, this.context);
+
+ TestLocalizableObject control = new TestLocalizableObject();
+ o.registry.register(control, "setLocalizableValue", "message");
+
+ // test value is set
+ assertNotNull(control.getLocalizableValue());
+
+ // test the set value
+ assertEquals("BundleMessage", control.getLocalizableValue());
+ }
+
+ public void testRegisterLocalizationByPropertyAndChangeLocale() {
+ // ensure the en Locale is set for this test
+ this.context.set(TranslationService.LOCALE, Locale.ENGLISH);
+ TestObject o = ContextInjectionFactory.make(TestObject.class, this.context);
+
+ TestLocalizableObject control = new TestLocalizableObject();
+ o.registry.registerProperty(control, "localizableValue", "message");
+
+ // test value is set
+ assertNotNull(control.getLocalizableValue());
+
+ // test the set value
+ assertEquals("BundleMessage", control.getLocalizableValue());
+
+ // change the locale to GERMAN
+ this.context.set(TranslationService.LOCALE, Locale.GERMAN);
+
+ assertEquals("BundleNachricht", control.getLocalizableValue());
+ }
+
+ public void testRegisterLocalizationByMethodAndChangeLocale() {
+ // ensure the en Locale is set for this test
+ this.context.set(TranslationService.LOCALE, Locale.ENGLISH);
+ TestObject o = ContextInjectionFactory.make(TestObject.class, this.context);
+
+ TestLocalizableObject control = new TestLocalizableObject();
+ o.registry.register(control, "setLocalizableValue", "message");
+
+ // test value is set
+ assertNotNull(control.getLocalizableValue());
+
+ // test the set value
+ assertEquals("BundleMessage", control.getLocalizableValue());
+
+ // change the locale to GERMAN
+ this.context.set(TranslationService.LOCALE, Locale.GERMAN);
+
+ assertEquals("BundleNachricht", control.getLocalizableValue());
+ }
+
+ // TODO add testcases for Java 8 method references
+}