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>
+ * &#064;Creatable
+ * public class ExampleMessageRegistry
+ * 		extends
+ * 			BaseMessageRegistry&lt;ExampleMessages&gt; {
+ *
+ * 	&#064;Override
+ * 	&#064;Inject
+ * 	public void updateMessages(@Translation ExampleMessages messages) {
+ * 		super.updateMessages(messages);
+ * 	}
+ * }
+ * </pre>
+ *
+ * <p>
+ * Note that the registry instance is annotated with &#064;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>
+	 * &#064;Inject
+	 * ExampleMessageRegistry registry;
+	 *
+	 * Label myFirstLabel = new Label(parent, SWT.WRAP);
+	 * registry.register(myFirstLabel::setText, (m) -&gt; 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 &#064;Inject and &#064;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
+}