/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG 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:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.internal.core.injector.extension;
import static org.eclipse.riena.internal.core.injector.extension.InterfaceBeanHandler.MethodKind.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.log.LogService;
import org.eclipse.core.runtime.ContributorFactoryOSGi;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.equinox.log.Logger;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.injector.extension.CreateLazy;
import org.eclipse.riena.core.injector.extension.DefaultValue;
import org.eclipse.riena.core.injector.extension.DoNotReplaceSymbols;
import org.eclipse.riena.core.injector.extension.DoNotWireExecutable;
import org.eclipse.riena.core.injector.extension.ExtensionInjector;
import org.eclipse.riena.core.injector.extension.ExtensionInterface;
import org.eclipse.riena.core.injector.extension.MapContent;
import org.eclipse.riena.core.injector.extension.MapName;
import org.eclipse.riena.core.util.Nop;
import org.eclipse.riena.core.util.StringUtils;
import org.eclipse.riena.core.wire.Wire;
import org.eclipse.riena.internal.core.Activator;
/**
* InvocationHandler for proxies that map to configuration elements.
*/
final class InterfaceBeanHandler implements InvocationHandler {
private final Class<?> interfaceType;
private final IConfigurationElement configurationElement;
private final boolean symbolReplace;
private final Map<Method, Result> resolved;
private final static Logger LOGGER = Log4r.getLogger(Activator.getDefault(), InterfaceBeanHandler.class);
InterfaceBeanHandler(final Class<?> interfaceType, final boolean symbolReplace,
final IConfigurationElement configurationElement) {
this.interfaceType = interfaceType;
this.configurationElement = configurationElement;
this.symbolReplace = symbolReplace && !interfaceType.isAnnotationPresent(DoNotReplaceSymbols.class);
this.resolved = new HashMap<Method, Result>();
if (!interfaceType.isAnnotationPresent(ExtensionInterface.class)) {
LOGGER.log(LogService.LOG_WARNING, "The interface '" + interfaceType.getName() //$NON-NLS-1$
+ "' is NOT annotated with @" + ExtensionInterface.class.getSimpleName() + " but it should!"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
final MethodKind methodKind = MethodKind.of(method);
synchronized (resolved) {
Result result = resolved.get(method);
if (result == null) {
result = invoke(method, args, methodKind);
if (result.cacheIt) {
resolved.put(method, result);
}
}
return result.object;
}
}
private Result invoke(final Method method, final Object[] args, final MethodKind methodKind) throws Throwable {
if (method.getParameterTypes().length == 0) {
if (method.getName().equals("toString")) { //$NON-NLS-1$
return Result.cache(proxiedToString());
} else if (method.getName().equals("hashCode")) { //$NON-NLS-1$
return Result.cache(proxiedHashCode());
}
}
if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == Object.class
&& method.getName().equals("equals")) { //$NON-NLS-1$
return Result.noCache(proxiedEquals(args[0]));
}
final Class<?> returnType = method.getReturnType();
if (returnType == Bundle.class) {
return Result.cache(ContributorFactoryOSGi.resolve(configurationElement.getContributor()));
}
if (returnType == IConfigurationElement.class) {
return Result.cache(configurationElement);
}
final String attributeName = getAttributeName(method, methodKind);
if (returnType == String.class) {
final boolean methodSymbolReplace = !method.isAnnotationPresent(DoNotReplaceSymbols.class);
return Result.noCache(modify(method.isAnnotationPresent(MapContent.class) ? configurationElement.getValue()
: getAttribute(attributeName, method), methodSymbolReplace));
}
if (returnType.isPrimitive()) {
final boolean methodSymbolReplace = !method.isAnnotationPresent(DoNotReplaceSymbols.class);
return Result.noCache(coerce(returnType, modify(getAttribute(attributeName, method), methodSymbolReplace)));
}
if (returnType == Class.class) {
String value = configurationElement.getAttribute(attributeName);
if (value == null) {
return Result.CACHED_NULL;
}
final Bundle bundle = ContributorFactoryOSGi.resolve(configurationElement.getContributor());
if (bundle == null) {
return Result.CACHED_NULL;
}
// does it contain initialization data?
final int colon = value.indexOf(':');
if (colon != -1) {
value = value.substring(0, colon);
}
return Result.cache(loadClass(bundle, value));
}
if (returnType.isEnum()) {
final String value = configurationElement.getAttribute(attributeName);
if (StringUtils.isEmpty(value)) {
return Result.CACHED_NULL;
}
for (final Object enumConstant : returnType.getEnumConstants()) {
if (enumConstant.toString().equalsIgnoreCase(value)) {
return Result.cache(enumConstant);
}
}
throw new IllegalStateException("Invalid enum value '" + value + "' for enum type '" + returnType.getName() //$NON-NLS-1$ //$NON-NLS-2$
+ "'" + within()); //$NON-NLS-1$
}
if (returnType.isInterface() && returnType.isAnnotationPresent(ExtensionInterface.class)) {
final IConfigurationElement[] cfgElements = configurationElement.getChildren(attributeName);
if (cfgElements.length == 0) {
return Result.CACHED_NULL;
}
if (cfgElements.length == 1) {
return Result.cache(Proxy.newProxyInstance(returnType.getClassLoader(), new Class[] { returnType },
new InterfaceBeanHandler(returnType, symbolReplace, cfgElements[0])));
}
throw new IllegalStateException(
"Got more than one configuration element but the interface expected exactly one, .i.e no array type has been specified for method '" //$NON-NLS-1$
+ method + "'" + within()); //$NON-NLS-1$
}
if (returnType.isArray() && returnType.getComponentType().isInterface()) {
final IConfigurationElement[] cfgElements = configurationElement.getChildren(attributeName);
final Object[] result = (Object[]) Array.newInstance(returnType.getComponentType(), cfgElements.length);
for (int i = 0; i < cfgElements.length; i++) {
result[i] = Proxy.newProxyInstance(returnType.getComponentType().getClassLoader(),
new Class[] { returnType.getComponentType() },
new InterfaceBeanHandler(returnType.getComponentType(), symbolReplace, cfgElements[i]));
}
return Result.cache(result);
}
if (returnType == Void.class || (args != null && args.length > 0)) {
throw new UnsupportedOperationException("Can not handle method '" + method + "'" + within());//$NON-NLS-1$ //$NON-NLS-2$
}
// Now try to create a fresh instance,i.e. createExecutableExtension()
if (configurationElement.getAttribute(attributeName) == null
&& configurationElement.getChildren(attributeName).length == 0) {
return Result.CACHED_NULL;
}
final boolean wire = !(method.isAnnotationPresent(DoNotWireExecutable.class) || Boolean
.getBoolean(ExtensionInjector.RIENA_EXTENSIONS_DONOTWIRE_SYSTEM_PROPERTY));
if (method.isAnnotationPresent(CreateLazy.class)) {
return Result.noCache(LazyExecutableExtension.newInstance(returnType, configurationElement, attributeName,
wire));
}
final Object result = configurationElement.createExecutableExtension(attributeName);
if (wire) {
// Try wiring the created executable extension
final Bundle bundle = ContributorFactoryOSGi.resolve(configurationElement.getContributor());
BundleContext context = null;
if (bundle != null) {
context = bundle.getBundleContext();
}
if (context == null) {
context = Activator.getDefault().getContext();
}
Wire.instance(result).andStart(context);
}
return Result.noCache(result);
}
private String within() {
return " with in interface '" + interfaceType.getName() + "' and bundle '" //$NON-NLS-1$ //$NON-NLS-2$
+ ContributorFactoryOSGi.resolve(configurationElement.getContributor()) + "'"; //$NON-NLS-1$
}
private Class<?> loadClass(final Bundle bundle, final String className) throws ClassNotFoundException {
try {
return bundle.loadClass(className);
} catch (final ClassNotFoundException e) {
// Are we a fragment bundle
final Bundle[] hosts = Platform.getHosts(bundle);
if (hosts == null) {
throw e;
}
for (final Bundle host : hosts) {
try {
return host.loadClass(className);
} catch (final ClassNotFoundException e2) {
Nop.reason("try next host"); //$NON-NLS-1$
}
}
throw e;
}
}
public boolean proxiedEquals(final Object obj) {
try {
final InvocationHandler handler = Proxy.getInvocationHandler(obj);
if (handler instanceof InterfaceBeanHandler) {
return configurationElement.equals(((InterfaceBeanHandler) handler).configurationElement);
}
} catch (final IllegalArgumentException e) {
return false;
}
return false;
}
public int proxiedHashCode() {
return configurationElement.hashCode();
}
public String proxiedToString() {
final StringBuilder bob = new StringBuilder("Dynamic proxy for "); //$NON-NLS-1$
bob.append(interfaceType.getName()).append(':');
final String[] names = configurationElement.getAttributeNames();
for (final String name : names) {
bob.append(name).append('=').append(configurationElement.getAttribute(name)).append(',');
}
bob.setLength(bob.length() - 1);
return bob.toString();
}
private String getAttributeName(final Method method, final MethodKind methodKind) {
final Annotation annotation = method.getAnnotation(MapName.class);
if (annotation != null) {
return ((MapName) annotation).value();
}
// No annotations
if (methodKind == OTHER) {
return null;
}
final String name = method.getName().substring(methodKind.prefix.length());
return name.substring(0, 1).toLowerCase() + name.substring(1);
}
private String getAttribute(final String attributeName, final Method method) {
final String value = configurationElement.getAttribute(attributeName);
if (value != null) {
return value;
}
final Annotation annotation = method.getAnnotation(DefaultValue.class);
if (annotation != null) {
return ((DefaultValue) annotation).value();
}
return null;
}
private Object coerce(final Class<?> toType, final String value) {
if (toType == Long.TYPE) {
return Long.valueOf(value);
}
if (toType == Integer.TYPE) {
return Integer.valueOf(value);
}
if (toType == Boolean.TYPE) {
return Boolean.valueOf(value);
}
if (toType == Float.TYPE) {
return Float.valueOf(value);
}
if (toType == Double.TYPE) {
return Double.valueOf(value);
}
if (toType == Short.TYPE) {
return Short.valueOf(value);
}
if (toType == Character.TYPE) {
return Character.valueOf(value.charAt(0));
}
if (toType == Byte.TYPE) {
return Byte.valueOf(value);
}
return value;
}
private String modify(final String value, final boolean methodSymbolReplace) {
if (!symbolReplace || !methodSymbolReplace || value == null) {
return value;
}
final IStringVariableManager variableManager = VariablesPlugin.getDefault().getStringVariableManager();
if (variableManager == null) {
return value;
}
try {
return variableManager.performStringSubstitution(value);
} catch (final CoreException e) {
LOGGER.log(LogService.LOG_ERROR, "Could not perfrom string substitution for '" + value + "' .", e); //$NON-NLS-1$ //$NON-NLS-2$
return value;
}
}
private final static class Result {
private final Object object;
private final boolean cacheIt;
private static final Result CACHED_NULL = Result.cache(null);
private static Result noCache(final Object object) {
return new Result(object, false);
}
private static Result cache(final Object object) {
return new Result(object, true);
}
private Result(final Object object, final boolean cacheIt) {
this.object = object;
this.cacheIt = cacheIt;
}
}
enum MethodKind {
GET("get"), IS("is"), CREATE("create"), OTHER; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
private final String prefix;
private MethodKind(final String kind) {
this.prefix = kind;
}
private MethodKind() {
this.prefix = null;
}
private static MethodKind of(final Method method) {
final String name = method.getName();
if (name.startsWith(GET.prefix)) {
return GET;
} else if (name.startsWith(IS.prefix)) {
return IS;
} else if (name.startsWith(CREATE.prefix)) {
return CREATE;
}
return OTHER;
}
@Override
public String toString() {
return prefix;
}
}
}