blob: 652d3679a93296386e53c5fc8c2354ee87caf6ed [file] [log] [blame]
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("&lt;");
break;
case '>':
sb.append("&gt;");
break;
case '&':
sb.append("&amp;");
break;
case '\'':
sb.append("&quot;");
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}
}