package com.rcpcompany.test.utils;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.runtime.Platform;
import org.eclipse.osgi.util.ManifestElement;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import com.rcpcompany.test.utils.xml.XMLDiff;
import com.rcpcompany.utils.logging.LogUtils;
/**
* Various utilities to ease testing of OSGi Declarative Services.
*
* @author Tonny Madsen, tonny.madsen@gmail.com
*/
public class DSTestUtils {
/**
* Tests that the Declarative Services are correctly configured
*
* @param bundleName
* the name of the bundle
*/
public static void checkDSFiles(String bundleName) {
final Bundle b = Platform.getBundle(bundleName);
assertNotNull("bundle '" + bundleName + "' not found", b);
final Context cxt = new Context(b);
parseDSClasses(cxt);
try {
final Enumeration<URL> dsFileEnumerator = cxt.bundle.findEntries("/OSGI-INF", "*.xml", true);
if (dsFileEnumerator != null) {
while (dsFileEnumerator.hasMoreElements()) {
final URL url = dsFileEnumerator.nextElement();
final String path = url.getPath();
if (path.startsWith(".")) {
continue;
}
if (!path.endsWith(".xml")) {
continue;
}
cxt.osgiinfDSFiles.add(path);
parseDSFile(cxt, url);
}
}
final Dictionary<String, String> headers = cxt.bundle.getHeaders();
assertNotNull(headers);
final String scHeader = headers.get(ComponentConstants.SERVICE_COMPONENT);
if (scHeader != null) {
try {
final ManifestElement[] elements = ManifestElement.parseHeader(
ComponentConstants.SERVICE_COMPONENT, scHeader);
for (final ManifestElement e : elements) {
final String v = e.getValue();
if (v.startsWith("../")) {
cxt.addProblem("Element of " + ComponentConstants.SERVICE_COMPONENT + " includes '../': '"
+ v + "'");
continue;
}
if (v.contains(" ") || v.contains("\t")) {
cxt.addProblem("Element of " + ComponentConstants.SERVICE_COMPONENT
+ " includes white space (' ' or tab) - missing comma?: '" + v + "'");
continue;
}
cxt.manifestDSFiles.add(v);
}
} catch (final BundleException ex) {
fail("" + ex);
return;
}
}
if (!cxt.manifestDSFiles.isEmpty()) {
final String activationPolicy = headers.get("Bundle-ActivationPolicy");
if (activationPolicy == null || !activationPolicy.equals("lazy")) {
cxt.addProblem("'Bundle-ActivationPolicy: lazy' required");
}
}
/*
* Go through the file specs of the ComponentConstants.SERVICE_COMPONENT header. And remove all matching
* files, one by one.
*/
for (final String ds : cxt.manifestDSFiles) {
final int ind = ds.lastIndexOf('/');
final String folder = ind != -1 ? ds.substring(0, ind) : "/"; //$NON-NLS-1$
final String filePattern = ind != -1 ? ds.substring(ind + 1) : ds;
final Enumeration<URL> urls = cxt.bundle.findEntries(folder, filePattern, false);
if (urls == null || !urls.hasMoreElements()) {
cxt.addProblem("DS string '" + ds + "' does not match any files");
continue;
}
while (urls.hasMoreElements()) {
final String path = urls.nextElement().getPath();
if (!cxt.osgiinfDSFiles.remove(path)) {
cxt.addProblem("entry not found in resources: " + path);
}
}
}
for (final String file : cxt.osgiinfDSFiles) {
cxt.addProblem("resource with no pattern: " + file);
}
} catch (final Exception ex) {
cxt.problems.append(ex);
}
for (final String c : cxt.dsClasses) {
cxt.addProblem(c + " has @Component but it is not found in any DS files");
}
if (cxt.problems.length() > 0) {
fail("Problems with DS files in bundle " + bundleName + ":" + cxt.problems);
}
}
public static void parseDSClasses(final Context cxt) {
try {
final Enumeration<URL> classFileEnumerator = cxt.bundle.findEntries("/", "*.class", true);
while (classFileEnumerator.hasMoreElements()) {
final URL u = classFileEnumerator.nextElement();
String e = u.getPath();
if (!e.endsWith(".class")) {
continue;
}
e = e.substring(1);
e = e.replaceAll(".class$", "");
e = e.replace("target/classes/", "");
e = e.replace("/", ".");
final String className = e;
final InputStream classStream = u.openStream();
try {
final ClassReader classReader = new ClassReader(classStream);
classReader.accept(new ClassVisitor(Opcodes.ASM4) {
@Override
public AnnotationVisitor visitAnnotation(final String annotationDesc, boolean visible) {
if (annotationDesc.equals("Lorg/osgi/service/component/annotations/Component;")) {
cxt.dsClasses.add(className);
LogUtils.debug(u, "annotation: " + className + " - " + annotationDesc);
return new AnnotationVisitor(Opcodes.ASM4) {
@Override
public void visit(String propertyName, Object propertyValue) {
LogUtils.debug(u, " property: " + propertyName + "=" + propertyValue);
}
};
}
return null;
}
@Override
public MethodVisitor visitMethod(int access, final String methodName, String desc,
String signature, String[] exceptions) {
return new MethodVisitor(Opcodes.ASM4) {
@Override
public AnnotationVisitor visitAnnotation(final String annotationDesc, boolean visible) {
if (annotationDesc.equals("Lorg/osgi/service/component/annotations/Reference;")) {
cxt.dsMethods.add(className + " - " + methodName);
if (!cxt.dsClasses.contains(className)) {
cxt.addProblem(className + "." + methodName
+ " has @Reference, but class does not have @Component");
}
LogUtils.debug(u, "annotation: " + className + "." + methodName + " - "
+ annotationDesc);
return new AnnotationVisitor(Opcodes.ASM4) {
@Override
public void visit(String propertyName, Object propertyValue) {
LogUtils.debug(u, " property: " + propertyName + "=" + propertyValue);
super.visit(propertyName, propertyValue);
}
};
}
if (annotationDesc.equals("Lorg/osgi/service/component/annotations/Activate;")) {
cxt.dsActivates.add(className + " - " + methodName);
if (!cxt.dsClasses.contains(className)) {
cxt.addProblem(className + "." + methodName
+ " has @Activate, but class does not have @Component");
}
LogUtils.debug(u, "annotation: " + className + "." + methodName + " - "
+ annotationDesc);
return new AnnotationVisitor(Opcodes.ASM4) {
@Override
public void visit(String propertyName, Object propertyValue) {
LogUtils.debug(u, " property: " + propertyName + "=" + propertyValue);
super.visit(propertyName, propertyValue);
}
};
}
if (annotationDesc.equals("Lorg/osgi/service/component/annotations/Deactivate;")) {
cxt.dsDeactivates.add(className + " - " + methodName);
if (!cxt.dsClasses.contains(className)) {
cxt.addProblem(className + "." + methodName
+ " has @Deactivate, but class does not have @Component");
}
LogUtils.debug(u, "annotation: " + className + "." + methodName + " - "
+ annotationDesc);
return new AnnotationVisitor(Opcodes.ASM4) {
@Override
public void visit(String propertyName, Object propertyValue) {
LogUtils.debug(u, " property: " + propertyName + "=" + propertyValue);
super.visit(propertyName, propertyValue);
}
};
}
return null;
}
};
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
} finally {
IOUtils.closeQuietly(classStream);
}
}
} catch (final Exception ex) {
cxt.addProblem("" + ex);
}
}
/**
* Parses the specified URL as an DS file and reports any cxt.problems.
*
* @param cxt
* the context
* @param url
* the URL for the DS filer (possibly in a fragment)
*/
private static void parseDSFile(Context cxt, URL url) {
final String path = url.getPath();
InputStream is = null;
Node node = null;
try {
is = url.openStream();
node = XMLDiff.toNode(is);
} catch (final Exception ex) {
cxt.addProblem(path + ": Found, but cannot open or parse: " + ex);
return;
} finally {
IOUtils.closeQuietly(is);
}
/*
* Validate the XML
*/
// final String p = XMLTestUtils.validate(url, "http://www.osgi.org/xmlns/scr/v1.1.0/scr.xsd");
// if (p != null) {
// cxt.addProblem(path + ": File is not valid: " + p);
// }
String implementationName = null;
final List<String> interfaces = new ArrayList<String>();
/*
* TODO activate and deactivate
*/
/*
* Look inside component node
*/
final Node children = node.getFirstChild();
/*
* Find the implementation node and parse this
*/
for (Node n = children.getFirstChild(); n != null; n = n.getNextSibling()) {
final String nodeName1 = n.getNodeName();
if (nodeName1.equals("implementation")) {
if (implementationName != null) {
cxt.addProblem(path + ": implementation class specified multiple times");
continue;
}
final NamedNodeMap attributes = n.getAttributes();
final Node implementationNameNode = attributes.getNamedItem("class");
if (implementationNameNode == null) {
cxt.addProblem(path + ": no implementation class specified");
continue;
}
implementationName = implementationNameNode.getNodeValue();
if (cxt.dsClasses.remove(implementationName)) {
continue;
}
final URL classRes = cxt.bundle.getResource(implementationName.replace('.', '/') + ".class");
if (classRes == null) {
cxt.problems
.append("\n " + path + ": implementation class '" + implementationName + "' not found");
} else {
cxt.addProblem(path + ": implementation class '" + implementationName
+ "' found, but misses @Component");
}
continue;
}
}
if (implementationName == null) {
cxt.addProblem(path + ": No implementation class found");
return;
}
/*
* Parse all other nodes
*/
for (Node n = children.getFirstChild(); n != null; n = n.getNextSibling()) {
final String nodeName2 = n.getNodeName();
if (nodeName2.equals("implementation")) {
continue;
}
if (nodeName2.equals("service")) {
for (Node sn = node.getFirstChild(); sn != null; sn = sn.getNextSibling()) {
if (sn.getNodeName().equals("provide")) {
final NamedNodeMap attributes = sn.getAttributes();
final Node interfaceNameNode = attributes.getNamedItem("interface");
if (interfaceNameNode == null) {
cxt.addProblem(path + ": no interface specified");
continue;
}
interfaces.add(interfaceNameNode.getNodeValue());
}
}
continue;
}
if (nodeName2.equals("reference")) {
final NamedNodeMap attributes = n.getAttributes();
final Node nameNode = attributes.getNamedItem("name");
final Node bindNode = attributes.getNamedItem("bind");
final Node interfaceNameNode = attributes.getNamedItem("interface");
if (nameNode == null) {
cxt.addProblem(path + ": no name specified for reference");
continue;
}
if (bindNode == null) {
cxt.addProblem(path + "/" + nameNode + ": no bind method specified");
continue;
}
if (interfaceNameNode == null) {
cxt.addProblem(path + "/" + nameNode + ": no reference interface specified");
continue;
}
final String bindName = bindNode.getNodeValue();
if (!cxt.dsMethods.remove(implementationName + " - " + bindName)) {
cxt.addProblem(path + "/" + nameNode + ": bind method '" + bindName + "' does not have @Reference");
}
final String interfaceName = interfaceNameNode.getNodeValue();
/*
* WRONG: search the correct bundle - can be from a fragment, which can have a different classpath
*/
final URL classRes = cxt.bundle.getResource(interfaceName.replace('.', '/') + ".class");
if (classRes == null) {
cxt.addProblem(path + "/" + nameNode + ": reference interface '" + interfaceName + "' not found");
}
continue;
}
if (nodeName2.equals("#text")) {
continue;
}
if (nodeName2.equals("property")) {
continue;
}
cxt.addProblem(path + "/" + nodeName2 + ": unknown node");
}
for (final String mn : cxt.dsMethods) {
if (!mn.startsWith(implementationName + " - ")) {
continue;
}
cxt.addProblem(path + ": " + mn + " has @Reference but is not referenced in this file");
}
}
protected static class Context {
public Context(Bundle b) {
bundle = b;
}
/**
* Adds a new problem to the problems string...
*
* @param text
*/
public void addProblem(String text) {
problems.append("\n " + text);
}
public final Bundle bundle;
/**
* Found class with the {@link Component} annotation.
*/
public final List<String> dsClasses = new ArrayList<String>();
/**
* Found methods with the {@link Reference} annotation.
* <p>
* On the form "className - methodName".
*/
public final List<String> dsMethods = new ArrayList<String>();
/**
* Found methods with the {@link Reference} annotation.
* <p>
* On the form "className - methodName".
*/
public final List<String> dsActivates = new ArrayList<String>();
/**
* Found methods with the {@link Reference} annotation.
* <p>
* On the form "className - methodName".
*/
public final List<String> dsDeactivates = new ArrayList<String>();
/**
* Found files in OSGI-INF
*/
public final List<String> osgiinfDSFiles = new ArrayList<String>();
/**
* Found references in MANIFEST.MF
*/
public final List<String> manifestDSFiles = new ArrayList<String>();
/**
* Accumulated problems.
*/
public final StringBuilder problems = new StringBuilder();
}
}