package org.springframework.roo.startlevel;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.io.IOUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.startlevel.StartLevel;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* Changes the OSGi framework to start level 99 once all the services marked
* with "immediate" are activated.
* <p>
* The OSGi Declarative Service Specification ensures services are only loaded
* when required. While the "immediate" attribute ensures they are loaded
* eagerly, it is impossible to receive a callback when all "immediate" services
* have finished loading. This {@link BundleActivator} resolves this issue by
* discovering all enabled service components with an "immediate" flag and
* monitoring their startup. Once all are started, the OSGi framework is set to
* start level 99, which enables other classes that depend on the activation of
* such services to react to the start level change.
* <p>
* Note that this functionality is only provided for services (simple components
* are insufficient). Services must be defined in the XML file indicated by the
* "Service-Component" manifest header.
*
* @author Ben Alex
*/
public class Activator implements BundleActivator {
/** key: required class, any one of its services interfaces */
private final SortedMap<String, String> requiredImplementations = new TreeMap<String, String>();
private final SortedSet<String> runningImplementations = new TreeSet<String>();
private StartLevel startLevel;
private ServiceReference startLevelServiceReference;
private String getClassName(final ServiceReference sr, final BundleContext context) {
if (sr == null) {
return null;
}
if (sr.getProperty("component.name") != null) {
// Roo's convention is the component name should be the fully
// qualified class name.
// Roo's other convention is bundle symbolic names should be fully
// qualified package names.
// However, the user can change the BSN or component name, so we
// need to do a quick sanity check.
final String componentName = sr.getProperty("component.name").toString();
if (componentName.startsWith(sr.getBundle().getSymbolicName())) {
// The type name appears under the BSN package, so they probably
// haven't changed our convention
return componentName;
}
}
// To get here we couldn't rely on component name. The following is far
// less reliable given the
// service may be unavailable by the time we try to do a getService(sr)
// invocation (ROO-1156).
final Object obj = context.getService(sr);
if (obj == null) {
return null;
}
return obj.getClass().getName();
}
private void potentiallyChangeStartLevel() {
if (requiredImplementations.keySet().equals(runningImplementations)) {
if (System.getProperty("roo.pause") != null) {
System.out.println("roo.pause detected; press any key to proceed");
try {
System.in.read();
} catch (final IOException ignored) {
}
}
startLevel.setStartLevel(99);
}
}
public void process(final URL url) {
Document document;
InputStream is = null;
try {
is = url.openStream();
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
} catch (final Exception ex) {
throw new IllegalStateException("Could not open " + url, ex);
} finally {
IOUtils.closeQuietly(is);
}
final Element rootElement = (Element) document.getFirstChild();
final NodeList components = rootElement.getElementsByTagName("scr:component");
if (components == null || components.getLength() == 0) {
return;
}
for (int i = 0; i < components.getLength(); i++) {
final Element component = (Element) components.item(i);
// Is this component enabled?
if (!component.hasAttribute("enabled") || !"true".equals(component.getAttribute("enabled"))) {
// Disabled, so skip it
continue;
}
// Is this an immediate starter?
if (!component.hasAttribute("immediate")
|| !"true".equals(component.getAttribute("immediate"))) {
// Not an immediate starter, so skip it
continue;
}
// Calculate implementing class name correctly
String componentName = null;
final NodeList implementation = component.getElementsByTagName("implementation");
if (implementation != null && implementation.getLength() == 1) {
final Element impl = (Element) implementation.item(0);
if (impl.hasAttribute("class")) {
componentName = impl.getAttribute("class");
}
}
// Get its first implementing service
String serviceInterface = null;
final NodeList service = component.getElementsByTagName("service");
if (service != null && service.getLength() == 1) {
final Element s = (Element) service.item(0);
final NodeList provide = s.getElementsByTagName("provide");
if (provide != null && provide.getLength() > 0) {
final Element firstProvide = (Element) provide.item(0);
if (firstProvide.hasAttribute("interface")) {
serviceInterface = firstProvide.getAttribute("interface");
}
}
}
if (componentName != null && serviceInterface != null) {
requiredImplementations.put(componentName, serviceInterface);
}
}
}
public void start(final BundleContext context) throws Exception {
startLevelServiceReference = context.getServiceReference(StartLevel.class.getName());
startLevel = (StartLevel) context.getService(startLevelServiceReference);
for (final Bundle bundle : context.getBundles()) {
final String value = bundle.getHeaders().get("Service-Component");
if (value != null) {
List<String> componentDescriptions = Arrays.asList(value.split("\\s*,\\s*"));
for (String desc : componentDescriptions) {
final URL url = bundle.getResource(desc);
process(url);
}
}
}
// Ensure I'm notified of other services changes
final BundleContext myContext = context;
context.addServiceListener(new ServiceListener() {
public void serviceChanged(final ServiceEvent event) {
final ServiceReference sr = event.getServiceReference();
final String className = getClassName(sr, myContext);
if (sr != null) {
myContext.ungetService(sr);
}
if (className == null) {
// Something went wrong
return;
}
if (event.getType() == ServiceEvent.REGISTERED) {
if (requiredImplementations.keySet().contains(className)) {
runningImplementations.add(className);
potentiallyChangeStartLevel();
}
} else if (event.getType() == ServiceEvent.UNREGISTERING) {
if (runningImplementations.contains(className)) {
runningImplementations.remove(className);
potentiallyChangeStartLevel();
}
}
}
});
// Now identify if any services I was interested in are already running
for (final String requiredService : requiredImplementations.keySet()) {
final String correspondingInterface = requiredImplementations.get(requiredService);
final ServiceReference[] srs = context.getServiceReferences(correspondingInterface, null);
if (srs != null) {
for (final ServiceReference sr : srs) {
final String className = getClassName(sr, context);
if (className == null) {
// Something went wrong
continue;
}
if (requiredImplementations.keySet().contains(className)) {
runningImplementations.add(className);
}
}
}
}
// Potentially change the start level, now that we've added all the
// known started services
potentiallyChangeStartLevel();
}
public void stop(final BundleContext context) throws Exception {
context.ungetService(startLevelServiceReference);
}
}