| 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(); |
| } |
| |
| } |