blob: 652d3679a93296386e53c5fc8c2354ee87caf6ed [file] [log] [blame]
Stuart McCulloch26e7a5a2011-10-17 10:31:43 +00001package aQute.bnd.make.component;
2
3import java.io.*;
4import java.util.*;
5import java.util.regex.*;
6
7import aQute.bnd.annotation.component.*;
8import aQute.bnd.make.metatype.*;
9import aQute.bnd.service.*;
10import aQute.lib.osgi.*;
11import aQute.lib.osgi.Clazz.*;
12import aQute.libg.version.*;
13
14/**
15 * This class is an analyzer plugin. It looks at the properties and tries to
16 * find out if the Service-Component header contains the bnd shortut syntax. If
17 * not, the header is copied to the output, if it does, an XML file is created
18 * and added to the JAR and the header is modified appropriately.
19 */
20public class ServiceComponent implements AnalyzerPlugin {
21 public final static String NAMESPACE_STEM = "http://www.osgi.org/xmlns/scr";
22 public final static String JIDENTIFIER = "<<identifier>>";
23 public final static String COMPONENT_NAME = "name:";
24 public final static String COMPONENT_FACTORY = "factory:";
25 public final static String COMPONENT_SERVICEFACTORY = "servicefactory:";
26 public final static String COMPONENT_IMMEDIATE = "immediate:";
27 public final static String COMPONENT_ENABLED = "enabled:";
28 public final static String COMPONENT_DYNAMIC = "dynamic:";
29 public final static String COMPONENT_MULTIPLE = "multiple:";
30 public final static String COMPONENT_PROVIDE = "provide:";
31 public final static String COMPONENT_OPTIONAL = "optional:";
32 public final static String COMPONENT_PROPERTIES = "properties:";
33 public final static String COMPONENT_IMPLEMENTATION = "implementation:";
34 public final static String COMPONENT_DESIGNATE = "designate:";
35 public final static String COMPONENT_DESIGNATEFACTORY = "designateFactory:";
36 public final static String COMPONENT_DESCRIPTORS = ".descriptors:";
37
38 // v1.1.0
39 public final static String COMPONENT_VERSION = "version:";
40 public final static String COMPONENT_CONFIGURATION_POLICY = "configuration-policy:";
41 public final static String COMPONENT_MODIFIED = "modified:";
42 public final static String COMPONENT_ACTIVATE = "activate:";
43 public final static String COMPONENT_DEACTIVATE = "deactivate:";
44
45 final static Map<String, String> EMPTY = Collections.emptyMap();
46
47 public final static String[] componentDirectives = new String[] {
48 COMPONENT_FACTORY, COMPONENT_IMMEDIATE, COMPONENT_ENABLED, COMPONENT_DYNAMIC,
49 COMPONENT_MULTIPLE, COMPONENT_PROVIDE, COMPONENT_OPTIONAL, COMPONENT_PROPERTIES,
50 COMPONENT_IMPLEMENTATION, COMPONENT_SERVICEFACTORY, COMPONENT_VERSION,
51 COMPONENT_CONFIGURATION_POLICY, COMPONENT_MODIFIED, COMPONENT_ACTIVATE,
52 COMPONENT_DEACTIVATE, COMPONENT_NAME, COMPONENT_DESCRIPTORS, COMPONENT_DESIGNATE,
53 COMPONENT_DESIGNATEFACTORY };
54
55 public final static Set<String> SET_COMPONENT_DIRECTIVES = new HashSet<String>(
56 Arrays.asList(componentDirectives));
57
58 public final static Set<String> SET_COMPONENT_DIRECTIVES_1_1 = //
59 new HashSet<String>(
60 Arrays.asList(
61 COMPONENT_VERSION,
62 COMPONENT_CONFIGURATION_POLICY,
63 COMPONENT_MODIFIED,
64 COMPONENT_ACTIVATE,
65 COMPONENT_DEACTIVATE));
66
67 public boolean analyzeJar(Analyzer analyzer) throws Exception {
68
69 ComponentMaker m = new ComponentMaker(analyzer);
70
71 Map<String, Map<String, String>> l = m.doServiceComponent();
72
73 analyzer.setProperty(Constants.SERVICE_COMPONENT, Processor.printClauses(l));
74
75 analyzer.getInfo(m, "Service-Component: ");
76 m.close();
77
78 return false;
79 }
80
81 private static class ComponentMaker extends Processor {
82 Analyzer analyzer;
83
84 ComponentMaker(Analyzer analyzer) {
85 super(analyzer);
86 this.analyzer = analyzer;
87 }
88
89 /**
90 * Iterate over the Service Component entries. There are two cases:
91 * <ol>
92 * <li>An XML file reference</li>
93 * <li>A FQN/wildcard with a set of attributes</li>
94 * </ol>
95 *
96 * An XML reference is immediately expanded, an FQN/wildcard is more
97 * complicated and is delegated to
98 * {@link #componentEntry(Map, String, Map)}.
99 *
100 * @throws Exception
101 */
102 Map<String, Map<String, String>> doServiceComponent() throws Exception {
103 Map<String, Map<String, String>> serviceComponents = newMap();
104 String header = getProperty(SERVICE_COMPONENT);
105 Map<String, Map<String, String>> sc = parseHeader(header);
106
107 for (Map.Entry<String, Map<String, String>> entry : sc.entrySet()) {
108 String name = entry.getKey();
109 Map<String, String> info = entry.getValue();
110
111 try {
112 if (name.indexOf('/') >= 0 || name.endsWith(".xml")) {
113 // Normal service component, we do not process it
114 serviceComponents.put(name, EMPTY);
115 } else {
116 componentEntry(serviceComponents, name, info);
117 }
118 } catch (Exception e) {
119 e.printStackTrace();
120 error("Invalid Service-Component header: %s %s, throws %s", name, info, e);
121 }
122 }
123 return serviceComponents;
124 }
125
126 /**
127 * Parse an entry in the Service-Component header. This header supports
128 * the following types:
129 * <ol>
130 * <li>An FQN + attributes describing a component</li>
131 * <li>A wildcard expression for finding annotated components.</li>
132 * </ol>
133 * The problem is the distinction between an FQN and a wildcard because
134 * an FQN can also be used as a wildcard.
135 *
136 * If the info specifies {@link Constants#NOANNOTATIONS} then wildcards
137 * are an error and the component must be fully described by the info.
138 * Otherwise the FQN/wildcard is expanded into a list of classes with
139 * annotations. If this list is empty, the FQN case is interpreted as a
140 * complete component definition. For the wildcard case, it is checked
141 * if any matching classes for the wildcard have been compiled for a
142 * class file format that does not support annotations, this can be a
143 * problem with JSR14 who silently ignores annotations. An error is
144 * reported in such a case.
145 *
146 * @param serviceComponents
147 * @param name
148 * @param info
149 * @throws Exception
150 * @throws IOException
151 */
152 private void componentEntry(Map<String, Map<String, String>> serviceComponents,
153 String name, Map<String, String> info) throws Exception, IOException {
154
155 boolean annotations = !Processor.isTrue(info.get(NOANNOTATIONS));
156 boolean fqn = Verifier.isFQN(name);
157
158 if (annotations) {
159
160 // Annotations possible!
161
162 Collection<Clazz> annotatedComponents = analyzer.getClasses("",
163 QUERY.ANNOTATION.toString(), Component.class.getName(), //
164 QUERY.NAMED.toString(), name //
165 );
166
167 if (fqn) {
168 if (annotatedComponents.isEmpty()) {
169
170 // No annotations, fully specified in header
171
172 createComponentResource(serviceComponents, name, info);
173 } else {
174
175 // We had a FQN so expect only one
176
177 for (Clazz c : annotatedComponents) {
178 annotated(serviceComponents, c, info);
179 }
180 }
181 } else {
182
183 // We did not have an FQN, so expect the use of wildcards
184
185 if (annotatedComponents.isEmpty())
186 checkAnnotationsFeasible(name);
187 else
188 for (Clazz c : annotatedComponents) {
189 annotated(serviceComponents, c, info);
190 }
191 }
192 } else {
193 // No annotations
194 if (fqn)
195 createComponentResource(serviceComponents, name, info);
196 else
197 error("Set to %s but entry %s is not an FQN ", NOANNOTATIONS, name);
198
199 }
200 }
201
202 /**
203 * Check if annotations are actually feasible looking at the class
204 * format. If the class format does not provide annotations then it is
205 * no use specifying annotated components.
206 *
207 * @param name
208 * @return
209 * @throws Exception
210 */
211 private Collection<Clazz> checkAnnotationsFeasible(String name) throws Exception {
212 Collection<Clazz> not = analyzer.getClasses("", QUERY.NAMED.toString(), name //
213 );
214
215 if (not.isEmpty())
216 if ( "*".equals(name))
217 return not;
218 else
219 error("Specified %s but could not find any class matching this pattern", name);
220
221 for (Clazz c : not) {
222 if (c.getFormat().hasAnnotations())
223 return not;
224 }
225
226 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",
227 name);
228
229 return not;
230 }
231
232 void annotated(Map<String, Map<String, String>> components, Clazz c,
233 Map<String, String> info) throws Exception {
234 // Get the component definition
235 // from the annotations
236 Map<String, String> map = ComponentAnnotationReader.getDefinition(c, this);
237
238 // Pick the name, the annotation can override
239 // the name.
240 String localname = map.get(COMPONENT_NAME);
241 if (localname == null)
242 localname = c.getFQN();
243
244 // Override the component info without manifest
245 // entries. We merge the properties though.
246
247 String merged = Processor.merge(info.remove(COMPONENT_PROPERTIES),
248 map.remove(COMPONENT_PROPERTIES));
249 if (merged != null && merged.length() > 0)
250 map.put(COMPONENT_PROPERTIES, merged);
251 map.putAll(info);
252 createComponentResource(components, localname, map);
253 }
254
255 private void createComponentResource(Map<String, Map<String, String>> components,
256 String name, Map<String, String> info) throws IOException {
257
258 // We can override the name in the parameters
259 if (info.containsKey(COMPONENT_NAME))
260 name = info.get(COMPONENT_NAME);
261
262 // Assume the impl==name, but allow override
263 String impl = name;
264 if (info.containsKey(COMPONENT_IMPLEMENTATION))
265 impl = info.get(COMPONENT_IMPLEMENTATION);
266
267 // Check if such a class exists
268 analyzer.referTo(impl);
269
270 boolean designate = designate(name, info.get(COMPONENT_DESIGNATE), false)
271 || designate(name, info.get(COMPONENT_DESIGNATEFACTORY), true);
272
273 // If we had a designate, we want a default configuration policy of
274 // require.
275 if (designate && info.get(COMPONENT_CONFIGURATION_POLICY) == null)
276 info.put(COMPONENT_CONFIGURATION_POLICY, "require");
277
278 // We have a definition, so make an XML resources
279 Resource resource = createComponentResource(name, impl, info);
280 analyzer.getJar().putResource("OSGI-INF/" + name + ".xml", resource);
281
282 components.put("OSGI-INF/" + name + ".xml", EMPTY);
283
284 }
285
286 /**
287 * Create a Metatype and Designate record out of the given
288 * configurations.
289 *
290 * @param name
291 * @param config
292 */
293 private boolean designate(String name, String config, boolean factory) {
294 if (config == null)
295 return false;
296
297 for (String c : Processor.split(config)) {
298 Clazz clazz = analyzer.getClassspace().get(Clazz.fqnToPath(c));
299 if (clazz != null) {
300 analyzer.referTo(c);
301 MetaTypeReader r = new MetaTypeReader(clazz, analyzer);
302 r.setDesignate(name, factory);
303 String rname = "OSGI-INF/metatype/" + name + ".xml";
304
305 analyzer.getJar().putResource(rname, r);
306 } else {
307 analyzer.error(
308 "Cannot find designated configuration class %s for component %s", c,
309 name);
310 }
311 }
312 return true;
313 }
314
315 /**
316 * Create the resource for a DS component.
317 *
318 * @param list
319 * @param name
320 * @param info
321 * @throws UnsupportedEncodingException
322 */
323 Resource createComponentResource(String name, String impl, Map<String, String> info)
324 throws IOException {
325 String namespace = getNamespace(info);
326 ByteArrayOutputStream out = new ByteArrayOutputStream();
327 PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, Constants.DEFAULT_CHARSET));
328 pw.println("<?xml version='1.0' encoding='utf-8'?>");
329 if (namespace != null)
330 pw.print("<scr:component xmlns:scr='" + namespace + "'");
331 else
332 pw.print("<component");
333
334 doAttribute(pw, name, "name");
335 doAttribute(pw, info.get(COMPONENT_FACTORY), "factory");
336 doAttribute(pw, info.get(COMPONENT_IMMEDIATE), "immediate", "false", "true");
337 doAttribute(pw, info.get(COMPONENT_ENABLED), "enabled", "true", "false");
338 doAttribute(pw, info.get(COMPONENT_CONFIGURATION_POLICY), "configuration-policy",
339 "optional", "require", "ignore");
340 doAttribute(pw, info.get(COMPONENT_ACTIVATE), "activate", JIDENTIFIER);
341 doAttribute(pw, info.get(COMPONENT_DEACTIVATE), "deactivate", JIDENTIFIER);
342 doAttribute(pw, info.get(COMPONENT_MODIFIED), "modified", JIDENTIFIER);
343
344 pw.println(">");
345
346 // Allow override of the implementation when people
347 // want to choose their own name
348 pw.println(" <implementation class='" + (impl == null ? name : impl) + "'/>");
349
350 String provides = info.get(COMPONENT_PROVIDE);
351 boolean servicefactory = Processor.isTrue(info.get(COMPONENT_SERVICEFACTORY));
352
353 if (servicefactory && Processor.isTrue(info.get(COMPONENT_IMMEDIATE))) {
354 // TODO can become error() if it is up to me
355 warning("For a Service Component, the immediate option and the servicefactory option are mutually exclusive for %(%s)",
356 name, impl);
357 }
358 provide(pw, provides, servicefactory, impl);
359 properties(pw, info);
360 reference(info, pw);
361
362 if (namespace != null)
363 pw.println("</scr:component>");
364 else
365 pw.println("</component>");
366
367 pw.close();
368 byte[] data = out.toByteArray();
369 out.close();
370 return new EmbeddedResource(data, 0);
371 }
372
373 private void doAttribute(PrintWriter pw, String value, String name, String... matches) {
374 if (value != null) {
375 if (matches.length != 0) {
376 if (matches.length == 1 && matches[0].equals(JIDENTIFIER)) {
377 if (!Verifier.isIdentifier(value))
378 error("Component attribute %s has value %s but is not a Java identifier",
379 name, value);
380 } else {
381
382 if (!Verifier.isMember(value, matches))
383 error("Component attribute %s has value %s but is not a member of %s",
384 name, value, Arrays.toString(matches));
385 }
386 }
387 pw.print(" ");
388 pw.print(name);
389 pw.print("='");
390 pw.print(value);
391 pw.print("'");
392 }
393 }
394
395 /**
396 * Check if we need to use the v1.1 namespace (or later).
397 *
398 * @param info
399 * @return
400 */
401 private String getNamespace(Map<String, String> info) {
402 String version = info.get(COMPONENT_VERSION);
403 if (version != null) {
404 try {
405 Version v = new Version(version);
406 return NAMESPACE_STEM + "/v" + v;
407 } catch (Exception e) {
408 error("version: specified on component header but not a valid version: "
409 + version);
410 return null;
411 }
412 }
413 for (String key : info.keySet()) {
414 if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) {
415 return NAMESPACE_STEM + "/v1.1.0";
416 }
417 }
418 return null;
419 }
420
421 /**
422 * Print the Service-Component properties element
423 *
424 * @param pw
425 * @param info
426 */
427 void properties(PrintWriter pw, Map<String, String> info) {
428 Collection<String> properties = split(info.get(COMPONENT_PROPERTIES));
429 for (Iterator<String> p = properties.iterator(); p.hasNext();) {
430 String clause = p.next();
431 int n = clause.indexOf('=');
432 if (n <= 0) {
433 error("Not a valid property in service component: " + clause);
434 } else {
435 String type = null;
436 String name = clause.substring(0, n);
437 if (name.indexOf('@') >= 0) {
438 String parts[] = name.split("@");
439 name = parts[1];
440 type = parts[0];
441 } else if (name.indexOf(':') >= 0) {
442 String parts[] = name.split(":");
443 name = parts[0];
444 type = parts[1];
445 }
446 String value = clause.substring(n + 1).trim();
447 // TODO verify validity of name and value.
448 pw.print(" <property name='");
449 pw.print(name);
450 pw.print("'");
451
452 if (type != null) {
453 if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
454 pw.print(" type='");
455 pw.print(type);
456 pw.print("'");
457 } else {
458 warning("Invalid property type '" + type + "' for property " + name);
459 }
460 }
461
462 String parts[] = value.split("\\s*(\\||\\n)\\s*");
463 if (parts.length > 1) {
464 pw.println(">");
465 for (String part : parts) {
466 pw.println(part);
467 }
468 pw.println("</property>");
469 } else {
470 pw.print(" value='");
471 pw.print(parts[0]);
472 pw.println("'/>");
473 }
474 }
475 }
476 }
477
478 /**
479 * @param pw
480 * @param provides
481 */
482 void provide(PrintWriter pw, String provides, boolean servicefactory, String impl) {
483 if (provides != null) {
484 if (!servicefactory)
485 pw.println(" <service>");
486 else
487 pw.println(" <service servicefactory='true'>");
488
489 StringTokenizer st = new StringTokenizer(provides, ",");
490 while (st.hasMoreTokens()) {
491 String interfaceName = st.nextToken();
492 pw.println(" <provide interface='" + interfaceName + "'/>");
493 analyzer.referTo(interfaceName);
494
495 // TODO verifies the impl. class extends or implements the
496 // interface
497 }
498 pw.println(" </service>");
499 } else if (servicefactory)
500 warning("The servicefactory:=true directive is set but no service is provided, ignoring it");
501 }
502
503 public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
504
505 /**
506 * @param info
507 * @param pw
508 */
509
510 void reference(Map<String, String> info, PrintWriter pw) {
511 Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC)));
512 Collection<String> optional = new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL)));
513 Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE)));
514
515 Collection<String> descriptors = split(info.get(COMPONENT_DESCRIPTORS));
516
517 for (Map.Entry<String, String> entry : info.entrySet()) {
518
519 // Skip directives
520 String referenceName = entry.getKey();
521 if (referenceName.endsWith(":")) {
522 if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
523 error("Unrecognized directive in Service-Component header: "
524 + referenceName);
525 continue;
526 }
527
528 // Parse the bind/unbind methods from the name
529 // if set. They are separated by '/'
530 String bind = null;
531 String unbind = null;
532
533 boolean unbindCalculated = false;
534
535 if (referenceName.indexOf('/') >= 0) {
536 String parts[] = referenceName.split("/");
537 referenceName = parts[0];
538 bind = parts[1];
539 if (parts.length > 2) {
540 unbind = parts[2];
541 } else {
542 unbindCalculated = true;
543 if (bind.startsWith("add"))
544 unbind = bind.replaceAll("add(.+)", "remove$1");
545 else
546 unbind = "un" + bind;
547 }
548 } else if (Character.isLowerCase(referenceName.charAt(0))) {
549 unbindCalculated = true;
550 bind = "set" + Character.toUpperCase(referenceName.charAt(0))
551 + referenceName.substring(1);
552 unbind = "un" + bind;
553 }
554
555 String interfaceName = entry.getValue();
556 if (interfaceName == null || interfaceName.length() == 0) {
557 error("Invalid Interface Name for references in Service Component: "
558 + referenceName + "=" + interfaceName);
559 continue;
560 }
561
562 // If we have descriptors, we have analyzed the component.
563 // So why not check the methods
564 if (descriptors.size() > 0) {
565 // Verify that the bind method exists
566 if (!descriptors.contains(bind))
567 error("The bind method %s for %s not defined", bind, referenceName);
568
569 // Check if the unbind method exists
570 if (!descriptors.contains(unbind)) {
571 if (unbindCalculated)
572 // remove it
573 unbind = null;
574 else
575 error("The unbind method %s for %s not defined", unbind, referenceName);
576 }
577 }
578 // Check tje cardinality by looking at the last
579 // character of the value
580 char c = interfaceName.charAt(interfaceName.length() - 1);
581 if ("?+*~".indexOf(c) >= 0) {
582 if (c == '?' || c == '*' || c == '~')
583 optional.add(referenceName);
584 if (c == '+' || c == '*')
585 multiple.add(referenceName);
586 if (c == '+' || c == '*' || c == '?')
587 dynamic.add(referenceName);
588 interfaceName = interfaceName.substring(0, interfaceName.length() - 1);
589 }
590
591 // Parse the target from the interface name
592 // The target is a filter.
593 String target = null;
594 Matcher m = REFERENCE.matcher(interfaceName);
595 if (m.matches()) {
596 interfaceName = m.group(1);
597 target = m.group(2);
598 }
599
600 analyzer.referTo(interfaceName);
601
602 pw.printf(" <reference name='%s'", referenceName);
603 pw.printf(" interface='%s'", interfaceName);
604
605 String cardinality = optional.contains(referenceName) ? "0" : "1";
606 cardinality += "..";
607 cardinality += multiple.contains(referenceName) ? "n" : "1";
608 if (!cardinality.equals("1..1"))
609 pw.print(" cardinality='" + cardinality + "'");
610
611 if (bind != null) {
612 pw.printf(" bind='%s'", bind);
613 if (unbind != null) {
614 pw.printf(" unbind='%s'", unbind);
615 }
616 }
617
618 if (dynamic.contains(referenceName)) {
619 pw.print(" policy='dynamic'");
620 }
621
622 if (target != null) {
623 // Filter filter = new Filter(target);
624 // if (filter.verify() == null)
625 // pw.print(" target='" + filter.toString() + "'");
626 // else
627 // error("Target for " + referenceName
628 // + " is not a correct filter: " + target + " "
629 // + filter.verify());
630 pw.print(" target='" + escape(target) + "'");
631 }
632 pw.println("/>");
633 }
634 }
635 }
636
637 /**
638 * Escape a string, do entity conversion.
639 */
640 static String escape(String s) {
641 StringBuffer sb = new StringBuffer();
642 for (int i = 0; i < s.length(); i++) {
643 char c = s.charAt(i);
644 switch (c) {
645 case '<':
646 sb.append("&lt;");
647 break;
648 case '>':
649 sb.append("&gt;");
650 break;
651 case '&':
652 sb.append("&amp;");
653 break;
654 case '\'':
655 sb.append("&quot;");
656 break;
657 default:
658 sb.append(c);
659 break;
660 }
661 }
662 return sb.toString();
663 }
664
665}