blob: a7940278041cb0078c5792d3747ebe945d4be818 [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();
37 private final static Set<String> allowed = new HashSet<String>(Arrays.asList(ComponentContextTR, BundleContextTR, MapTR));
38 private final static Set<String> allowedDeactivate = new HashSet<String>(Arrays.asList(ComponentContextTR, BundleContextTR, MapTR, IntTR));
39
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
94 public void method(MethodDef md) {
95 Set<String> allowedParams = allowed;
96 String lifecycleName = null;
97
98 boolean isLifecycle = (cd.activate == null? "activate": cd.activate).equals(md.getName()) ||
99 md.getName().equals(cd.modified);
100 if (!isLifecycle && (cd.deactivate == null? "deactivate": cd.deactivate).equals(md.getName())) {
101 isLifecycle = true;
102 allowedParams = allowedDeactivate;
103 }
104 if (isLifecycle && !lifecycleMethods.containsKey(md.getName()) &&
105 (md.isPublic() ||
106 md.isProtected() ||
107 (md.isPrivate() && pa) ||
108 (!md.isPrivate()) && da) &&
109 isBetter(md, classLifecyclemethods.get(md.getName()), allowedParams)) {
110 classLifecyclemethods.put(md.getName(), md);
111 }
112 if (!bindmethods.containsKey(md.getName()) &&
113 (md.isPublic() ||
114 md.isProtected() ||
115 (md.isPrivate() && pa) ||
116 (!md.isPrivate()) && da) &&
117 isBetterBind(md, classBindmethods.get(md.getName()))) {
118 classBindmethods.put(md.getName(), md);
119 }
120 }
121
122 private boolean isBetter(MethodDef test, MethodDef existing, Set<String> allowedParams) {
123 int testRating = rateLifecycle(test, allowedParams);
124 if (existing == null)
125 return testRating < 6;// ignore invalid methods
126 if (testRating < rateLifecycle(existing, allowedParams))
127 return true;
128
129 return false;
130 }
131
132 private boolean isBetterBind(MethodDef test, MethodDef existing) {
133 int testRating = rateBind(test);
134 if (existing == null)
135 return testRating < 6;// ignore invalid methods
136 if (testRating < rateBind(existing))
137 return true;
138
139 return false;
140 }
141
142 });
143 lifecycleMethods.putAll(classLifecyclemethods);
144 bindmethods.putAll(classBindmethods);
145 typeRef = clazz.getSuper();
146 if (typeRef == null)
147 break;
148 clazz = analyzer.findClass(typeRef);
149 privateAllowed = false;
150 defaultAllowed = defaultAllowed && topPackage.equals(typeRef.getPackageRef().getFQN());
151 }
152
153
154 if (cd.activate != null && !lifecycleMethods.containsKey(cd.activate)) {
155 error("in component %s, activate method %s specified but not found", cd.implementation.getFQN(), cd.activate);
156 cd.activate = null;
157 }
158 if (cd.deactivate != null && !lifecycleMethods.containsKey(cd.deactivate)) {
159 error("in component %s, deactivate method %s specified but not found", cd.implementation.getFQN(), cd.deactivate);
160 cd.activate = null;
161 }
162 if (cd.modified != null && !lifecycleMethods.containsKey(cd.modified)) {
163 error("in component %s, modified method %s specified but not found", cd.implementation.getFQN(), cd.modified);
164 cd.activate = null;
165 }
166
167 provide(cd, provides, impl);
168 properties(cd, info, name);
169 reference(info, impl, cd, bindmethods);
170 //compute namespace after references, an updated method means ds 1.2.
171 cd.xmlns = getNamespace(info, cd, lifecycleMethods);
172 cd.prepare(analyzer);
173 return cd.getTag();
174
175 }
176
177 private String checkIdentifier(String name, String value) {
178 if (value != null) {
179 if (!Verifier.isIdentifier(value)) {
180 error("Component attribute %s has value %s but is not a Java identifier",
181 name, value);
182 return null;
183 }
184 }
185 return value;
186 }
187
188 /**
189 * Check if we need to use the v1.1 namespace (or later).
190 *
191 * @param info
192 * @param cd TODO
193 * @param descriptors TODO
194 * @return
195 */
196 private String getNamespace(Map<String, String> info, ComponentDef cd, Map<String,MethodDef> descriptors) {
197 String namespace = info.get(COMPONENT_NAMESPACE);
198 if (namespace != null) {
199 return namespace;
200 }
201 String version = info.get(COMPONENT_VERSION);
202 if (version != null) {
203 try {
204 Version v = new Version(version);
205 return NAMESPACE_STEM + "/v" + v;
206 } catch (Exception e) {
207 error("version: specified on component header but not a valid version: "
208 + version);
209 return null;
210 }
211 }
212 for (String key : info.keySet()) {
213 if (SET_COMPONENT_DIRECTIVES_1_2.contains(key)) {
214 return NAMESPACE_STEM + "/v1.2.0";
215 }
216 }
217 for (ReferenceDef rd: cd.references.values()) {
218 if (rd.updated != null) {
219 return NAMESPACE_STEM + "/v1.2.0";
220 }
221 }
222 //among other things this picks up any specified lifecycle methods
223 for (String key : info.keySet()) {
224 if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) {
225 return NAMESPACE_STEM + "/v1.1.0";
226 }
227 }
228 for (String lifecycle: LIFECYCLE_METHODS) {
229 //lifecycle methods were not specified.... check for non 1.0 signatures.
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000230 MethodDef test = descriptors.get(lifecycle);
231 if (descriptors.containsKey(lifecycle) && (!(test.isPublic() || test.isProtected()) ||
232 rateLifecycle(test, "deactivate".equals(lifecycle)? allowedDeactivate: allowed) > 1)) {
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000233 return NAMESPACE_STEM + "/v1.1.0";
234 }
235 }
236 return null;
237 }
238
239 /**
240 * Print the Service-Component properties element
241 *
242 * @param cd
243 * @param info
244 */
245 void properties(ComponentDef cd, Map<String, String> info, String name) {
246 Collection<String> properties = split(info.get(COMPONENT_PROPERTIES));
247 for (String p : properties) {
248 Matcher m = PROPERTY_PATTERN.matcher(p);
249
250 if (m.matches()) {
251 String key = m.group(1).replaceAll("@", ":");
252 String value = m.group(4);
253 String parts[] = value.split("\\s*(\\||\\n)\\s*");
254 for (String part: parts) {
255 cd.property.add(key, part);
256 }
257 } else
258 throw new IllegalArgumentException("Malformed property '" + p
259 + "' on component: " + name);
260 }
261 }
262
263 /**
264 * @param cd
265 * @param provides
266 */
267 void provide(ComponentDef cd, String provides, String impl) {
268 if (provides != null) {
269 StringTokenizer st = new StringTokenizer(provides, ",");
270 List<TypeRef> provide = new ArrayList<TypeRef>();
271 while (st.hasMoreTokens()) {
272 String interfaceName = st.nextToken();
273 TypeRef ref = analyzer.getTypeRefFromFQN(interfaceName);
274 provide.add(ref);
275 analyzer.referTo(ref);
276
277 // TODO verifies the impl. class extends or implements the
278 // interface
279 }
280 cd.service = provide.toArray(new TypeRef[provide.size()]);
281 }
282 }
283
284 public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
285
286 /**
287 * rates the methods according to the scale in 112.5.8 (compendium 4.3, ds 1.2), also returning "6" for invalid methods
288 * We don't look at return values yet due to proposal to all them for setting service properties.
289 * @param test methodDef to examine for suitability as a DS lifecycle method
290 * @param allowedParams TODO
291 * @return rating; 6 if invalid, lower is better
292 */
293 private int rateLifecycle(MethodDef test, Set<String> allowedParams) {
294 TypeRef[] prototype = test.getDescriptor().getPrototype();
295 if (prototype.length == 1 && ComponentContextTR.equals(prototype[0].getFQN()))
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000296 return 1;
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000297 if (prototype.length == 1 && BundleContextTR.equals(prototype[0].getFQN()))
298 return 2;
299 if (prototype.length == 1 && MapTR.equals(prototype[0].getFQN()))
300 return 3;
301 if (prototype.length > 1) {
302 for (TypeRef tr: prototype) {
303 if (!allowedParams.contains(tr.getFQN()))
304 return 6;
305 }
306 return 5;
307 }
308 if (prototype.length == 0)
309 return 5;
310
311 return 6;
312 }
313
314 /**
315 * see 112.3.2. We can't distinguish the bind type, so we just accept anything.
316 * @param test
317 * @return
318 */
319 private int rateBind(MethodDef test) {
320 TypeRef[] prototype = test.getDescriptor().getPrototype();
321 if (prototype.length == 1 && ServiceReferenceTR.equals(prototype[0].getFQN()))
322 return 1;
323 if (prototype.length == 1)
324 return 2;
325 if (prototype.length == 2 && MapTR.equals(prototype[1].getFQN()))
326 return 3;
327 return 6;
328 }
329
330 /**
331 * @param info
332 * @param impl TODO
333 * @param descriptors TODO
334 * @param pw
335 * @throws Exception
336 */
337 void reference(Map<String, String> info, String impl, ComponentDef cd, Map<String,MethodDef> descriptors) throws Exception {
338 Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC)));
339 Collection<String> optional = new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL)));
340 Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE)));
341 Collection<String> greedy = new ArrayList<String>(split(info.get(COMPONENT_GREEDY)));
342
343
344 for (Map.Entry<String, String> entry : info.entrySet()) {
345
346 // Skip directives
347 String referenceName = entry.getKey();
348 if (referenceName.endsWith(":")) {
349 if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
350 error("Unrecognized directive in Service-Component header: "
351 + referenceName);
352 continue;
353 }
354
355 // Parse the bind/unbind methods from the name
356 // if set. They are separated by '/'
357 String bind = null;
358 String unbind = null;
359 String updated = null;
360
361 boolean bindCalculated = true;
362 boolean unbindCalculated = true;
363 boolean updatedCalculated = true;
364
365 if (referenceName.indexOf('/') >= 0) {
366 String parts[] = referenceName.split("/");
367 referenceName = parts[0];
368 if (parts[1].length() > 0) {
369 bind = parts[1];
370 bindCalculated = false;
371 } else {
372 bind = calculateBind(referenceName);
373 }
374 bind = parts[1].length() == 0? calculateBind(referenceName): parts[1];
375 if (parts.length > 2 && parts[2].length() > 0) {
376 unbind = parts[2] ;
377 unbindCalculated = false;
378 } else {
379 if (bind.startsWith("add"))
380 unbind = bind.replaceAll("add(.+)", "remove$1");
381 else
382 unbind = "un" + bind;
383 }
384 if (parts.length > 3) {
385 updated = parts[3];
386 updatedCalculated = false;
387 }
388 } else if (Character.isLowerCase(referenceName.charAt(0))) {
389 bind = calculateBind(referenceName);
390 unbind = "un" + bind;
391 updated = "updated" + Character.toUpperCase(referenceName.charAt(0))
392 + referenceName.substring(1);
393 }
394
395 String interfaceName = entry.getValue();
396 if (interfaceName == null || interfaceName.length() == 0) {
397 error("Invalid Interface Name for references in Service Component: "
398 + referenceName + "=" + interfaceName);
399 continue;
400 }
401
402 // If we have descriptors, we have analyzed the component.
403 // So why not check the methods
404 if (descriptors.size() > 0) {
405 // Verify that the bind method exists
406 if (!descriptors.containsKey(bind))
407 if (bindCalculated)
408 bind = null;
409 else
410 error("In component %s, the bind method %s for %s not defined", cd.name, bind, referenceName);
411
412 // Check if the unbind method exists
413 if (!descriptors.containsKey(unbind)) {
414 if (unbindCalculated)
415 // remove it
416 unbind = null;
417 else
418 error("In component %s, the unbind method %s for %s not defined", cd.name, unbind, referenceName);
419 }
420 if (!descriptors.containsKey(updated)) {
421 if (updatedCalculated)
422 //remove it
423 updated = null;
424 else
425 error("In component %s, the updated method %s for %s is not defined", cd.name, updated, referenceName);
426 }
427 }
428 // Check the cardinality by looking at the last
429 // character of the value
430 char c = interfaceName.charAt(interfaceName.length() - 1);
431 if ("?+*~".indexOf(c) >= 0) {
432 if (c == '?' || c == '*' || c == '~')
433 optional.add(referenceName);
434 if (c == '+' || c == '*')
435 multiple.add(referenceName);
436 if (c == '+' || c == '*' || c == '?')
437 dynamic.add(referenceName);
438 interfaceName = interfaceName.substring(0, interfaceName.length() - 1);
439 }
440
441 // Parse the target from the interface name
442 // The target is a filter.
443 String target = null;
444 Matcher m = REFERENCE.matcher(interfaceName);
445 if (m.matches()) {
446 interfaceName = m.group(1);
447 target = m.group(2);
448 }
449 TypeRef ref = analyzer.getTypeRefFromFQN(interfaceName);
450 analyzer.referTo(ref);
451 ReferenceDef rd = new ReferenceDef();
452 rd.name = referenceName;
453 rd.service = interfaceName;
454
455 if (optional.contains(referenceName)) {
456 if (multiple.contains(referenceName)) {
457 rd.cardinality = ReferenceCardinality.MULTIPLE;
458 } else {
459 rd.cardinality = ReferenceCardinality.OPTIONAL;
460 }
461 } else {
462 if (multiple.contains(referenceName)) {
463 rd.cardinality = ReferenceCardinality.AT_LEAST_ONE;
464 } else {
465 rd.cardinality = ReferenceCardinality.MANDATORY;
466 }
467 }
468 if (bind != null) {
469 rd.bind = bind;
470 if (unbind != null) {
471 rd.unbind = unbind;
472 }
473 if (updated != null) {
474 rd.updated = updated;
475 }
476 }
477
478 if (dynamic.contains(referenceName)) {
479 rd.policy = ReferencePolicy.DYNAMIC;
480 }
481
482 if (greedy.contains(referenceName)) {
483 rd.policyOption = ReferencePolicyOption.GREEDY;
484 }
485
486 if (target != null) {
487 rd.target = target;
488 }
489 cd.references.put(referenceName, rd);
490 }
491 }
492
493 private String calculateBind(String referenceName) {
494 return "set" + Character.toUpperCase(referenceName.charAt(0))
495 + referenceName.substring(1);
496 }
497
498}