blob: b772770092b14e9cf651aeabf4b839beb1355cec [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2018 BestSolution.at and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - modifications to instance creation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 460308
******************************************************************************/
package org.eclipse.e4.core.internal.services;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import javax.annotation.PostConstruct;
import org.eclipse.e4.core.services.nls.IMessageFactoryService;
import org.eclipse.e4.core.services.nls.Message;
import org.eclipse.e4.core.services.nls.Message.ReferenceType;
import org.eclipse.e4.core.services.translation.ResourceBundleProvider;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.log.Logger;
import org.osgi.service.log.LoggerFactory;
@Component
public class MessageFactoryServiceImpl implements IMessageFactoryService {
private LoggerFactory factory;
private Logger logger;
// Cache so when multiple instance use the same message class
private Map<Object, Reference<Object>> SOFT_CACHE = Collections
.synchronizedMap(new HashMap<>());
private Map<Object, Reference<Object>> WEAK_CACHE = Collections
.synchronizedMap(new HashMap<>());
private int CLEANUPCOUNT = 0;
@Override
public <M> M getMessageInstance(final Locale locale, final Class<M> messages,
final ResourceBundleProvider provider) {
String key = messages.getName() + "_" + locale; //$NON-NLS-1$
final Message annotation = messages.getAnnotation(Message.class);
Map<Object, Reference<Object>> cache = null;
ReferenceType type = ReferenceType.NONE;
if (++CLEANUPCOUNT > 1000) {
Iterator<Entry<Object, Reference<Object>>> it = WEAK_CACHE.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue().get() == null) {
it.remove();
}
}
it = SOFT_CACHE.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue().get() == null) {
it.remove();
}
}
CLEANUPCOUNT = 0;
}
if (annotation == null || annotation.referenceType() == ReferenceType.SOFT) {
cache = SOFT_CACHE;
type = ReferenceType.SOFT;
} else if (annotation.referenceType() == ReferenceType.WEAK) {
cache = WEAK_CACHE;
type = ReferenceType.WEAK;
}
if (cache != null && cache.containsKey(key)) {
@SuppressWarnings("unchecked")
Reference<M> ref = (Reference<M>) cache.get(key);
M o = ref.get();
if (o != null) {
return o;
}
cache.remove(key);
}
M instance;
if (System.getSecurityManager() == null) {
instance = createInstance(locale, messages, annotation, provider);
} else {
instance = AccessController.doPrivileged((PrivilegedAction<M>) () -> createInstance(locale, messages, annotation, provider));
}
if (cache != null) {
if (type == ReferenceType.SOFT) {
cache.put(key, new SoftReference<>(instance));
} else if (type == ReferenceType.WEAK) {
cache.put(key, new WeakReference<>(instance));
}
}
return instance;
}
/**
* 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:
* <ol>
* <li>URI location<br>
* If the message class is annotated with <code>@Message</code> and the <i>contributorURI</i>
* attribute is set, the {@link ResourceBundle} is searched at the specified location</li>
* <li>Relative location<br>
* If the message class is not annotated with <code>@Message</code> 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.</li>
* <li>Bundle localization<br>
* 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.</li>
* </ol>
* 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 <code>!key!</code> 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 rbProvider
* 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} or
* <code>null</code> if an error occured on creating the instance.
*/
@SuppressWarnings("deprecation")
private <M> M createInstance(Locale locale, Class<M> messages, Message annotation,
ResourceBundleProvider rbProvider) {
ResourceBundle resourceBundle = null;
if (annotation != null) {
if (!annotation.contributionURI().isEmpty()) {
resourceBundle = ResourceBundleHelper.getResourceBundleForUri(
annotation.contributionURI(), locale, rbProvider);
} else if (!annotation.contributorURI().isEmpty()) {
Logger log = this.logger;
if (log != null) {
log.warn(
"Usage of @Message#contributorURI detected! Please use @Message#contributionURI instead!"); //$NON-NLS-1$
}
resourceBundle = ResourceBundleHelper.getResourceBundleForUri(
annotation.contributorURI(), locale, rbProvider);
}
}
if (resourceBundle == null) {
// check for the resource bundle relative to the messages class
String baseName = messages.getName().replace('.', '/');
resourceBundle = ResourceBundleHelper.getEquinoxResourceBundle(baseName, locale,
messages.getClassLoader());
if (resourceBundle == null) {
// check for the resource bundle relative to the messages class by searching
// the properties file lower case
// this is a fix for Linux environments
resourceBundle = ResourceBundleHelper.getEquinoxResourceBundle(
baseName.toLowerCase(), locale, messages.getClassLoader());
}
}
if (resourceBundle == null) {
// retrieve the OSGi resource bundle
Bundle bundle = FrameworkUtil.getBundle(messages);
resourceBundle = rbProvider.getResourceBundle(bundle, locale.toString());
}
// always create a provider, if there is no resource bundle found, simply the modified keys
// will be returned by this provider to show that there is something
// wrong on loading it
ResourceBundleTranslationProvider provider = new ResourceBundleTranslationProvider(
resourceBundle);
M instance = null;
try {
instance = messages.newInstance();
Field[] fields = messages.getDeclaredFields();
for (Field field : fields) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
if (field.getType().isAssignableFrom(String.class)) {
field.set(instance, provider.translate(field.getName()));
}
}
} catch (InstantiationException e) {
Logger log = this.logger;
if (log != null) {
log.error("Instantiation of messages class failed", e); //$NON-NLS-1$
}
} catch (IllegalAccessException e) {
Logger log = this.logger;
if (log != null) {
log.error("Failed to access messages class", e); //$NON-NLS-1$
}
}
// invoke the method annotated with @PostConstruct
processPostConstruct(instance, messages);
return instance;
}
/**
* Searches for the method annotated {@link PostConstruct} in the messages class. If there is
* one found it will be executed.
* <p>
* Note: The method annotated with {@link PostConstruct} does not support method injection
* because we are not using the injection mechanism to call.
*
* @param messageObject
* The message instance of the given class where the method annotated with
* {@link PostConstruct} should be called
* @param messageClass
* The type of the message class whose instance is requested.
*/
private void processPostConstruct(Object messageObject, Class<?> messageClass) {
if (messageObject != null) {
Method[] methods = messageClass.getDeclaredMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(PostConstruct.class)) {
continue;
} else {
try {
method.invoke(messageObject);
} catch (Exception e) {
Logger log = this.logger;
if (log != null) {
log.error(
"Exception on trying to execute the @PostConstruct annotated method in {}", //$NON-NLS-1$
messageClass, e);
}
}
}
}
}
}
@org.osgi.service.component.annotations.Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setLogger(LoggerFactory factory) {
this.factory = factory;
this.logger = factory.getLogger(getClass());
}
void unsetLogger(LoggerFactory loggerFactory) {
if (this.factory == loggerFactory) {
// if the factory we referenced locally is unset, we set the logger reference to
// null
this.factory = null;
this.logger = null;
}
}
}