blob: d69b0aad11fc2d5d63eb14fd40ff87f325bedfed [file] [log] [blame]
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +00001package aQute.bnd.component;
2
3import java.util.ArrayList;
4import java.util.Arrays;
5import java.util.Collection;
6import java.util.HashMap;
7import java.util.HashSet;
8import java.util.List;
9import java.util.Map;
10import java.util.Set;
11import java.util.StringTokenizer;
12import java.util.regex.Matcher;
13import java.util.regex.Pattern;
14
15import org.osgi.service.component.annotations.ConfigurationPolicy;
16import org.osgi.service.component.annotations.ReferenceCardinality;
17import org.osgi.service.component.annotations.ReferencePolicy;
18import org.osgi.service.component.annotations.ReferencePolicyOption;
19
20import aQute.bnd.osgi.*;
21import aQute.bnd.osgi.Clazz.MethodDef;
22import aQute.bnd.osgi.Descriptors.TypeRef;
23import aQute.lib.tag.Tag;
24import aQute.bnd.version.Version;
25
26public class HeaderReader extends Processor {
27 final static Pattern PROPERTY_PATTERN = Pattern
28 .compile("([^=]+([:@](Boolean|Byte|Char|Short|Integer|Long|Float|Double|String))?)\\s*=(.*)");
29 private final static Set<String> LIFECYCLE_METHODS = new HashSet<String>(Arrays.asList("activate", "deactivate", "modified"));
30
31 private final Analyzer analyzer;
32
33 private final static String ComponentContextTR = "org.osgi.service.component.ComponentContext";
34 private final static String BundleContextTR = "org.osgi.framework.BundleContext";
35 private final static String MapTR = Map.class.getName();
36 private final static String IntTR = int.class.getName();
Stuart McCullochb215bfd2012-09-06 18:28:06 +000037 final static Set<String> allowed = new HashSet<String>(Arrays.asList(ComponentContextTR, BundleContextTR, MapTR));
38 final static Set<String> allowedDeactivate = new HashSet<String>(Arrays.asList(ComponentContextTR, BundleContextTR, MapTR, IntTR));
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +000039
40 private final static String ServiceReferenceTR = "org.osgi.framework.ServiceReference";
41
42 public HeaderReader(Analyzer analyzer) {
43 this.analyzer = analyzer;
44 }
45
46 public Tag createComponentTag(String name, String impl, Map<String, String> info)
47 throws Exception {
48 final ComponentDef cd = new ComponentDef();
49 cd.name = name;
50 if (info.get(COMPONENT_ENABLED) != null)
51 cd.enabled = Boolean.valueOf(info.get(COMPONENT_ENABLED));
52 cd.factory = info.get(COMPONENT_FACTORY);
53 if (info.get(COMPONENT_IMMEDIATE) != null)
54 cd.immediate = Boolean.valueOf(info.get(COMPONENT_IMMEDIATE));
55 if (info.get(COMPONENT_CONFIGURATION_POLICY) != null)
56 cd.configurationPolicy = ConfigurationPolicy.valueOf(info.get(COMPONENT_CONFIGURATION_POLICY).toUpperCase());
57 cd.activate = checkIdentifier(COMPONENT_ACTIVATE, info.get(COMPONENT_ACTIVATE));
58 cd.deactivate = checkIdentifier(COMPONENT_DEACTIVATE, info.get(COMPONENT_DEACTIVATE));
59 cd.modified = checkIdentifier(COMPONENT_MODIFIED, info.get(COMPONENT_MODIFIED));
60
61 cd.implementation = analyzer.getTypeRefFromFQN(impl == null? name: impl);
62
63
64 String provides = info.get(COMPONENT_PROVIDE);
65 if (info.get(COMPONENT_SERVICEFACTORY) != null) {
66 if (provides != null)
67 cd.servicefactory = Boolean.valueOf(info.get(COMPONENT_SERVICEFACTORY));
68 else
69 warning("The servicefactory:=true directive is set but no service is provided, ignoring it");
70 }
71
72 if (cd.servicefactory != null && cd.servicefactory && cd.immediate != null && cd.immediate) {
73 // TODO can become error() if it is up to me
74 warning("For a Service Component, the immediate option and the servicefactory option are mutually exclusive for %(%s)",
75 name, impl);
76 }
77
78 //analyze the class for suitable methods.
79 final Map<String, MethodDef> lifecycleMethods = new HashMap<String, MethodDef>();
80 final Map<String, MethodDef> bindmethods = new HashMap<String, MethodDef>();
81 TypeRef typeRef = analyzer.getTypeRefFromFQN(impl);
82 Clazz clazz = analyzer.findClass(typeRef);
83 boolean privateAllowed = true;
84 boolean defaultAllowed = true;
85 String topPackage = typeRef.getPackageRef().getFQN();
86 while (clazz != null) {
87 final boolean pa = privateAllowed;
88 final boolean da = defaultAllowed;
89 final Map<String, MethodDef> classLifecyclemethods = new HashMap<String, MethodDef>();
90 final Map<String, MethodDef> classBindmethods = new HashMap<String, MethodDef>();
91
92 clazz.parseClassFileWithCollector(new ClassDataCollector() {
93
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +000094 @Override
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +000095 public void method(MethodDef md) {
96 Set<String> allowedParams = allowed;
97 String lifecycleName = null;
98
99 boolean isLifecycle = (cd.activate == null? "activate": cd.activate).equals(md.getName()) ||
100 md.getName().equals(cd.modified);
101 if (!isLifecycle && (cd.deactivate == null? "deactivate": cd.deactivate).equals(md.getName())) {
102 isLifecycle = true;
103 allowedParams = allowedDeactivate;
104 }
105 if (isLifecycle && !lifecycleMethods.containsKey(md.getName()) &&
106 (md.isPublic() ||
107 md.isProtected() ||
108 (md.isPrivate() && pa) ||
109 (!md.isPrivate()) && da) &&
110 isBetter(md, classLifecyclemethods.get(md.getName()), allowedParams)) {
111 classLifecyclemethods.put(md.getName(), md);
112 }
113 if (!bindmethods.containsKey(md.getName()) &&
114 (md.isPublic() ||
115 md.isProtected() ||
116 (md.isPrivate() && pa) ||
117 (!md.isPrivate()) && da) &&
118 isBetterBind(md, classBindmethods.get(md.getName()))) {
119 classBindmethods.put(md.getName(), md);
120 }
121 }
122
123 private boolean isBetter(MethodDef test, MethodDef existing, Set<String> allowedParams) {
124 int testRating = rateLifecycle(test, allowedParams);
125 if (existing == null)
126 return testRating < 6;// ignore invalid methods
127 if (testRating < rateLifecycle(existing, allowedParams))
128 return true;
129
130 return false;
131 }
132
133 private boolean isBetterBind(MethodDef test, MethodDef existing) {
134 int testRating = rateBind(test);
135 if (existing == null)
136 return testRating < 6;// ignore invalid methods
137 if (testRating < rateBind(existing))
138 return true;
139
140 return false;
141 }
142
143 });
144 lifecycleMethods.putAll(classLifecyclemethods);
145 bindmethods.putAll(classBindmethods);
146 typeRef = clazz.getSuper();
147 if (typeRef == null)
148 break;
149 clazz = analyzer.findClass(typeRef);
150 privateAllowed = false;
151 defaultAllowed = defaultAllowed && topPackage.equals(typeRef.getPackageRef().getFQN());
152 }
153
154
155 if (cd.activate != null && !lifecycleMethods.containsKey(cd.activate)) {
156 error("in component %s, activate method %s specified but not found", cd.implementation.getFQN(), cd.activate);
157 cd.activate = null;
158 }
159 if (cd.deactivate != null && !lifecycleMethods.containsKey(cd.deactivate)) {
160 error("in component %s, deactivate method %s specified but not found", cd.implementation.getFQN(), cd.deactivate);
161 cd.activate = null;
162 }
163 if (cd.modified != null && !lifecycleMethods.containsKey(cd.modified)) {
164 error("in component %s, modified method %s specified but not found", cd.implementation.getFQN(), cd.modified);
165 cd.activate = null;
166 }
167
168 provide(cd, provides, impl);
169 properties(cd, info, name);
170 reference(info, impl, cd, bindmethods);
171 //compute namespace after references, an updated method means ds 1.2.
Stuart McCullochec47fe72012-09-19 12:56:05 +0000172 getNamespace(info, cd, lifecycleMethods);
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000173 cd.prepare(analyzer);
174 return cd.getTag();
175
176 }
177
178 private String checkIdentifier(String name, String value) {
179 if (value != null) {
180 if (!Verifier.isIdentifier(value)) {
181 error("Component attribute %s has value %s but is not a Java identifier",
182 name, value);
183 return null;
184 }
185 }
186 return value;
187 }
188
189 /**
190 * Check if we need to use the v1.1 namespace (or later).
191 *
192 * @param info
193 * @param cd TODO
194 * @param descriptors TODO
195 * @return
196 */
Stuart McCullochec47fe72012-09-19 12:56:05 +0000197 private void getNamespace(Map<String, String> info, ComponentDef cd, Map<String,MethodDef> descriptors) {
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000198 String namespace = info.get(COMPONENT_NAMESPACE);
199 if (namespace != null) {
Stuart McCullochec47fe72012-09-19 12:56:05 +0000200 cd.xmlns = namespace;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000201 }
202 String version = info.get(COMPONENT_VERSION);
203 if (version != null) {
204 try {
205 Version v = new Version(version);
Stuart McCullochec47fe72012-09-19 12:56:05 +0000206 cd.updateVersion(v);
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000207 } catch (Exception e) {
208 error("version: specified on component header but not a valid version: "
209 + version);
Stuart McCullochec47fe72012-09-19 12:56:05 +0000210 return;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000211 }
212 }
213 for (String key : info.keySet()) {
214 if (SET_COMPONENT_DIRECTIVES_1_2.contains(key)) {
Stuart McCullochec47fe72012-09-19 12:56:05 +0000215 cd.updateVersion(AnnotationReader.V1_2);
216 return;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000217 }
218 }
219 for (ReferenceDef rd: cd.references.values()) {
220 if (rd.updated != null) {
Stuart McCullochec47fe72012-09-19 12:56:05 +0000221 cd.updateVersion(AnnotationReader.V1_2);
222 return;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000223 }
224 }
225 //among other things this picks up any specified lifecycle methods
226 for (String key : info.keySet()) {
227 if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) {
Stuart McCullochec47fe72012-09-19 12:56:05 +0000228 cd.updateVersion(AnnotationReader.V1_1);
229 return;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000230 }
231 }
232 for (String lifecycle: LIFECYCLE_METHODS) {
233 //lifecycle methods were not specified.... check for non 1.0 signatures.
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000234 MethodDef test = descriptors.get(lifecycle);
235 if (descriptors.containsKey(lifecycle) && (!(test.isPublic() || test.isProtected()) ||
236 rateLifecycle(test, "deactivate".equals(lifecycle)? allowedDeactivate: allowed) > 1)) {
Stuart McCullochec47fe72012-09-19 12:56:05 +0000237 cd.updateVersion(AnnotationReader.V1_1);
238 return;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000239 }
240 }
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000241 }
242
243 /**
244 * Print the Service-Component properties element
245 *
246 * @param cd
247 * @param info
248 */
249 void properties(ComponentDef cd, Map<String, String> info, String name) {
250 Collection<String> properties = split(info.get(COMPONENT_PROPERTIES));
251 for (String p : properties) {
252 Matcher m = PROPERTY_PATTERN.matcher(p);
253
254 if (m.matches()) {
255 String key = m.group(1).replaceAll("@", ":");
256 String value = m.group(4);
257 String parts[] = value.split("\\s*(\\||\\n)\\s*");
258 for (String part: parts) {
259 cd.property.add(key, part);
260 }
261 } else
262 throw new IllegalArgumentException("Malformed property '" + p
263 + "' on component: " + name);
264 }
265 }
266
267 /**
268 * @param cd
269 * @param provides
270 */
271 void provide(ComponentDef cd, String provides, String impl) {
272 if (provides != null) {
273 StringTokenizer st = new StringTokenizer(provides, ",");
274 List<TypeRef> provide = new ArrayList<TypeRef>();
275 while (st.hasMoreTokens()) {
276 String interfaceName = st.nextToken();
277 TypeRef ref = analyzer.getTypeRefFromFQN(interfaceName);
278 provide.add(ref);
279 analyzer.referTo(ref);
280
281 // TODO verifies the impl. class extends or implements the
282 // interface
283 }
284 cd.service = provide.toArray(new TypeRef[provide.size()]);
285 }
286 }
287
288 public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
289
290 /**
291 * rates the methods according to the scale in 112.5.8 (compendium 4.3, ds 1.2), also returning "6" for invalid methods
292 * We don't look at return values yet due to proposal to all them for setting service properties.
293 * @param test methodDef to examine for suitability as a DS lifecycle method
294 * @param allowedParams TODO
295 * @return rating; 6 if invalid, lower is better
296 */
Stuart McCullochb215bfd2012-09-06 18:28:06 +0000297 int rateLifecycle(MethodDef test, Set<String> allowedParams) {
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000298 TypeRef[] prototype = test.getDescriptor().getPrototype();
299 if (prototype.length == 1 && ComponentContextTR.equals(prototype[0].getFQN()))
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000300 return 1;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000301 if (prototype.length == 1 && BundleContextTR.equals(prototype[0].getFQN()))
302 return 2;
303 if (prototype.length == 1 && MapTR.equals(prototype[0].getFQN()))
304 return 3;
305 if (prototype.length > 1) {
306 for (TypeRef tr: prototype) {
307 if (!allowedParams.contains(tr.getFQN()))
308 return 6;
309 }
310 return 5;
311 }
312 if (prototype.length == 0)
313 return 5;
314
315 return 6;
316 }
317
318 /**
319 * see 112.3.2. We can't distinguish the bind type, so we just accept anything.
320 * @param test
321 * @return
322 */
Stuart McCullochb215bfd2012-09-06 18:28:06 +0000323 int rateBind(MethodDef test) {
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000324 TypeRef[] prototype = test.getDescriptor().getPrototype();
325 if (prototype.length == 1 && ServiceReferenceTR.equals(prototype[0].getFQN()))
326 return 1;
327 if (prototype.length == 1)
328 return 2;
329 if (prototype.length == 2 && MapTR.equals(prototype[1].getFQN()))
330 return 3;
331 return 6;
332 }
333
334 /**
335 * @param info
336 * @param impl TODO
337 * @param descriptors TODO
338 * @param pw
339 * @throws Exception
340 */
341 void reference(Map<String, String> info, String impl, ComponentDef cd, Map<String,MethodDef> descriptors) throws Exception {
342 Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC)));
343 Collection<String> optional = new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL)));
344 Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE)));
345 Collection<String> greedy = new ArrayList<String>(split(info.get(COMPONENT_GREEDY)));
346
347
348 for (Map.Entry<String, String> entry : info.entrySet()) {
349
350 // Skip directives
351 String referenceName = entry.getKey();
352 if (referenceName.endsWith(":")) {
353 if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
354 error("Unrecognized directive in Service-Component header: "
355 + referenceName);
356 continue;
357 }
358
359 // Parse the bind/unbind methods from the name
360 // if set. They are separated by '/'
361 String bind = null;
362 String unbind = null;
363 String updated = null;
364
365 boolean bindCalculated = true;
366 boolean unbindCalculated = true;
367 boolean updatedCalculated = true;
368
369 if (referenceName.indexOf('/') >= 0) {
370 String parts[] = referenceName.split("/");
371 referenceName = parts[0];
372 if (parts[1].length() > 0) {
373 bind = parts[1];
374 bindCalculated = false;
375 } else {
376 bind = calculateBind(referenceName);
377 }
378 bind = parts[1].length() == 0? calculateBind(referenceName): parts[1];
379 if (parts.length > 2 && parts[2].length() > 0) {
380 unbind = parts[2] ;
381 unbindCalculated = false;
382 } else {
383 if (bind.startsWith("add"))
384 unbind = bind.replaceAll("add(.+)", "remove$1");
385 else
386 unbind = "un" + bind;
387 }
388 if (parts.length > 3) {
389 updated = parts[3];
390 updatedCalculated = false;
391 }
392 } else if (Character.isLowerCase(referenceName.charAt(0))) {
393 bind = calculateBind(referenceName);
394 unbind = "un" + bind;
395 updated = "updated" + Character.toUpperCase(referenceName.charAt(0))
396 + referenceName.substring(1);
397 }
398
399 String interfaceName = entry.getValue();
400 if (interfaceName == null || interfaceName.length() == 0) {
401 error("Invalid Interface Name for references in Service Component: "
402 + referenceName + "=" + interfaceName);
403 continue;
404 }
405
406 // If we have descriptors, we have analyzed the component.
407 // So why not check the methods
408 if (descriptors.size() > 0) {
409 // Verify that the bind method exists
410 if (!descriptors.containsKey(bind))
411 if (bindCalculated)
412 bind = null;
413 else
414 error("In component %s, the bind method %s for %s not defined", cd.name, bind, referenceName);
415
416 // Check if the unbind method exists
417 if (!descriptors.containsKey(unbind)) {
418 if (unbindCalculated)
419 // remove it
420 unbind = null;
421 else
422 error("In component %s, the unbind method %s for %s not defined", cd.name, unbind, referenceName);
423 }
424 if (!descriptors.containsKey(updated)) {
425 if (updatedCalculated)
426 //remove it
427 updated = null;
428 else
429 error("In component %s, the updated method %s for %s is not defined", cd.name, updated, referenceName);
430 }
431 }
432 // Check the cardinality by looking at the last
433 // character of the value
434 char c = interfaceName.charAt(interfaceName.length() - 1);
435 if ("?+*~".indexOf(c) >= 0) {
436 if (c == '?' || c == '*' || c == '~')
437 optional.add(referenceName);
438 if (c == '+' || c == '*')
439 multiple.add(referenceName);
440 if (c == '+' || c == '*' || c == '?')
441 dynamic.add(referenceName);
442 interfaceName = interfaceName.substring(0, interfaceName.length() - 1);
443 }
444
445 // Parse the target from the interface name
446 // The target is a filter.
447 String target = null;
448 Matcher m = REFERENCE.matcher(interfaceName);
449 if (m.matches()) {
450 interfaceName = m.group(1);
451 target = m.group(2);
452 }
453 TypeRef ref = analyzer.getTypeRefFromFQN(interfaceName);
454 analyzer.referTo(ref);
455 ReferenceDef rd = new ReferenceDef();
456 rd.name = referenceName;
457 rd.service = interfaceName;
458
459 if (optional.contains(referenceName)) {
460 if (multiple.contains(referenceName)) {
461 rd.cardinality = ReferenceCardinality.MULTIPLE;
462 } else {
463 rd.cardinality = ReferenceCardinality.OPTIONAL;
464 }
465 } else {
466 if (multiple.contains(referenceName)) {
467 rd.cardinality = ReferenceCardinality.AT_LEAST_ONE;
468 } else {
469 rd.cardinality = ReferenceCardinality.MANDATORY;
470 }
471 }
472 if (bind != null) {
473 rd.bind = bind;
474 if (unbind != null) {
475 rd.unbind = unbind;
476 }
477 if (updated != null) {
478 rd.updated = updated;
479 }
480 }
481
482 if (dynamic.contains(referenceName)) {
483 rd.policy = ReferencePolicy.DYNAMIC;
484 }
485
486 if (greedy.contains(referenceName)) {
487 rd.policyOption = ReferencePolicyOption.GREEDY;
488 }
489
490 if (target != null) {
491 rd.target = target;
492 }
493 cd.references.put(referenceName, rd);
494 }
495 }
496
497 private String calculateBind(String referenceName) {
498 return "set" + Character.toUpperCase(referenceName.charAt(0))
499 + referenceName.substring(1);
500 }
501
502}