/*******************************************************************************
* 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.core.wire;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.SynchronousBundleListener;
import org.osgi.service.log.LogService;
import org.eclipse.core.runtime.Assert;
import org.eclipse.equinox.log.Logger;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.injector.IStoppable;
import org.eclipse.riena.core.injector.InjectionFailure;
import org.eclipse.riena.core.injector.extension.ExtensionInjector;
import org.eclipse.riena.core.injector.service.ServiceInjector;
import org.eclipse.riena.core.util.Iter;
import org.eclipse.riena.core.util.WeakRef;
import org.eclipse.riena.internal.core.ignore.IgnoreFindBugs;
import org.eclipse.riena.internal.core.wire.ExtensionInjectorBuilder;
import org.eclipse.riena.internal.core.wire.ServiceInjectorBuilder;
/**
* The {@code WirePuller} is responsible for the wiring of a bean.
*/
public class WirePuller implements IStoppable {
private final WeakRef<?> beanRef;
private BundleContext context;
private List<IWiring> wirings;
private List<IStoppable> injections;
private State state = State.PENDING;
private BundleListener bundleStoppingListener;
private static final Comparator<MAOTupel> ORDER_COMPARATOR = new OrderComparator();
// Only for unit testing of classes using accessors
private static Map<Class<?>, Class<? extends IWiring>> wiringMocks;
private static final Logger LOGGER = Log4r.getLogger(WirePuller.class);
/**
* Create a 'wire puller' for the given bean.
*
* @param bean
*/
WirePuller(final Object bean) {
Assert.isLegal(bean != null, "bean must exist"); //$NON-NLS-1$
this.beanRef = new WeakRef<Object>(bean, new Runnable() {
public void run() {
stop();
}
});
}
/**
* Start the wiring.<br>
* This method tries to retrieve the required {@code BundleContext} from the
* {@code bean} that should be wired. This requires that the OSGi runtime is
* up - which might not the case in unit tests. However, in such a case no
* wiring will be done and this method returns {@code null}.
*
* @return the {@code WirePuller} or null if wiring was not possible
* @since 3.0
*/
public synchronized WirePuller andStart() {
final Class<?> beanClass = getBeanClass();
if (beanClass == null) {
LOGGER.log(LogService.LOG_WARNING, "Could not wire bean because it is aleady garbage collected."); //$NON-NLS-1$
return null;
}
final Bundle bundle = FrameworkUtil.getBundle(beanClass);
if (bundle != null) {
return andStart(bundle.getBundleContext());
}
LOGGER.log(LogService.LOG_WARNING, "Could not wire bean '" + beanClass //$NON-NLS-1$
+ "' because there is no bundle context."); //$NON-NLS-1$
return null;
}
/**
* Start the wiring.
*
* @param context
* the bundle context
* @return the {@code WirePuller}
*/
public synchronized WirePuller andStart(final BundleContext context) {
Assert.isLegal(context != null, "context must be given."); //$NON-NLS-1$
Assert.isLegal(state == State.PENDING, "state must be pending."); //$NON-NLS-1$
this.context = context;
if (!wire(getBean(), getBeanClass())) {
return this;
}
state = State.STARTED;
bundleStoppingListener = new BundleStoppingListener();
context.addBundleListener(bundleStoppingListener);
return this;
}
/**
* Stop the wiring.
*/
public synchronized void stop() {
if (state != State.STARTED) {
return;
}
context.removeBundleListener(bundleStoppingListener);
final Object bean = getBean();
if (bean != null && wirings != null) {
for (final IWiring wiring : wirings) {
wiring.unwire(bean, context);
}
}
for (final IStoppable stoppable : Iter.ableReverse(injections)) {
stoppable.stop();
}
state = State.STOPPED;
}
private boolean wire(final Object bean, final Class<?> beanClass) {
if (beanClass == null || bean == null || beanClass == Object.class) {
return false;
}
final IWiring wiring = getWiring(getWiringClass(beanClass));
add(wiring);
boolean hasWirings = wire(bean, beanClass.getSuperclass());
if (wiring != null) {
wiring.wire(bean, context);
hasWirings = true;
}
return injectIntoAnnotatedMethods(bean, beanClass) | hasWirings;
}
private boolean injectIntoAnnotatedMethods(final Object bean, final Class<?> beanClass) {
boolean hasWirings = false;
for (final MAOTupel maot : sortMethodsByInjectionOrder(beanClass.getDeclaredMethods())) {
if (maot.annotation == InjectService.class) {
verifyOrder(beanClass, maot);
injectServiceInto(bean, maot.method);
hasWirings = true;
} else if (maot.annotation == InjectExtension.class) {
verifyOrder(beanClass, maot);
injectExtensionInto(bean, maot.method);
hasWirings = true;
} else if (maot.annotation == OnWiringDone.class) {
notifyBean(bean, maot.method);
}
}
return hasWirings;
}
private void verifyOrder(final Class<?> beanClass, final MAOTupel maot) {
if (maot.getOrder() == Integer.MAX_VALUE) {
throw new InjectionFailure("Annotation '" + maot.annotation + "' on '" + beanClass.getName() + "." //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ maot.method.getName() + "' has forbidden order Integer.MAX_VALUE"); //$NON-NLS-1$
}
}
private List<MAOTupel> sortMethodsByInjectionOrder(final Method[] methods) {
final List<MAOTupel> maots = new ArrayList<MAOTupel>();
for (final Method method : methods) {
final InjectService injectServiceAnnotation = method.getAnnotation(InjectService.class);
if (injectServiceAnnotation != null) {
maots.add(new MAOTupel(method, InjectService.class, injectServiceAnnotation.order()));
}
final InjectExtension injectExtensionAnnotation = method.getAnnotation(InjectExtension.class);
if (injectExtensionAnnotation != null) {
maots.add(new MAOTupel(method, InjectExtension.class, injectExtensionAnnotation.order()));
}
final OnWiringDone wiringDoneAnnotation = method.getAnnotation(OnWiringDone.class);
if (wiringDoneAnnotation != null) {
maots.add(new MAOTupel(method, OnWiringDone.class, Integer.MAX_VALUE));
}
}
Collections.sort(maots, ORDER_COMPARATOR);
return maots;
}
private void injectServiceInto(final Object bean, final Method method) {
final ServiceInjectorBuilder bob = new ServiceInjectorBuilder(bean, method);
final ServiceInjector injector = bob.build();
add(injector.andStart(context));
}
private void injectExtensionInto(final Object bean, final Method method) {
final ExtensionInjectorBuilder bob = new ExtensionInjectorBuilder(bean, method);
final ExtensionInjector injector = bob.build();
add(injector.andStart(context));
}
private void notifyBean(final Object bean, final Method method) {
try {
method.invoke(bean);
} catch (final Exception e) {
throw new InjectionFailure("Invoking the @WiringDone method '" + method + "' on bean class '" //$NON-NLS-1$ //$NON-NLS-2$
+ bean.getClass().getName() + "'.", e); //$NON-NLS-1$
}
}
/**
* Collect the wirings.
*
* @param wiring
*/
private void add(final IWiring wiring) {
if (wiring == null) {
return;
}
if (wirings == null) {
wirings = new ArrayList<IWiring>(2);
}
wirings.add(wiring);
}
/**
* Collect the stoppable injections.
*
* @param stoppable
*/
private void add(final IStoppable stoppable) {
if (injections == null) {
injections = new ArrayList<IStoppable>();
}
injections.add(stoppable);
}
private IWiring getWiring(final Class<? extends IWiring> wiringClass) {
if (wiringClass == null) {
return null;
}
try {
return wiringClass.newInstance();
} catch (final InstantiationException e) {
throw new IllegalStateException("Could not create instance of wiring " + wiringClass, e); //$NON-NLS-1$
} catch (final IllegalAccessException e) {
throw new IllegalStateException("Could not create instance of wiring " + wiringClass, e); //$NON-NLS-1$
}
}
private Class<? extends IWiring> getWiringClass(final Class<?> beanClass) {
// If mocks are defined, we use them instead of the regular mechanism.
if (wiringMocks != null) {
return wiringMocks.get(beanClass);
}
final WireWith wiringAnnotation = beanClass.getAnnotation(WireWith.class);
// Does the bean have a wiring annotation? Yes, use this class for wiring.
if (wiringAnnotation != null) {
return wiringAnnotation.value();
}
return null;
}
/**
* Stops this wire-puller if the owning bundle is stopping.
*/
private class BundleStoppingListener implements SynchronousBundleListener {
@IgnoreFindBugs(value = "IS2_INCONSISTENT_SYNC", justification = "Field context of WirePuller is written only before this listener is added, field bundle of BundleContext is immutable.")
public void bundleChanged(final BundleEvent event) {
if (event.getBundle() != context.getBundle()) {
return;
}
if (event.getType() == BundleEvent.STOPPING) {
stop();
}
}
}
// Deref bean
private Object getBean() {
return beanRef.get();
}
// Deref bean class
private Class<?> getBeanClass() {
final Object bean = getBean();
return bean == null ? null : bean.getClass();
}
/**
* For testing purposes it is sometimes necessary to change the default
* behavior for retrieving the wiring classes. With this method it is
* possible to define a map that tells the {@codeWirePuller} how to find the
* wiring classes.
*
* @param wiringMocks
*/
public static void injectWiringMocks(final Map<Class<?>, Class<? extends IWiring>> wiringMocks) {
WirePuller.wiringMocks = wiringMocks;
}
private enum State {
STARTED, STOPPED, PENDING
}
private static class MAOTupel {
private final Method method;
private final Class<?> annotation;
private final int order;
public MAOTupel(final Method method, final Class<?> annotation, final int order) {
this.method = method;
this.annotation = annotation;
this.order = order;
}
public int getOrder() {
return order;
}
}
/**
* Compares MAOTuples by order.
*/
private static final class OrderComparator implements Comparator<MAOTupel> {
public int compare(final MAOTupel tupel1, final MAOTupel tupel2) {
return tupel1.getOrder() == tupel2.getOrder() ? 0 : tupel1.getOrder() < tupel2.getOrder() ? -1 : 1;
}
}
}