Temporarily include bndlib 1.47 for testing purposes (not yet on central)
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1185095 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/Make.java b/bundleplugin/src/main/java/aQute/bnd/make/Make.java
new file mode 100644
index 0000000..6827c6d
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/Make.java
@@ -0,0 +1,99 @@
+package aQute.bnd.make;
+
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class Make {
+ Builder builder;
+ Map<Instruction, Map<String, String>> make;
+
+ public Make(Builder builder) {
+ this.builder = builder;
+ }
+
+ public Resource process(String source) {
+ Map<Instruction, Map<String, String>> make = getMakeHeader();
+ builder.trace("make " + source);
+
+ for (Map.Entry<Instruction, Map<String, String>> entry : make
+ .entrySet()) {
+ Instruction instr = entry.getKey();
+ Matcher m = instr.getMatcher(source);
+ if (m.matches() || instr.isNegated()) {
+ Map<String, String> arguments = replace(m, entry.getValue());
+ List<MakePlugin> plugins = builder.getPlugins(MakePlugin.class);
+ for (MakePlugin plugin : plugins) {
+ try {
+ Resource resource = plugin.make(builder,
+ source, arguments);
+ if (resource != null) {
+ builder.trace("Made " + source + " from args "
+ + arguments + " with " + plugin);
+ return resource;
+ }
+ } catch (Exception e) {
+ builder.error("Plugin " + plugin
+ + " generates error when use in making "
+ + source + " with args " + arguments, e);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private Map<String, String> replace(Matcher m, Map<String, String> value) {
+ Map<String, String> newArgs = Processor.newMap();
+ for (Map.Entry<String, String> entry : value.entrySet()) {
+ String s = entry.getValue();
+ s = replace(m, s);
+ newArgs.put(entry.getKey(), s);
+ }
+ return newArgs;
+ }
+
+ String replace(Matcher m, CharSequence s) {
+ StringBuffer sb = new StringBuffer();
+ int max = '0' + m.groupCount() + 1;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '$' && i < s.length() - 1) {
+ c = s.charAt(++i);
+ if (c >= '0' && c <= max) {
+ int index = c - '0';
+ String replacement = m.group(index);
+ if (replacement != null)
+ sb.append(replacement);
+ } else {
+ if (c == '$')
+ i++;
+ sb.append(c);
+ }
+ } else
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ Map<Instruction, Map<String, String>> getMakeHeader() {
+ if (make != null)
+ return make;
+ make = Processor.newMap();
+
+ String s = builder.getProperty(Builder.MAKE);
+ Map<String, Map<String, String>> make = builder.parseHeader(s);
+ for (Iterator<Map.Entry<String, Map<String, String>>> e = make
+ .entrySet().iterator(); e.hasNext();) {
+ Map.Entry<String, Map<String, String>> entry = e.next();
+ String pattern = Processor.removeDuplicateMarker(entry.getKey());
+
+ Instruction instr = Instruction.getPattern(pattern);
+ this.make.put(instr, entry.getValue());
+ }
+
+ return this.make;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java b/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java
new file mode 100644
index 0000000..f2e90d8
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/MakeBnd.java
@@ -0,0 +1,65 @@
+package aQute.bnd.make;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.build.*;
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class MakeBnd implements MakePlugin, Constants {
+ final static Pattern JARFILE = Pattern.compile("(.+)\\.(jar|ipa)");
+
+ public Resource make(Builder builder, String destination,
+ Map<String, String> argumentsOnMake) throws Exception {
+ String type = (String) argumentsOnMake.get("type");
+ if (!"bnd".equals(type))
+ return null;
+
+ String recipe = (String) argumentsOnMake.get("recipe");
+ if (recipe == null) {
+ builder.error("No recipe specified on a make instruction for "
+ + destination);
+ return null;
+ }
+ File bndfile = builder.getFile(recipe);
+ if (bndfile.isFile()) {
+ // We do not use a parent because then we would
+ // build ourselves again. So we can not blindly
+ // inherit the properties.
+ Builder bchild = builder.getSubBuilder();
+ bchild.removeBundleSpecificHeaders();
+
+ // We must make sure that we do not include ourselves again!
+ bchild.setProperty(Analyzer.INCLUDE_RESOURCE, "");
+ bchild.setProperty(Analyzer.INCLUDERESOURCE, "");
+ bchild.setProperties(bndfile, builder.getBase());
+
+ Jar jar = bchild.build();
+ Jar dot = builder.getTarget();
+
+ if (builder.hasSources()) {
+ for (String key : jar.getResources().keySet()) {
+ if (key.startsWith("OSGI-OPT/src"))
+ dot.putResource(key, (Resource) jar.getResource(key));
+ }
+ }
+ builder.getInfo(bchild, bndfile.getName() +": ");
+ String debug = bchild.getProperty(DEBUG);
+ if (Processor.isTrue(debug)) {
+ if ( builder instanceof ProjectBuilder ) {
+ ProjectBuilder pb = (ProjectBuilder) builder;
+ File target = pb.getProject().getTarget();
+ String bsn = bchild.getBsn();
+ File output = new File(target, bsn+".jar");
+ jar.write(output);
+ pb.getProject().getWorkspace().changedFile(output);
+ }
+ }
+ return new JarResource(jar);
+ } else
+ return null;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java b/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java
new file mode 100644
index 0000000..3d5e4c8
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/MakeCopy.java
@@ -0,0 +1,45 @@
+package aQute.bnd.make;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class MakeCopy implements MakePlugin {
+
+ public Resource make(Builder builder, String destination,
+ Map<String, String> argumentsOnMake) throws Exception {
+ String type = argumentsOnMake.get("type");
+ if (!type.equals("copy"))
+ return null;
+
+ String from = argumentsOnMake.get("from");
+ if (from == null) {
+ String content = argumentsOnMake.get("content");
+ if (content == null)
+ throw new IllegalArgumentException(
+ "No 'from' or 'content' field in copy "
+ + argumentsOnMake);
+ return new EmbeddedResource(content.getBytes("UTF-8"),0);
+ } else {
+
+ File f = builder.getFile(from);
+ if (f.isFile())
+ return new FileResource(f);
+ else {
+ try {
+ URL url = new URL(from);
+ return new URLResource(url);
+ } catch(MalformedURLException mfue) {
+ // We ignore this
+ }
+ throw new IllegalArgumentException(
+ "Copy source does not exist " + from
+ + " for destination " + destination);
+ }
+ }
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/calltree/CalltreeResource.java b/bundleplugin/src/main/java/aQute/bnd/make/calltree/CalltreeResource.java
new file mode 100644
index 0000000..2775a57
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/calltree/CalltreeResource.java
@@ -0,0 +1,165 @@
+package aQute.bnd.make.calltree;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import aQute.lib.osgi.*;
+
+/**
+ * Create an XML call tree of a set of classes. The structure of the XML is:
+ *
+ * <pre>
+ * calltree ::= <using> <usedby>
+ * using ::= <method> *
+ * usedby ::= <method> *
+ * method ::= <ref>
+ * </pre>
+ *
+ * The <code>using</code> element contains methods in the set of classes and
+ * their references. The <code>usedby</code> element contains the used methods
+ * and their references to the set of classes. The <code>ref</code> element
+ * contains the class, the method name, the descriptor, and a pretty print
+ * version of the method.
+ *
+ * The XML does not contain an XML processor instruction to make it easier to
+ * include in other XML. The encoding is always UTF-8.
+ *
+ * This class can be used as a resource, just add it to a JAR and the data is
+ * generated when the resource is written (saving time when the JAR is up to
+ * date and does not have to be generated). However, the actual write method is
+ * a static method and can be called as well:
+ * {@link #writeCalltree(PrintWriter, Collection)}.
+ */
+public class CalltreeResource extends WriteResource {
+ Collection<Clazz> classes;
+
+ /**
+ * Create a resource for inclusion that will print a call tree.
+ *
+ * @param values the classes for which the call tree is generated.
+ */
+ public CalltreeResource(Collection<Clazz> values) {
+ this.classes = values;
+ System.out.println(values);
+ }
+
+ /**
+ * We set the last modified to 0 so this resource does not force
+ * a new JAR if all other resources are up to date.
+ */
+ public long lastModified() {
+ return 0;
+ }
+
+ /**
+ * The write method is called to write the resource. We just call the static
+ * method.
+ */
+ public void write(OutputStream out) throws Exception {
+ OutputStreamWriter osw = new OutputStreamWriter(out, Constants.DEFAULT_CHARSET);
+ PrintWriter pw = new PrintWriter(osw);
+ try {
+ writeCalltree(pw, classes);
+ } finally {
+ pw.flush();
+ }
+ }
+
+ /**
+ * Print the call tree in XML.
+ *
+ * @param out The output writer
+ * @param classes The set of classes
+ * @throws IOException Any errors
+ */
+ public static void writeCalltree(PrintWriter out, Collection<Clazz> classes)
+ throws Exception {
+
+ final Map<Clazz.MethodDef, Set<Clazz.MethodDef>> using = new TreeMap<Clazz.MethodDef, Set<Clazz.MethodDef>>();
+ final Map<Clazz.MethodDef, Set<Clazz.MethodDef>> usedby = new TreeMap<Clazz.MethodDef, Set<Clazz.MethodDef>>();
+
+ ClassDataCollector cd = new ClassDataCollector() {
+ Clazz.MethodDef source;
+
+ // Before a method is parsed
+ public void method(Clazz.MethodDef source) {
+ this.source = source;
+ xref(using, source, null);
+ xref(usedby, source, null);
+ }
+
+ // For any reference in the previous method.
+ public void reference(Clazz.MethodDef reference) {
+ xref(using, source, reference);
+ xref(usedby, reference, source);
+ }
+ };
+ for (Clazz clazz : classes) {
+ clazz.parseClassFileWithCollector(cd);
+ }
+
+ out.println("<calltree>");
+ xref(out, "using", using);
+ xref(out, "usedby", usedby);
+ out.println("</calltree>");
+ }
+
+ /*
+ * Add a new reference
+ */
+ private static void xref(
+ Map<Clazz.MethodDef, Set<Clazz.MethodDef>> references,
+ Clazz.MethodDef source, Clazz.MethodDef reference) {
+ Set<Clazz.MethodDef> set = references.get(source);
+ if (set == null)
+ references.put(source, set=new TreeSet<Clazz.MethodDef>());
+ if ( reference != null)
+ set.add(reference);
+ }
+
+ /*
+ * Print out either using or usedby sets
+ */
+ private static void xref(PrintWriter out, String group,
+ Map<Clazz.MethodDef, Set<Clazz.MethodDef>> references) {
+ out.println(" <" + group + ">");
+ for (Map.Entry<Clazz.MethodDef, Set<Clazz.MethodDef>> entry : references
+ .entrySet()) {
+ Clazz.MethodDef source = entry.getKey();
+ Set<Clazz.MethodDef> refs = entry.getValue();
+ method(out, "method", source, ">");
+ for (Clazz.MethodDef ref : refs) {
+ method(out, "ref", ref, "/>");
+ }
+ out.println(" </method>");
+ }
+ out.println(" </" + group + ">");
+ }
+
+ /*
+ * Print out a method.
+ */
+ private static void method(PrintWriter out, String element,
+ Clazz.MethodDef source, String closeElement) {
+ out.println(" <" + element + " class='" + source.clazz + "'"
+ + getAccess(source.access) +
+ ( source.isConstructor() ? "" : " name='" + source.name + "'") + " descriptor='" + source.descriptor + "' pretty='"
+ + source.getPretty() + "'" + closeElement);
+ }
+
+ private static String getAccess(int access) {
+ StringBuilder sb = new StringBuilder();
+ if ( Modifier.isPublic(access) )
+ sb.append(" public='true'");
+ if ( Modifier.isStatic(access) )
+ sb.append(" static='true'");
+ if ( Modifier.isProtected(access) )
+ sb.append(" protected='true'");
+ if ( Modifier.isInterface(access) )
+ sb.append(" interface='true'");
+
+ return sb.toString();
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/component/ComponentAnnotationReader.java b/bundleplugin/src/main/java/aQute/bnd/make/component/ComponentAnnotationReader.java
new file mode 100644
index 0000000..a73e714
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/component/ComponentAnnotationReader.java
@@ -0,0 +1,343 @@
+package aQute.bnd.make.component;
+
+import static aQute.bnd.make.component.ServiceComponent.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.annotation.component.*;
+import aQute.lib.osgi.*;
+import aQute.libg.reporter.*;
+
+public class ComponentAnnotationReader extends ClassDataCollector {
+ String EMPTY[] = new String[0];
+ private static final String V1_1 = "1.1.0"; // "1.1.0"
+ static Pattern BINDDESCRIPTOR = Pattern
+ .compile("\\(L([^;]*);(Ljava/util/Map;|Lorg/osgi/framework/ServiceReference;)*\\)V");
+ static Pattern BINDMETHOD = Pattern.compile("(set|bind|add)(.)(.*)");
+
+ static Pattern ACTIVATEDESCRIPTOR = Pattern
+ .compile("\\(((Lorg/osgi/service/component/ComponentContext;)|(Lorg/osgi/framework/BundleContext;)|(Ljava/util/Map;))*\\)V");
+ static Pattern OLDACTIVATEDESCRIPTOR = Pattern
+ .compile("\\(Lorg/osgi/service/component/ComponentContext;\\)V");
+ static Pattern OLDBINDDESCRIPTOR = Pattern.compile("\\(L([^;]*);\\)V");
+ static Pattern REFERENCEBINDDESCRIPTOR = Pattern
+ .compile("\\(Lorg/osgi/framework/ServiceReference;\\)V");
+
+ Reporter reporter = new Processor();
+ String method;
+ String methodDescriptor;
+ int methodAccess;
+ String className;
+ Clazz clazz;
+ String interfaces[];
+ Set<String> multiple = new HashSet<String>();
+ Set<String> optional = new HashSet<String>();
+ Set<String> dynamic = new HashSet<String>();
+
+ Map<String, String> map = new TreeMap<String, String>();
+ Set<String> descriptors = new HashSet<String>();
+ List<String> properties = new ArrayList<String>();
+ String version = null;
+
+ // TODO make patterns for descriptors
+
+ ComponentAnnotationReader(Clazz clazz) {
+ this.clazz = clazz;
+ }
+
+ public void setReporter(Reporter reporter) {
+ this.reporter = reporter;
+ }
+
+ public Reporter getReporter() {
+ return this.reporter;
+ }
+
+ public static Map<String, String> getDefinition(Clazz c) throws Exception {
+ return getDefinition(c, new Processor());
+ }
+
+ public static Map<String, String> getDefinition(Clazz c, Reporter reporter) throws Exception {
+ ComponentAnnotationReader r = new ComponentAnnotationReader(c);
+ r.setReporter(reporter);
+ c.parseClassFileWithCollector(r);
+ r.finish();
+ return r.map;
+ }
+
+ public void annotation(Annotation annotation) {
+
+ if (annotation.getName().equals(Component.RNAME)) {
+ set(COMPONENT_NAME, annotation.get(Component.NAME), "<>");
+ set(COMPONENT_FACTORY, annotation.get(Component.FACTORY), false);
+ setBoolean(COMPONENT_ENABLED, annotation.get(Component.ENABLED), true);
+ setBoolean(COMPONENT_IMMEDIATE, annotation.get(Component.IMMEDIATE), false);
+ setBoolean(COMPONENT_SERVICEFACTORY, annotation.get(Component.SERVICEFACTORY), false);
+
+ if (annotation.get(Component.DESIGNATE) != null) {
+ String configs = annotation.get(Component.DESIGNATE);
+ if (configs != null) {
+ set(COMPONENT_DESIGNATE, Clazz.objectDescriptorToFQN(configs), "");
+ }
+ }
+
+ if (annotation.get(Component.DESIGNATE_FACTORY) != null) {
+ String configs = annotation.get(Component.DESIGNATE_FACTORY);
+ if (configs != null) {
+ set(COMPONENT_DESIGNATEFACTORY, Clazz.objectDescriptorToFQN(configs), "");
+ }
+ }
+
+ setVersion((String) annotation.get(Component.VERSION));
+
+ String configurationPolicy = annotation.get(Component.CONFIGURATION_POLICY);
+ if (configurationPolicy != null)
+ set(COMPONENT_CONFIGURATION_POLICY, configurationPolicy.toLowerCase(), "");
+
+ doProperties(annotation);
+
+ Object[] provides = (Object[]) annotation.get(Component.PROVIDE);
+ String[] p;
+ if (provides == null) {
+ // Use the found interfaces, but convert from internal to
+ // fqn.
+ if (interfaces != null) {
+ List<String> result = new ArrayList<String>();
+ for (int i = 0; i < interfaces.length; i++) {
+ if (!interfaces[i].equals("scala/ScalaObject") )
+ result.add(Clazz.internalToFqn(interfaces[i]));
+ }
+ p = result.toArray(EMPTY);
+ } else
+ p = EMPTY;
+ } else {
+ // We have explicit interfaces set
+ p = new String[provides.length];
+ for (int i = 0; i < provides.length; i++) {
+ p[i] = descriptorToFQN(provides[i].toString());
+ }
+ }
+ if (p.length > 0) {
+ set(COMPONENT_PROVIDE, Processor.join(Arrays.asList(p)), "<>");
+ }
+
+ } else if (annotation.getName().equals(Activate.RNAME)) {
+ if (!checkMethod())
+ setVersion(V1_1);
+
+ if (!ACTIVATEDESCRIPTOR.matcher(methodDescriptor).matches())
+ reporter.error(
+ "Activate method for %s does not have an acceptable prototype, only Map, ComponentContext, or BundleContext is allowed. Found: %s",
+ className, methodDescriptor);
+
+ if (method.equals("activate")
+ && OLDACTIVATEDESCRIPTOR.matcher(methodDescriptor).matches()) {
+ // this is the default!
+ } else {
+ setVersion(V1_1);
+ set(COMPONENT_ACTIVATE, method, "<>");
+ }
+
+ } else if (annotation.getName().equals(Deactivate.RNAME)) {
+ if (!checkMethod())
+ setVersion(V1_1);
+
+ if (!ACTIVATEDESCRIPTOR.matcher(methodDescriptor).matches())
+ reporter.error(
+ "Deactivate method for %s does not have an acceptable prototype, only Map, ComponentContext, or BundleContext is allowed. Found: %s",
+ className, methodDescriptor);
+ if (method.equals("deactivate")
+ && OLDACTIVATEDESCRIPTOR.matcher(methodDescriptor).matches()) {
+ // This is the default!
+ } else {
+ setVersion(V1_1);
+ set(COMPONENT_DEACTIVATE, method, "<>");
+ }
+ } else if (annotation.getName().equals(Modified.RNAME)) {
+ set(COMPONENT_MODIFIED, method, "<>");
+ setVersion(V1_1);
+ } else if (annotation.getName().equals(Reference.RNAME)) {
+
+ String name = (String) annotation.get(Reference.NAME);
+ String bind = method;
+ String unbind = null;
+
+ if (name == null) {
+ Matcher m = BINDMETHOD.matcher(method);
+ if (m.matches()) {
+ name = m.group(2).toLowerCase() + m.group(3);
+ } else {
+ name = method.toLowerCase();
+ }
+ }
+ String simpleName = name;
+
+ unbind = annotation.get(Reference.UNBIND);
+
+ if (bind != null) {
+ name = name + "/" + bind;
+ if (unbind != null)
+ name = name + "/" + unbind;
+ }
+ String service = annotation.get(Reference.SERVICE);
+
+ if (service != null) {
+ service = Clazz.objectDescriptorToFQN(service);
+ } else {
+ // We have to find the type of the current method to
+ // link it to the referenced service.
+ Matcher m = BINDDESCRIPTOR.matcher(methodDescriptor);
+ if (m.matches()) {
+ service = Clazz.internalToFqn(m.group(1));
+ } else
+ throw new IllegalArgumentException(
+ "Cannot detect the type of a Component Reference from the descriptor: "
+ + methodDescriptor);
+ }
+
+ // Check if we have a target, this must be a filter
+ String target = annotation.get(Reference.TARGET);
+ if (target != null) {
+ Verifier.verifyFilter(target, 0);
+ service = service + target;
+ }
+
+ Integer c = annotation.get(Reference.TYPE);
+ if (c != null && !c.equals(0) && !c.equals((int) '1')) {
+ service = service + (char) c.intValue();
+ }
+
+ if (map.containsKey(name))
+ reporter.error(
+ "In component %s, Multiple references with the same name: %s. Previous def: %s, this def: %s",
+ name, map.get(name), service, "");
+ map.put(name, service);
+
+ if (isTrue(annotation.get(Reference.MULTIPLE)))
+ multiple.add(simpleName);
+ if (isTrue(annotation.get(Reference.OPTIONAL)))
+ optional.add(simpleName);
+ if (isTrue(annotation.get(Reference.DYNAMIC)))
+ dynamic.add(simpleName);
+
+ if (!checkMethod())
+ setVersion(V1_1);
+ else if (REFERENCEBINDDESCRIPTOR.matcher(methodDescriptor).matches()
+ || !OLDBINDDESCRIPTOR.matcher(methodDescriptor).matches())
+ setVersion(V1_1);
+ }
+ }
+
+ private void setVersion(String v) {
+ if (v == null)
+ return;
+
+ if (version == null)
+ version = v;
+ else if (v.compareTo(version) > 0) // we're safe to 9.9.9
+ version = v;
+ }
+
+ private boolean checkMethod() {
+ return Modifier.isPublic(methodAccess) || Modifier.isProtected(methodAccess);
+ }
+
+ static Pattern PROPERTY_PATTERN = Pattern.compile("[^=]+=.+");
+
+ private void doProperties(Annotation annotation) {
+ Object[] properties = annotation.get(Component.PROPERTIES);
+
+ if (properties != null) {
+ for (Object o : properties) {
+ String p = (String) o;
+ if (PROPERTY_PATTERN.matcher(p).matches())
+ this.properties.add(p);
+ else
+ throw new IllegalArgumentException("Malformed property '" + p + "' on: "
+ + annotation.get(Component.NAME));
+ }
+ }
+ }
+
+ private boolean isTrue(Object object) {
+ if (object == null)
+ return false;
+ return (Boolean) object;
+ }
+
+ private void setBoolean(String string, Object object, boolean b) {
+ if (object == null)
+ object = b;
+
+ Boolean bb = (Boolean) object;
+ if (bb == b)
+ return;
+
+ map.put(string, bb.toString());
+ }
+
+ private void set(String string, Object object, Object deflt) {
+ if (object == null || object.equals(deflt))
+ return;
+
+ map.put(string, object.toString());
+ }
+
+ /**
+ * Skip L and ; and replace / for . in an object descriptor.
+ *
+ * A string like Lcom/acme/Foo; becomes com.acme.Foo
+ *
+ * @param string
+ * @return
+ */
+
+ private String descriptorToFQN(String string) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 1; i < string.length() - 1; i++) {
+ char c = string.charAt(i);
+ if (c == '/')
+ c = '.';
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ @Override public void classBegin(int access, String name) {
+ className = name;
+ }
+
+ @Override public void implementsInterfaces(String[] interfaces) {
+ this.interfaces = interfaces;
+ }
+
+ @Override public void method(int access, String name, String descriptor) {
+ this.method = name;
+ this.methodDescriptor = descriptor;
+ this.methodAccess = access;
+ descriptors.add(method);
+ }
+
+ void set(String name, Collection<String> l) {
+ if (l.size() == 0)
+ return;
+
+ set(name, Processor.join(l), "<>");
+ }
+
+ public void finish() {
+ set(COMPONENT_MULTIPLE, multiple);
+ set(COMPONENT_DYNAMIC, dynamic);
+ set(COMPONENT_OPTIONAL, optional);
+ set(COMPONENT_IMPLEMENTATION, clazz.getFQN(), "<>");
+ set(COMPONENT_PROPERTIES, properties);
+ if (version != null) {
+ set(COMPONENT_VERSION, version, "<>");
+ reporter.trace("Component %s is v1.1", map);
+ }
+ set(COMPONENT_DESCRIPTORS, descriptors);
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/component/ServiceComponent.java b/bundleplugin/src/main/java/aQute/bnd/make/component/ServiceComponent.java
new file mode 100644
index 0000000..652d367
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/component/ServiceComponent.java
@@ -0,0 +1,665 @@
+package aQute.bnd.make.component;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.annotation.component.*;
+import aQute.bnd.make.metatype.*;
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.*;
+import aQute.libg.version.*;
+
+/**
+ * This class is an analyzer plugin. It looks at the properties and tries to
+ * find out if the Service-Component header contains the bnd shortut syntax. If
+ * not, the header is copied to the output, if it does, an XML file is created
+ * and added to the JAR and the header is modified appropriately.
+ */
+public class ServiceComponent implements AnalyzerPlugin {
+ public final static String NAMESPACE_STEM = "http://www.osgi.org/xmlns/scr";
+ public final static String JIDENTIFIER = "<<identifier>>";
+ public final static String COMPONENT_NAME = "name:";
+ public final static String COMPONENT_FACTORY = "factory:";
+ public final static String COMPONENT_SERVICEFACTORY = "servicefactory:";
+ public final static String COMPONENT_IMMEDIATE = "immediate:";
+ public final static String COMPONENT_ENABLED = "enabled:";
+ public final static String COMPONENT_DYNAMIC = "dynamic:";
+ public final static String COMPONENT_MULTIPLE = "multiple:";
+ public final static String COMPONENT_PROVIDE = "provide:";
+ public final static String COMPONENT_OPTIONAL = "optional:";
+ public final static String COMPONENT_PROPERTIES = "properties:";
+ public final static String COMPONENT_IMPLEMENTATION = "implementation:";
+ public final static String COMPONENT_DESIGNATE = "designate:";
+ public final static String COMPONENT_DESIGNATEFACTORY = "designateFactory:";
+ public final static String COMPONENT_DESCRIPTORS = ".descriptors:";
+
+ // v1.1.0
+ public final static String COMPONENT_VERSION = "version:";
+ public final static String COMPONENT_CONFIGURATION_POLICY = "configuration-policy:";
+ public final static String COMPONENT_MODIFIED = "modified:";
+ public final static String COMPONENT_ACTIVATE = "activate:";
+ public final static String COMPONENT_DEACTIVATE = "deactivate:";
+
+ final static Map<String, String> EMPTY = Collections.emptyMap();
+
+ public final static String[] componentDirectives = new String[] {
+ COMPONENT_FACTORY, COMPONENT_IMMEDIATE, COMPONENT_ENABLED, COMPONENT_DYNAMIC,
+ COMPONENT_MULTIPLE, COMPONENT_PROVIDE, COMPONENT_OPTIONAL, COMPONENT_PROPERTIES,
+ COMPONENT_IMPLEMENTATION, COMPONENT_SERVICEFACTORY, COMPONENT_VERSION,
+ COMPONENT_CONFIGURATION_POLICY, COMPONENT_MODIFIED, COMPONENT_ACTIVATE,
+ COMPONENT_DEACTIVATE, COMPONENT_NAME, COMPONENT_DESCRIPTORS, COMPONENT_DESIGNATE,
+ COMPONENT_DESIGNATEFACTORY };
+
+ public final static Set<String> SET_COMPONENT_DIRECTIVES = new HashSet<String>(
+ Arrays.asList(componentDirectives));
+
+ public final static Set<String> SET_COMPONENT_DIRECTIVES_1_1 = //
+ new HashSet<String>(
+ Arrays.asList(
+ COMPONENT_VERSION,
+ COMPONENT_CONFIGURATION_POLICY,
+ COMPONENT_MODIFIED,
+ COMPONENT_ACTIVATE,
+ COMPONENT_DEACTIVATE));
+
+ public boolean analyzeJar(Analyzer analyzer) throws Exception {
+
+ ComponentMaker m = new ComponentMaker(analyzer);
+
+ Map<String, Map<String, String>> l = m.doServiceComponent();
+
+ analyzer.setProperty(Constants.SERVICE_COMPONENT, Processor.printClauses(l));
+
+ analyzer.getInfo(m, "Service-Component: ");
+ m.close();
+
+ return false;
+ }
+
+ private static class ComponentMaker extends Processor {
+ Analyzer analyzer;
+
+ ComponentMaker(Analyzer analyzer) {
+ super(analyzer);
+ this.analyzer = analyzer;
+ }
+
+ /**
+ * Iterate over the Service Component entries. There are two cases:
+ * <ol>
+ * <li>An XML file reference</li>
+ * <li>A FQN/wildcard with a set of attributes</li>
+ * </ol>
+ *
+ * An XML reference is immediately expanded, an FQN/wildcard is more
+ * complicated and is delegated to
+ * {@link #componentEntry(Map, String, Map)}.
+ *
+ * @throws Exception
+ */
+ Map<String, Map<String, String>> doServiceComponent() throws Exception {
+ Map<String, Map<String, String>> serviceComponents = newMap();
+ String header = getProperty(SERVICE_COMPONENT);
+ Map<String, Map<String, String>> sc = parseHeader(header);
+
+ for (Map.Entry<String, Map<String, String>> entry : sc.entrySet()) {
+ String name = entry.getKey();
+ Map<String, String> info = entry.getValue();
+
+ try {
+ if (name.indexOf('/') >= 0 || name.endsWith(".xml")) {
+ // Normal service component, we do not process it
+ serviceComponents.put(name, EMPTY);
+ } else {
+ componentEntry(serviceComponents, name, info);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ error("Invalid Service-Component header: %s %s, throws %s", name, info, e);
+ }
+ }
+ return serviceComponents;
+ }
+
+ /**
+ * Parse an entry in the Service-Component header. This header supports
+ * the following types:
+ * <ol>
+ * <li>An FQN + attributes describing a component</li>
+ * <li>A wildcard expression for finding annotated components.</li>
+ * </ol>
+ * The problem is the distinction between an FQN and a wildcard because
+ * an FQN can also be used as a wildcard.
+ *
+ * If the info specifies {@link Constants#NOANNOTATIONS} then wildcards
+ * are an error and the component must be fully described by the info.
+ * Otherwise the FQN/wildcard is expanded into a list of classes with
+ * annotations. If this list is empty, the FQN case is interpreted as a
+ * complete component definition. For the wildcard case, it is checked
+ * if any matching classes for the wildcard have been compiled for a
+ * class file format that does not support annotations, this can be a
+ * problem with JSR14 who silently ignores annotations. An error is
+ * reported in such a case.
+ *
+ * @param serviceComponents
+ * @param name
+ * @param info
+ * @throws Exception
+ * @throws IOException
+ */
+ private void componentEntry(Map<String, Map<String, String>> serviceComponents,
+ String name, Map<String, String> info) throws Exception, IOException {
+
+ boolean annotations = !Processor.isTrue(info.get(NOANNOTATIONS));
+ boolean fqn = Verifier.isFQN(name);
+
+ if (annotations) {
+
+ // Annotations possible!
+
+ Collection<Clazz> annotatedComponents = analyzer.getClasses("",
+ QUERY.ANNOTATION.toString(), Component.class.getName(), //
+ QUERY.NAMED.toString(), name //
+ );
+
+ if (fqn) {
+ if (annotatedComponents.isEmpty()) {
+
+ // No annotations, fully specified in header
+
+ createComponentResource(serviceComponents, name, info);
+ } else {
+
+ // We had a FQN so expect only one
+
+ for (Clazz c : annotatedComponents) {
+ annotated(serviceComponents, c, info);
+ }
+ }
+ } else {
+
+ // We did not have an FQN, so expect the use of wildcards
+
+ if (annotatedComponents.isEmpty())
+ checkAnnotationsFeasible(name);
+ else
+ for (Clazz c : annotatedComponents) {
+ annotated(serviceComponents, c, info);
+ }
+ }
+ } else {
+ // No annotations
+ if (fqn)
+ createComponentResource(serviceComponents, name, info);
+ else
+ error("Set to %s but entry %s is not an FQN ", NOANNOTATIONS, name);
+
+ }
+ }
+
+ /**
+ * Check if annotations are actually feasible looking at the class
+ * format. If the class format does not provide annotations then it is
+ * no use specifying annotated components.
+ *
+ * @param name
+ * @return
+ * @throws Exception
+ */
+ private Collection<Clazz> checkAnnotationsFeasible(String name) throws Exception {
+ Collection<Clazz> not = analyzer.getClasses("", QUERY.NAMED.toString(), name //
+ );
+
+ if (not.isEmpty())
+ if ( "*".equals(name))
+ return not;
+ else
+ error("Specified %s but could not find any class matching this pattern", name);
+
+ for (Clazz c : not) {
+ if (c.getFormat().hasAnnotations())
+ return not;
+ }
+
+ warning("Wildcards are used (%s) requiring annotations to decide what is a component. Wildcard maps to classes that are compiled with java.target < 1.5. Annotations were introduced in Java 1.5",
+ name);
+
+ return not;
+ }
+
+ void annotated(Map<String, Map<String, String>> components, Clazz c,
+ Map<String, String> info) throws Exception {
+ // Get the component definition
+ // from the annotations
+ Map<String, String> map = ComponentAnnotationReader.getDefinition(c, this);
+
+ // Pick the name, the annotation can override
+ // the name.
+ String localname = map.get(COMPONENT_NAME);
+ if (localname == null)
+ localname = c.getFQN();
+
+ // Override the component info without manifest
+ // entries. We merge the properties though.
+
+ String merged = Processor.merge(info.remove(COMPONENT_PROPERTIES),
+ map.remove(COMPONENT_PROPERTIES));
+ if (merged != null && merged.length() > 0)
+ map.put(COMPONENT_PROPERTIES, merged);
+ map.putAll(info);
+ createComponentResource(components, localname, map);
+ }
+
+ private void createComponentResource(Map<String, Map<String, String>> components,
+ String name, Map<String, String> info) throws IOException {
+
+ // We can override the name in the parameters
+ if (info.containsKey(COMPONENT_NAME))
+ name = info.get(COMPONENT_NAME);
+
+ // Assume the impl==name, but allow override
+ String impl = name;
+ if (info.containsKey(COMPONENT_IMPLEMENTATION))
+ impl = info.get(COMPONENT_IMPLEMENTATION);
+
+ // Check if such a class exists
+ analyzer.referTo(impl);
+
+ boolean designate = designate(name, info.get(COMPONENT_DESIGNATE), false)
+ || designate(name, info.get(COMPONENT_DESIGNATEFACTORY), true);
+
+ // If we had a designate, we want a default configuration policy of
+ // require.
+ if (designate && info.get(COMPONENT_CONFIGURATION_POLICY) == null)
+ info.put(COMPONENT_CONFIGURATION_POLICY, "require");
+
+ // We have a definition, so make an XML resources
+ Resource resource = createComponentResource(name, impl, info);
+ analyzer.getJar().putResource("OSGI-INF/" + name + ".xml", resource);
+
+ components.put("OSGI-INF/" + name + ".xml", EMPTY);
+
+ }
+
+ /**
+ * Create a Metatype and Designate record out of the given
+ * configurations.
+ *
+ * @param name
+ * @param config
+ */
+ private boolean designate(String name, String config, boolean factory) {
+ if (config == null)
+ return false;
+
+ for (String c : Processor.split(config)) {
+ Clazz clazz = analyzer.getClassspace().get(Clazz.fqnToPath(c));
+ if (clazz != null) {
+ analyzer.referTo(c);
+ MetaTypeReader r = new MetaTypeReader(clazz, analyzer);
+ r.setDesignate(name, factory);
+ String rname = "OSGI-INF/metatype/" + name + ".xml";
+
+ analyzer.getJar().putResource(rname, r);
+ } else {
+ analyzer.error(
+ "Cannot find designated configuration class %s for component %s", c,
+ name);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create the resource for a DS component.
+ *
+ * @param list
+ * @param name
+ * @param info
+ * @throws UnsupportedEncodingException
+ */
+ Resource createComponentResource(String name, String impl, Map<String, String> info)
+ throws IOException {
+ String namespace = getNamespace(info);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, Constants.DEFAULT_CHARSET));
+ pw.println("<?xml version='1.0' encoding='utf-8'?>");
+ if (namespace != null)
+ pw.print("<scr:component xmlns:scr='" + namespace + "'");
+ else
+ pw.print("<component");
+
+ doAttribute(pw, name, "name");
+ doAttribute(pw, info.get(COMPONENT_FACTORY), "factory");
+ doAttribute(pw, info.get(COMPONENT_IMMEDIATE), "immediate", "false", "true");
+ doAttribute(pw, info.get(COMPONENT_ENABLED), "enabled", "true", "false");
+ doAttribute(pw, info.get(COMPONENT_CONFIGURATION_POLICY), "configuration-policy",
+ "optional", "require", "ignore");
+ doAttribute(pw, info.get(COMPONENT_ACTIVATE), "activate", JIDENTIFIER);
+ doAttribute(pw, info.get(COMPONENT_DEACTIVATE), "deactivate", JIDENTIFIER);
+ doAttribute(pw, info.get(COMPONENT_MODIFIED), "modified", JIDENTIFIER);
+
+ pw.println(">");
+
+ // Allow override of the implementation when people
+ // want to choose their own name
+ pw.println(" <implementation class='" + (impl == null ? name : impl) + "'/>");
+
+ String provides = info.get(COMPONENT_PROVIDE);
+ boolean servicefactory = Processor.isTrue(info.get(COMPONENT_SERVICEFACTORY));
+
+ if (servicefactory && Processor.isTrue(info.get(COMPONENT_IMMEDIATE))) {
+ // TODO can become error() if it is up to me
+ warning("For a Service Component, the immediate option and the servicefactory option are mutually exclusive for %(%s)",
+ name, impl);
+ }
+ provide(pw, provides, servicefactory, impl);
+ properties(pw, info);
+ reference(info, pw);
+
+ if (namespace != null)
+ pw.println("</scr:component>");
+ else
+ pw.println("</component>");
+
+ pw.close();
+ byte[] data = out.toByteArray();
+ out.close();
+ return new EmbeddedResource(data, 0);
+ }
+
+ private void doAttribute(PrintWriter pw, String value, String name, String... matches) {
+ if (value != null) {
+ if (matches.length != 0) {
+ if (matches.length == 1 && matches[0].equals(JIDENTIFIER)) {
+ if (!Verifier.isIdentifier(value))
+ error("Component attribute %s has value %s but is not a Java identifier",
+ name, value);
+ } else {
+
+ if (!Verifier.isMember(value, matches))
+ error("Component attribute %s has value %s but is not a member of %s",
+ name, value, Arrays.toString(matches));
+ }
+ }
+ pw.print(" ");
+ pw.print(name);
+ pw.print("='");
+ pw.print(value);
+ pw.print("'");
+ }
+ }
+
+ /**
+ * Check if we need to use the v1.1 namespace (or later).
+ *
+ * @param info
+ * @return
+ */
+ private String getNamespace(Map<String, String> info) {
+ String version = info.get(COMPONENT_VERSION);
+ if (version != null) {
+ try {
+ Version v = new Version(version);
+ return NAMESPACE_STEM + "/v" + v;
+ } catch (Exception e) {
+ error("version: specified on component header but not a valid version: "
+ + version);
+ return null;
+ }
+ }
+ for (String key : info.keySet()) {
+ if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) {
+ return NAMESPACE_STEM + "/v1.1.0";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Print the Service-Component properties element
+ *
+ * @param pw
+ * @param info
+ */
+ void properties(PrintWriter pw, Map<String, String> info) {
+ Collection<String> properties = split(info.get(COMPONENT_PROPERTIES));
+ for (Iterator<String> p = properties.iterator(); p.hasNext();) {
+ String clause = p.next();
+ int n = clause.indexOf('=');
+ if (n <= 0) {
+ error("Not a valid property in service component: " + clause);
+ } else {
+ String type = null;
+ String name = clause.substring(0, n);
+ if (name.indexOf('@') >= 0) {
+ String parts[] = name.split("@");
+ name = parts[1];
+ type = parts[0];
+ } else if (name.indexOf(':') >= 0) {
+ String parts[] = name.split(":");
+ name = parts[0];
+ type = parts[1];
+ }
+ String value = clause.substring(n + 1).trim();
+ // TODO verify validity of name and value.
+ pw.print(" <property name='");
+ pw.print(name);
+ pw.print("'");
+
+ if (type != null) {
+ if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
+ pw.print(" type='");
+ pw.print(type);
+ pw.print("'");
+ } else {
+ warning("Invalid property type '" + type + "' for property " + name);
+ }
+ }
+
+ String parts[] = value.split("\\s*(\\||\\n)\\s*");
+ if (parts.length > 1) {
+ pw.println(">");
+ for (String part : parts) {
+ pw.println(part);
+ }
+ pw.println("</property>");
+ } else {
+ pw.print(" value='");
+ pw.print(parts[0]);
+ pw.println("'/>");
+ }
+ }
+ }
+ }
+
+ /**
+ * @param pw
+ * @param provides
+ */
+ void provide(PrintWriter pw, String provides, boolean servicefactory, String impl) {
+ if (provides != null) {
+ if (!servicefactory)
+ pw.println(" <service>");
+ else
+ pw.println(" <service servicefactory='true'>");
+
+ StringTokenizer st = new StringTokenizer(provides, ",");
+ while (st.hasMoreTokens()) {
+ String interfaceName = st.nextToken();
+ pw.println(" <provide interface='" + interfaceName + "'/>");
+ analyzer.referTo(interfaceName);
+
+ // TODO verifies the impl. class extends or implements the
+ // interface
+ }
+ pw.println(" </service>");
+ } else if (servicefactory)
+ warning("The servicefactory:=true directive is set but no service is provided, ignoring it");
+ }
+
+ public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
+
+ /**
+ * @param info
+ * @param pw
+ */
+
+ void reference(Map<String, String> info, PrintWriter pw) {
+ Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC)));
+ Collection<String> optional = new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL)));
+ Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE)));
+
+ Collection<String> descriptors = split(info.get(COMPONENT_DESCRIPTORS));
+
+ for (Map.Entry<String, String> entry : info.entrySet()) {
+
+ // Skip directives
+ String referenceName = entry.getKey();
+ if (referenceName.endsWith(":")) {
+ if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
+ error("Unrecognized directive in Service-Component header: "
+ + referenceName);
+ continue;
+ }
+
+ // Parse the bind/unbind methods from the name
+ // if set. They are separated by '/'
+ String bind = null;
+ String unbind = null;
+
+ boolean unbindCalculated = false;
+
+ if (referenceName.indexOf('/') >= 0) {
+ String parts[] = referenceName.split("/");
+ referenceName = parts[0];
+ bind = parts[1];
+ if (parts.length > 2) {
+ unbind = parts[2];
+ } else {
+ unbindCalculated = true;
+ if (bind.startsWith("add"))
+ unbind = bind.replaceAll("add(.+)", "remove$1");
+ else
+ unbind = "un" + bind;
+ }
+ } else if (Character.isLowerCase(referenceName.charAt(0))) {
+ unbindCalculated = true;
+ bind = "set" + Character.toUpperCase(referenceName.charAt(0))
+ + referenceName.substring(1);
+ unbind = "un" + bind;
+ }
+
+ String interfaceName = entry.getValue();
+ if (interfaceName == null || interfaceName.length() == 0) {
+ error("Invalid Interface Name for references in Service Component: "
+ + referenceName + "=" + interfaceName);
+ continue;
+ }
+
+ // If we have descriptors, we have analyzed the component.
+ // So why not check the methods
+ if (descriptors.size() > 0) {
+ // Verify that the bind method exists
+ if (!descriptors.contains(bind))
+ error("The bind method %s for %s not defined", bind, referenceName);
+
+ // Check if the unbind method exists
+ if (!descriptors.contains(unbind)) {
+ if (unbindCalculated)
+ // remove it
+ unbind = null;
+ else
+ error("The unbind method %s for %s not defined", unbind, referenceName);
+ }
+ }
+ // Check tje cardinality by looking at the last
+ // character of the value
+ char c = interfaceName.charAt(interfaceName.length() - 1);
+ if ("?+*~".indexOf(c) >= 0) {
+ if (c == '?' || c == '*' || c == '~')
+ optional.add(referenceName);
+ if (c == '+' || c == '*')
+ multiple.add(referenceName);
+ if (c == '+' || c == '*' || c == '?')
+ dynamic.add(referenceName);
+ interfaceName = interfaceName.substring(0, interfaceName.length() - 1);
+ }
+
+ // Parse the target from the interface name
+ // The target is a filter.
+ String target = null;
+ Matcher m = REFERENCE.matcher(interfaceName);
+ if (m.matches()) {
+ interfaceName = m.group(1);
+ target = m.group(2);
+ }
+
+ analyzer.referTo(interfaceName);
+
+ pw.printf(" <reference name='%s'", referenceName);
+ pw.printf(" interface='%s'", interfaceName);
+
+ String cardinality = optional.contains(referenceName) ? "0" : "1";
+ cardinality += "..";
+ cardinality += multiple.contains(referenceName) ? "n" : "1";
+ if (!cardinality.equals("1..1"))
+ pw.print(" cardinality='" + cardinality + "'");
+
+ if (bind != null) {
+ pw.printf(" bind='%s'", bind);
+ if (unbind != null) {
+ pw.printf(" unbind='%s'", unbind);
+ }
+ }
+
+ if (dynamic.contains(referenceName)) {
+ pw.print(" policy='dynamic'");
+ }
+
+ if (target != null) {
+ // Filter filter = new Filter(target);
+ // if (filter.verify() == null)
+ // pw.print(" target='" + filter.toString() + "'");
+ // else
+ // error("Target for " + referenceName
+ // + " is not a correct filter: " + target + " "
+ // + filter.verify());
+ pw.print(" target='" + escape(target) + "'");
+ }
+ pw.println("/>");
+ }
+ }
+ }
+
+ /**
+ * Escape a string, do entity conversion.
+ */
+ static String escape(String s) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '<':
+ sb.append("<");
+ break;
+ case '>':
+ sb.append(">");
+ break;
+ case '&':
+ sb.append("&");
+ break;
+ case '\'':
+ sb.append(""");
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/coverage/Coverage.java b/bundleplugin/src/main/java/aQute/bnd/make/coverage/Coverage.java
new file mode 100644
index 0000000..bc33dc2
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/coverage/Coverage.java
@@ -0,0 +1,95 @@
+package aQute.bnd.make.coverage;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.*;
+
+/**
+ * This class can create a coverage table between two classspaces. The
+ * destination class space is used to create a table of methods. All source
+ * methods that refer to a specific dest are then filled into the table.
+ *
+ */
+public class Coverage {
+
+ /**
+ * Create a cross reference table from source to dest.
+ *
+ * @param source
+ * The methods that refer to dest
+ * @param dest
+ * The methods that are being referred to
+ * @return A mapping of source methods to destination methods.
+ * @throws IOException
+ */
+ public static Map<MethodDef, List<MethodDef>> getCrossRef(
+ Collection<Clazz> source, Collection<Clazz> dest)
+ throws Exception {
+ final Map<MethodDef, List<MethodDef>> catalog = buildCatalog(dest);
+ crossRef(source, catalog);
+ return catalog;
+ }
+
+ private static void crossRef(Collection<Clazz> source,
+ final Map<MethodDef, List<MethodDef>> catalog) throws Exception {
+ for (final Clazz clazz : source) {
+ clazz.parseClassFileWithCollector(new ClassDataCollector() {
+ MethodDef source;
+
+ public void implementsInterfaces(String names[]) {
+ MethodDef def = new MethodDef(0, clazz.getFQN(),
+ "<implements>", "()V");
+ for (String interfaceName : names) {
+ interfaceName = Clazz.internalToFqn(interfaceName);
+ for (Map.Entry<MethodDef, List<MethodDef>> entry : catalog
+ .entrySet()) {
+ String catalogClass = entry.getKey().clazz;
+ List<MethodDef> references = entry.getValue();
+
+ if (catalogClass.equals(interfaceName)) {
+ references.add(def);
+ }
+ }
+ }
+ }
+
+ // Method definitions
+ public void method(MethodDef source) {
+ this.source = source;
+ }
+
+ public void reference(MethodDef reference) {
+ List<MethodDef> references = catalog.get(reference);
+ if (references != null) {
+ references.add(source);
+ }
+ }
+ });
+ }
+ }
+
+ private static Map<MethodDef, List<MethodDef>> buildCatalog(
+ Collection<Clazz> sources) throws Exception {
+ final Map<MethodDef, List<MethodDef>> catalog = new TreeMap<MethodDef, List<MethodDef>>();
+ for (final Clazz clazz : sources) {
+ clazz.parseClassFileWithCollector(new ClassDataCollector() {
+
+ public boolean classStart(int access, String name) {
+ return clazz.isPublic();
+ }
+
+ public void method(MethodDef source) {
+ if (java.lang.reflect.Modifier.isPublic(source.access)
+ || Modifier.isProtected(source.access))
+ catalog.put(source, new ArrayList<MethodDef>());
+ }
+
+ });
+ }
+ return catalog;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/coverage/CoverageResource.java b/bundleplugin/src/main/java/aQute/bnd/make/coverage/CoverageResource.java
new file mode 100644
index 0000000..3827391
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/coverage/CoverageResource.java
@@ -0,0 +1,92 @@
+package aQute.bnd.make.coverage;
+
+import static aQute.bnd.make.coverage.Coverage.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.*;
+import aQute.lib.tag.*;
+
+/**
+ * Creates an XML Coverage report. This class can be used as a resource so the
+ * report is created only when the JAR is written.
+ *
+ */
+public class CoverageResource extends WriteResource {
+ Collection<Clazz> testsuite;
+ Collection<Clazz> service;
+
+ public CoverageResource(Collection<Clazz> testsuite,
+ Collection<Clazz> service) {
+ this.testsuite = testsuite;
+ this.service = service;
+ }
+
+ @Override
+ public long lastModified() {
+ return 0;
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ try {
+ Map<MethodDef, List<MethodDef>> table = getCrossRef(testsuite,
+ service);
+ Tag coverage = toTag(table);
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(out,
+ Constants.DEFAULT_CHARSET));
+ try {
+ coverage.print(0, pw);
+ } finally {
+ pw.flush();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static Tag toTag(Map<MethodDef, List<MethodDef>> catalog) {
+ Tag coverage = new Tag("coverage");
+ String currentClass = null;
+ Tag classTag = null;
+
+ for (Map.Entry<MethodDef, List<MethodDef>> m : catalog.entrySet()) {
+ String className = m.getKey().clazz;
+ if (!className.equals(currentClass)) {
+ classTag = new Tag("class");
+ classTag.addAttribute("name", className);
+ classTag.addAttribute("package", Clazz.getPackage(className));
+ classTag.addAttribute("short", Clazz.getShortName(className));
+ coverage.addContent(classTag);
+ currentClass = className;
+ }
+ Tag method = doMethod(new Tag("method"), m.getKey());
+ classTag.addContent(method);
+ for (MethodDef r : m.getValue()) {
+ Tag ref = doMethod(new Tag("ref"), r);
+ method.addContent(ref);
+ }
+ }
+ return coverage;
+ }
+
+ private static Tag doMethod(Tag tag, MethodDef method) {
+ tag.addAttribute("pretty", method.getPretty());
+ if (Modifier.isPublic(method.access))
+ tag.addAttribute("public", true);
+ if (Modifier.isStatic(method.access))
+ tag.addAttribute("static", true);
+ if (Modifier.isProtected(method.access))
+ tag.addAttribute("protected", true);
+ if (Modifier.isInterface(method.access))
+ tag.addAttribute("interface", true);
+ tag.addAttribute("constructor", method.isConstructor());
+ if (!method.isConstructor())
+ tag.addAttribute("name", method.name);
+ tag.addAttribute("descriptor", method.descriptor);
+ return tag;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetaTypeReader.java b/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetaTypeReader.java
new file mode 100644
index 0000000..94a65ae
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetaTypeReader.java
@@ -0,0 +1,341 @@
+package aQute.bnd.make.metatype;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.regex.*;
+
+import aQute.bnd.annotation.metatype.*;
+import aQute.lib.io.*;
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.MethodDef;
+import aQute.lib.tag.*;
+import aQute.libg.generics.*;
+
+public class MetaTypeReader extends ClassDataCollector implements Resource {
+ final Analyzer reporter;
+ Clazz clazz;
+ String interfaces[];
+ Tag metadata = new Tag("metatype:MetaData", new String[] {
+ "xmlns:metatype", "http://www.osgi.org/xmlns/metatype/v1.1.0" });
+ Tag ocd = new Tag(metadata, "OCD");
+ Tag designate = new Tag(metadata, "Designate");
+ Tag object = new Tag(designate, "Object");
+
+ // Resource
+ String extra;
+
+ // One time init
+ boolean finished;
+
+ // Designate
+ boolean override;
+ String designatePid;
+ boolean factory;
+
+ // AD
+ Map<MethodDef, Meta.AD> methods = new LinkedHashMap<MethodDef, Meta.AD>();
+
+ // OCD
+ Annotation ocdAnnotation;
+
+ MethodDef method;
+
+ public MetaTypeReader(Clazz clazz, Analyzer reporter) {
+ this.clazz = clazz;
+ this.reporter = reporter;
+ }
+
+ public void annotation(Annotation annotation) {
+ try {
+ Meta.OCD ocd = annotation.getAnnotation(Meta.OCD.class);
+ Meta.AD ad = annotation.getAnnotation(Meta.AD.class);
+ if (ocd != null) {
+ this.ocdAnnotation = annotation;
+ }
+ if (ad != null) {
+ assert method != null;
+ methods.put(method, ad);
+ }
+ } catch (Exception e) {
+ reporter.error("Error during annotation parsing %s : %s", clazz, e);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @param id
+ * @param name
+ * @param cardinality
+ * @param required
+ * @param deflt
+ * @param type
+ * @param max
+ * @param min
+ * @param optionLabels
+ * @param optionValues
+ */
+
+ static Pattern COLLECTION = Pattern
+ .compile("(.*(Collection|Set|List|Queue|Stack|Deque))<(L.+;)>");
+
+ private void addMethod(MethodDef method, Meta.AD ad) throws Exception {
+
+ // Set all the defaults.
+ String rtype = method.getReturnType();
+ String id = Configurable.mangleMethodName(method.name);
+ String name = Clazz.unCamel(id);
+
+ int cardinality = 0;
+
+ if (rtype.endsWith("[]")) {
+ cardinality = Integer.MAX_VALUE;
+ rtype = rtype.substring(0, rtype.length() - 2);
+ }
+ if (rtype.indexOf('<') > 0) {
+ if (cardinality != 0)
+ reporter.error(
+ "AD for %s.%s uses an array of collections in return type (%s), Metatype allows either Vector or array",
+ clazz.getFQN(), method.name, method.getReturnType());
+ Matcher m = COLLECTION.matcher(rtype);
+ if (m.matches()) {
+ rtype = Clazz.objectDescriptorToFQN(m.group(3));
+ cardinality = Integer.MIN_VALUE;
+ }
+ }
+
+ Meta.Type type = getType(rtype);
+
+ boolean required = ad ==null || ad.required();
+ String deflt = null;
+ String max = null;
+ String min = null;
+ String[] optionLabels = null;
+ String[] optionValues = null;
+ String description = null;
+
+ Clazz c = reporter.findClass(Clazz.fqnToPath(rtype));
+ if (c != null && c.isEnum()) {
+ optionValues = parseOptionValues(c);
+ }
+
+ // Now parse the annotation for any overrides
+
+ if (ad != null) {
+ if (ad.id() != null)
+ id = ad.id();
+ if (ad.name() != null)
+ name = ad.name();
+ if (ad.cardinality() != 0)
+ cardinality = ad.cardinality();
+ if (ad.type() != null)
+ type = ad.type();
+ if (ad.required() || ad.deflt() == null)
+ required = true;
+
+ if (ad.description() != null)
+ description = ad.description();
+
+ if (ad.optionLabels() != null)
+ optionLabels = ad.optionLabels();
+ if (ad.optionValues() != null )
+ optionValues = ad.optionValues();
+
+ if (ad.min() != null)
+ min = ad.min();
+ if (ad.max() != null)
+ max = ad.max();
+
+ if (ad.deflt() != null)
+ deflt = ad.deflt();
+ }
+
+ if (optionValues != null) {
+ if (optionLabels == null || optionLabels.length == 0) {
+ optionLabels = new String[optionValues.length];
+ for (int i = 0; i < optionValues.length; i++)
+ optionLabels[i] = Clazz.unCamel(optionValues[i]);
+ }
+
+ if (optionLabels.length != optionValues.length) {
+ reporter.error("Option labels and option values not the same length for %s", id);
+ optionLabels = optionValues;
+ }
+ }
+
+ Tag adt = new Tag(this.ocd, "AD");
+ adt.addAttribute("name", name);
+ adt.addAttribute("id", id);
+ adt.addAttribute("cardinality", cardinality);
+ adt.addAttribute("required", required);
+ adt.addAttribute("default", deflt);
+ adt.addAttribute("type", type);
+ adt.addAttribute("max", max);
+ adt.addAttribute("min", min);
+ adt.addAttribute("description", description);
+
+ if (optionLabels != null) {
+ for (int i = 0; i < optionLabels.length; i++) {
+ Tag option = new Tag(adt, "Option");
+ option.addAttribute("label", optionLabels[i]);
+ option.addAttribute("value", optionValues[i]);
+ }
+ }
+ }
+
+ private String[] parseOptionValues(Clazz c) throws Exception {
+ final List<String> values = Create.list();
+
+ c.parseClassFileWithCollector(new ClassDataCollector() {
+ public void field(Clazz.FieldDef def) {
+ if (def.isEnum()) {
+ values.add(def.name);
+ }
+ }
+ });
+ return values.toArray(new String[values.size()]);
+ }
+
+ Meta.Type getType(String rtype) {
+ if (rtype.endsWith("[]")) {
+ rtype = rtype.substring(0, rtype.length() - 2);
+ if (rtype.endsWith("[]"))
+ throw new IllegalArgumentException("Can only handle array of depth one");
+ }
+
+ if ("boolean".equals(rtype) || Boolean.class.getName().equals(rtype))
+ return Meta.Type.Boolean;
+ else if ("byte".equals(rtype) || Byte.class.getName().equals(rtype))
+ return Meta.Type.Byte;
+ else if ("char".equals(rtype) || Character.class.getName().equals(rtype))
+ return Meta.Type.Character;
+ else if ("short".equals(rtype) || Short.class.getName().equals(rtype))
+ return Meta.Type.Short;
+ else if ("int".equals(rtype) || Integer.class.getName().equals(rtype))
+ return Meta.Type.Integer;
+ else if ("long".equals(rtype) || Long.class.getName().equals(rtype))
+ return Meta.Type.Long;
+ else if ("float".equals(rtype) || Float.class.getName().equals(rtype))
+ return Meta.Type.Float;
+ else if ("double".equals(rtype) || Double.class.getName().equals(rtype))
+ return Meta.Type.Double;
+ else
+ return Meta.Type.String;
+ }
+
+ @Override public void method(MethodDef mdef) {
+ method = mdef;
+ methods.put(mdef, null);
+ }
+
+ public String getExtra() {
+ return extra;
+ }
+
+ public long lastModified() {
+ return 0;
+ }
+
+ public InputStream openInputStream() throws IOException {
+ final PipedInputStream pin = new PipedInputStream();
+ final PipedOutputStream pout = new PipedOutputStream(pin);
+ getExecutor().execute(new Runnable() {
+ public void run() {
+ try {
+ write(pout);
+ } catch (IOException e) {
+ // Cause an exception in the other end
+ IO.close(pin);
+ }
+ IO.close(pout);
+ }
+ });
+ return pin;
+ }
+
+ private Executor getExecutor() {
+ return reporter.getPlugin(Executor.class);
+ }
+
+ public void setExtra(String extra) {
+ this.extra = extra;
+ }
+
+ public void write(OutputStream out) throws IOException {
+ try {
+ finish();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
+ pw.println("<?xml version='1.0'?>");
+ metadata.print(0, pw);
+ pw.flush();
+ }
+
+ void finish() throws Exception {
+ if (!finished) {
+ finished = true;
+ clazz.parseClassFileWithCollector(this);
+ Meta.OCD ocd = null;
+ if (this.ocdAnnotation != null)
+ ocd = this.ocdAnnotation.getAnnotation(Meta.OCD.class);
+ else
+ ocd = Configurable.createConfigurable(Meta.OCD.class,
+ new HashMap<String, Object>());
+
+ // defaults
+ String id = clazz.getFQN();
+ String name = Clazz.unCamel(Clazz.getShortName(clazz.getFQN()));
+ String description = null;
+ String localization = id;
+ boolean factory = this.factory;
+
+ if (ocd.id() != null)
+ id = ocd.id();
+
+
+ if (ocd.name() != null)
+ name = ocd.name();
+
+ if (ocd.localization() != null)
+ localization = ocd.localization();
+
+ if (ocd.description() != null)
+ description = ocd.description();
+
+ String pid = id;
+ if (override) {
+ pid = this.designatePid;
+ factory = this.factory;
+ id = this.designatePid; // for the felix problems
+ } else {
+ if (ocdAnnotation.get("factory") != null) {
+ factory = true;
+ }
+ }
+
+ this.ocd.addAttribute("name", name);
+ this.ocd.addAttribute("id", id);
+ this.ocd.addAttribute("description", description);
+ this.ocd.addAttribute("localization", localization);
+
+ // do ADs
+ for (Map.Entry<MethodDef, Meta.AD> entry : methods.entrySet())
+ addMethod(entry.getKey(), entry.getValue());
+
+ this.designate.addAttribute("pid", pid);
+ if (factory)
+ this.designate.addAttribute("factoryPid", pid);
+
+ this.object.addAttribute("ocdref", id);
+
+ }
+ }
+
+ public void setDesignate(String pid, boolean factory) {
+ this.override = true;
+ this.factory = factory;
+ this.designatePid = pid;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetatypePlugin.java b/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetatypePlugin.java
new file mode 100644
index 0000000..4801ec9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/make/metatype/MetatypePlugin.java
@@ -0,0 +1,35 @@
+package aQute.bnd.make.metatype;
+
+import java.util.*;
+
+import aQute.bnd.annotation.metatype.*;
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.*;
+
+/**
+ * This class is responsible for meta type types. It is a plugin that can
+ * @author aqute
+ *
+ */
+public class MetatypePlugin implements AnalyzerPlugin {
+
+ public boolean analyzeJar(Analyzer analyzer) throws Exception {
+
+ Map<String, Map<String, String>> map = analyzer.parseHeader(analyzer
+ .getProperty(Constants.METATYPE));
+
+ Jar jar = analyzer.getJar();
+ for (String name : map.keySet()) {
+ Collection<Clazz> metatypes = analyzer.getClasses("", QUERY.ANNOTATION.toString(),
+ Meta.OCD.class.getName(), //
+ QUERY.NAMED.toString(), name //
+ );
+ for (Clazz c : metatypes) {
+ jar.putResource("OSGI-INF/metatype/" + c.getFQN() + ".xml", new MetaTypeReader(c,
+ analyzer));
+ }
+ }
+ return false;
+ }
+}