Stuart McCulloch | d00f971 | 2009-07-13 10:06:47 +0000 | [diff] [blame^] | 1 | package aQute.bnd.make; |
| 2 | |
| 3 | import java.io.*; |
| 4 | import java.util.*; |
| 5 | import java.util.regex.*; |
| 6 | |
| 7 | import aQute.bnd.service.*; |
| 8 | import aQute.lib.filter.*; |
| 9 | import aQute.lib.osgi.*; |
| 10 | import aQute.libg.version.*; |
| 11 | |
| 12 | /** |
| 13 | * This class is an analyzer plugin. It looks at the properties and tries to |
| 14 | * find out if the Service-Component header contains the bnd shortut syntax. If |
| 15 | * not, the header is copied to the output, if it does, an XML file is created |
| 16 | * and added to the JAR and the header is modified appropriately. |
| 17 | */ |
| 18 | public class ServiceComponent implements AnalyzerPlugin { |
| 19 | public final static String NAMESPACE_STEM = "http://www.osgi.org/xmlns/scr"; |
| 20 | public final static String JIDENTIFIER = "<<identifier>>"; |
| 21 | public final static String COMPONENT_FACTORY = "factory:"; |
| 22 | public final static String COMPONENT_SERVICEFACTORY = "servicefactory:"; |
| 23 | public final static String COMPONENT_IMMEDIATE = "immediate:"; |
| 24 | public final static String COMPONENT_ENABLED = "enabled:"; |
| 25 | public final static String COMPONENT_DYNAMIC = "dynamic:"; |
| 26 | public final static String COMPONENT_MULTIPLE = "multiple:"; |
| 27 | public final static String COMPONENT_PROVIDE = "provide:"; |
| 28 | public final static String COMPONENT_OPTIONAL = "optional:"; |
| 29 | public final static String COMPONENT_PROPERTIES = "properties:"; |
| 30 | public final static String COMPONENT_IMPLEMENTATION = "implementation:"; |
| 31 | |
| 32 | // v1.1.0 |
| 33 | public final static String COMPONENT_VERSION = "version:"; |
| 34 | public final static String COMPONENT_CONFIGURATION_POLICY = "configuration-policy:"; |
| 35 | public final static String COMPONENT_MODIFIED = "modified:"; |
| 36 | public final static String COMPONENT_ACTIVATE = "activate:"; |
| 37 | public final static String COMPONENT_DEACTIVATE = "deactivate:"; |
| 38 | |
| 39 | public final static String[] componentDirectives = new String[] { |
| 40 | COMPONENT_FACTORY, COMPONENT_IMMEDIATE, COMPONENT_ENABLED, |
| 41 | COMPONENT_DYNAMIC, COMPONENT_MULTIPLE, COMPONENT_PROVIDE, |
| 42 | COMPONENT_OPTIONAL, COMPONENT_PROPERTIES, COMPONENT_IMPLEMENTATION, |
| 43 | COMPONENT_SERVICEFACTORY, COMPONENT_VERSION, |
| 44 | COMPONENT_CONFIGURATION_POLICY, COMPONENT_MODIFIED, |
| 45 | COMPONENT_ACTIVATE, COMPONENT_DEACTIVATE }; |
| 46 | |
| 47 | public final static Set<String> SET_COMPONENT_DIRECTIVES = new HashSet<String>( |
| 48 | Arrays |
| 49 | .asList(componentDirectives)); |
| 50 | |
| 51 | public final static Set<String> SET_COMPONENT_DIRECTIVES_1_1 = // |
| 52 | new HashSet<String>( |
| 53 | Arrays |
| 54 | .asList( |
| 55 | COMPONENT_VERSION, |
| 56 | COMPONENT_CONFIGURATION_POLICY, |
| 57 | COMPONENT_MODIFIED, |
| 58 | COMPONENT_ACTIVATE, |
| 59 | COMPONENT_DEACTIVATE)); |
| 60 | |
| 61 | public boolean analyzeJar(Analyzer analyzer) throws Exception { |
| 62 | |
| 63 | ComponentMaker m = new ComponentMaker(analyzer); |
| 64 | |
| 65 | Map<String, Map<String, String>> l = m.doServiceComponent(); |
| 66 | |
| 67 | if (!l.isEmpty()) |
| 68 | analyzer.setProperty(Constants.SERVICE_COMPONENT, Processor |
| 69 | .printClauses(l, "")); |
| 70 | |
| 71 | analyzer.getInfo(m, "Service Component"); |
| 72 | m.close(); |
| 73 | return false; |
| 74 | } |
| 75 | |
| 76 | private static class ComponentMaker extends Processor { |
| 77 | Analyzer analyzer; |
| 78 | |
| 79 | ComponentMaker(Analyzer analyzer) { |
| 80 | super(analyzer); |
| 81 | this.analyzer = analyzer; |
| 82 | } |
| 83 | |
| 84 | Map<String, Map<String, String>> doServiceComponent() throws Exception { |
| 85 | String header = getProperty(SERVICE_COMPONENT); |
| 86 | return doServiceComponent(header); |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Check if a service component header is actually referring to a class. |
| 91 | * If so, replace the reference with an XML file reference. This makes |
| 92 | * it easier to create and use components. |
| 93 | * |
| 94 | * @throws UnsupportedEncodingException |
| 95 | * |
| 96 | */ |
| 97 | public Map<String, Map<String, String>> doServiceComponent( |
| 98 | String serviceComponent) throws IOException { |
| 99 | Map<String, Map<String, String>> list = newMap(); |
| 100 | Map<String, Map<String, String>> sc = parseHeader(serviceComponent); |
| 101 | Map<String, String> empty = Collections.emptyMap(); |
| 102 | |
| 103 | for (Iterator<Map.Entry<String, Map<String, String>>> i = sc |
| 104 | .entrySet().iterator(); i.hasNext();) { |
| 105 | Map.Entry<String, Map<String, String>> entry = i.next(); |
| 106 | String name = entry.getKey(); |
| 107 | Map<String, String> info = entry.getValue(); |
| 108 | if (name == null) { |
| 109 | error("No name in Service-Component header: " + info); |
| 110 | continue; |
| 111 | } |
| 112 | if (name.indexOf("*") >= 0 || analyzer.getJar().exists(name)) { |
| 113 | // Normal service component, we do not process them |
| 114 | list.put(name, info); |
| 115 | } else { |
| 116 | String impl = name; |
| 117 | |
| 118 | if (info.containsKey(COMPONENT_IMPLEMENTATION)) |
| 119 | impl = info.get(COMPONENT_IMPLEMENTATION); |
| 120 | |
| 121 | if (!analyzer.checkClass(impl)) { |
| 122 | error("Not found Service-Component header: " + name); |
| 123 | } else { |
| 124 | // We have a definition, so make an XML resources |
| 125 | Resource resource = createComponentResource(name, info); |
| 126 | analyzer.getJar().putResource( |
| 127 | "OSGI-INF/" + name + ".xml", resource); |
| 128 | list.put("OSGI-INF/" + name + ".xml", empty); |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | return list; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Create the resource for a DS component. |
| 137 | * |
| 138 | * @param list |
| 139 | * @param name |
| 140 | * @param info |
| 141 | * @throws UnsupportedEncodingException |
| 142 | */ |
| 143 | Resource createComponentResource(String name, Map<String, String> info) |
| 144 | throws IOException { |
| 145 | String namespace = getNamespace(info); |
| 146 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 147 | PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, |
| 148 | "UTF-8")); |
| 149 | pw.println("<?xml version='1.0' encoding='utf-8'?>"); |
| 150 | pw.print("<component name='" + name + "'"); |
| 151 | if (namespace != null) { |
| 152 | pw.print(" xmlns='" + namespace + "'"); |
| 153 | } |
| 154 | |
| 155 | doAttribute(pw, info.get(COMPONENT_FACTORY), "factory"); |
| 156 | doAttribute(pw, info.get(COMPONENT_IMMEDIATE), "immediate", |
| 157 | "false", "true"); |
| 158 | doAttribute(pw, info.get(COMPONENT_ENABLED), "enabled", "true", |
| 159 | "false"); |
| 160 | doAttribute(pw, info.get(COMPONENT_CONFIGURATION_POLICY), |
| 161 | "configuration-policy", "optional", "require", "ignore"); |
| 162 | doAttribute(pw, info.get(COMPONENT_ACTIVATE), "activate", |
| 163 | JIDENTIFIER); |
| 164 | doAttribute(pw, info.get(COMPONENT_DEACTIVATE), "deactivate", |
| 165 | JIDENTIFIER); |
| 166 | doAttribute(pw, info.get(COMPONENT_MODIFIED), "modified", |
| 167 | JIDENTIFIER); |
| 168 | |
| 169 | pw.println(">"); |
| 170 | |
| 171 | // Allow override of the implementation when people |
| 172 | // want to choose their own name |
| 173 | String impl = (String) info.get(COMPONENT_IMPLEMENTATION); |
| 174 | pw.println(" <implementation class='" |
| 175 | + (impl == null ? name : impl) + "'/>"); |
| 176 | |
| 177 | String provides = info.get(COMPONENT_PROVIDE); |
| 178 | boolean servicefactory = Boolean.getBoolean(info |
| 179 | .get(COMPONENT_SERVICEFACTORY) |
| 180 | + ""); |
| 181 | provides(pw, provides, servicefactory); |
| 182 | properties(pw, info); |
| 183 | reference(info, pw); |
| 184 | pw.println("</component>"); |
| 185 | pw.close(); |
| 186 | byte[] data = out.toByteArray(); |
| 187 | out.close(); |
| 188 | return new EmbeddedResource(data, 0); |
| 189 | } |
| 190 | |
| 191 | private void doAttribute(PrintWriter pw, String value, String name, |
| 192 | String... matches) { |
| 193 | if (value != null) { |
| 194 | if (matches.length != 0) { |
| 195 | if (matches.length == 1 && matches[0].equals(JIDENTIFIER)) { |
| 196 | if (!Verifier.isIdentifier(value)) |
| 197 | error( |
| 198 | "Component attribute %s has value %s but is not a Java identifier", |
| 199 | name, value); |
| 200 | } else { |
| 201 | |
| 202 | if (!Verifier.isMember(value, matches)) |
| 203 | error( |
| 204 | "Component attribute %s has value %s but is not a member of %s", |
| 205 | name, value, Arrays.toString(matches)); |
| 206 | } |
| 207 | } |
| 208 | pw.print(" "); |
| 209 | pw.print(name); |
| 210 | pw.print("='"); |
| 211 | pw.print(value); |
| 212 | pw.print("'"); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Check if we need to use the v1.1 namespace (or later). |
| 218 | * |
| 219 | * @param info |
| 220 | * @return |
| 221 | */ |
| 222 | private String getNamespace(Map<String, String> info) { |
| 223 | String version = info.get(COMPONENT_VERSION); |
| 224 | if (version != null) { |
| 225 | try { |
| 226 | Version v = new Version(version); |
| 227 | return NAMESPACE_STEM + "/v" + v; |
| 228 | } catch (Exception e) { |
| 229 | error("version: specified on component header but not a valid version: " |
| 230 | + version); |
| 231 | return null; |
| 232 | } |
| 233 | } |
| 234 | for (String key : info.keySet()) { |
| 235 | if (SET_COMPONENT_DIRECTIVES_1_1.contains(key)) { |
| 236 | return NAMESPACE_STEM + "/v1.1.0"; |
| 237 | } |
| 238 | } |
| 239 | return null; |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Print the Service-Component properties element |
| 244 | * |
| 245 | * @param pw |
| 246 | * @param info |
| 247 | */ |
| 248 | void properties(PrintWriter pw, Map<String, String> info) { |
| 249 | Collection<String> properties = split(info |
| 250 | .get(COMPONENT_PROPERTIES)); |
| 251 | for (Iterator<String> p = properties.iterator(); p.hasNext();) { |
| 252 | String clause = p.next(); |
| 253 | int n = clause.indexOf('='); |
| 254 | if (n <= 0) { |
| 255 | error("Not a valid property in service component: " |
| 256 | + clause); |
| 257 | } else { |
| 258 | String type = null; |
| 259 | String name = clause.substring(0, n); |
| 260 | if (name.indexOf('@') >= 0) { |
| 261 | String parts[] = name.split("@"); |
| 262 | name = parts[1]; |
| 263 | type = parts[0]; |
| 264 | } |
| 265 | String value = clause.substring(n + 1).trim(); |
| 266 | // TODO verify validity of name and value. |
| 267 | pw.print("<property name='"); |
| 268 | pw.print(name); |
| 269 | pw.print("'"); |
| 270 | |
| 271 | if (type != null) { |
| 272 | if (VALID_PROPERTY_TYPES.matcher(type).matches()) { |
| 273 | pw.print(" type='"); |
| 274 | pw.print(type); |
| 275 | pw.print("'"); |
| 276 | } else { |
| 277 | warning("Invalid property type '" + type |
| 278 | + "' for property " + name); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | String parts[] = value.split("\\s*(\\||\\n)\\s*"); |
| 283 | if (parts.length > 1) { |
| 284 | pw.println(">"); |
| 285 | for (String part : parts) { |
| 286 | pw.println(part); |
| 287 | } |
| 288 | pw.println("</property>"); |
| 289 | } else { |
| 290 | pw.print(" value='"); |
| 291 | pw.print(parts[0]); |
| 292 | pw.print("'/>"); |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * @param pw |
| 300 | * @param provides |
| 301 | */ |
| 302 | void provides(PrintWriter pw, String provides, boolean servicefactory) { |
| 303 | if (provides != null) { |
| 304 | if (!servicefactory) |
| 305 | pw.println(" <service>"); |
| 306 | else |
| 307 | pw.println(" <service servicefactory='true'>"); |
| 308 | |
| 309 | StringTokenizer st = new StringTokenizer(provides, ","); |
| 310 | while (st.hasMoreTokens()) { |
| 311 | String interfaceName = st.nextToken(); |
| 312 | pw.println(" <provide interface='" + interfaceName |
| 313 | + "'/>"); |
| 314 | if (!analyzer.checkClass(interfaceName)) |
| 315 | error("Component definition provides a class that is neither imported nor contained: " |
| 316 | + interfaceName); |
| 317 | } |
| 318 | pw.println(" </service>"); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | public final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?"); |
| 323 | |
| 324 | /** |
| 325 | * @param info |
| 326 | * @param pw |
| 327 | */ |
| 328 | |
| 329 | void reference(Map<String, String> info, PrintWriter pw) { |
| 330 | Collection<String> dynamic = new ArrayList<String>(split(info.get(COMPONENT_DYNAMIC))); |
| 331 | Collection<String> optional = new ArrayList<String>(split(info.get(COMPONENT_OPTIONAL))); |
| 332 | Collection<String> multiple = new ArrayList<String>(split(info.get(COMPONENT_MULTIPLE))); |
| 333 | |
| 334 | for (Iterator<Map.Entry<String, String>> r = info.entrySet() |
| 335 | .iterator(); r.hasNext();) { |
| 336 | Map.Entry<String, String> ref = r.next(); |
| 337 | String referenceName = (String) ref.getKey(); |
| 338 | String target = null; |
| 339 | String interfaceName = (String) ref.getValue(); |
| 340 | if (interfaceName == null || interfaceName.length() == 0) { |
| 341 | error("Invalid Interface Name for references in Service Component: " |
| 342 | + referenceName + "=" + interfaceName); |
| 343 | } |
| 344 | char c = interfaceName.charAt(interfaceName.length() - 1); |
| 345 | if ("?+*~".indexOf(c) >= 0) { |
| 346 | if (c == '?' || c == '*' || c == '~') |
| 347 | optional.add(referenceName); |
| 348 | if (c == '+' || c == '*') |
| 349 | multiple.add(referenceName); |
| 350 | if (c == '+' || c == '*' || c == '?') |
| 351 | dynamic.add(referenceName); |
| 352 | interfaceName = interfaceName.substring(0, interfaceName |
| 353 | .length() - 1); |
| 354 | } |
| 355 | |
| 356 | if (referenceName.endsWith(":")) { |
| 357 | if (!SET_COMPONENT_DIRECTIVES.contains(referenceName)) |
| 358 | error("Unrecognized directive in Service-Component header: " |
| 359 | + referenceName); |
| 360 | continue; |
| 361 | } |
| 362 | |
| 363 | Matcher m = REFERENCE.matcher(interfaceName); |
| 364 | if (m.matches()) { |
| 365 | interfaceName = m.group(1); |
| 366 | target = m.group(2); |
| 367 | } |
| 368 | |
| 369 | if (!analyzer.checkClass(interfaceName)) |
| 370 | error("Component definition refers to a class that is neither imported nor contained: " |
| 371 | + interfaceName); |
| 372 | |
| 373 | pw.print(" <reference name='" + referenceName |
| 374 | + "' interface='" + interfaceName + "'"); |
| 375 | |
| 376 | String cardinality = optional.contains(referenceName) ? "0" |
| 377 | : "1"; |
| 378 | cardinality += ".."; |
| 379 | cardinality += multiple.contains(referenceName) ? "n" : "1"; |
| 380 | if (!cardinality.equals("1..1")) |
| 381 | pw.print(" cardinality='" + cardinality + "'"); |
| 382 | |
| 383 | if (Character.isLowerCase(referenceName.charAt(0))) { |
| 384 | String z = referenceName.substring(0, 1).toUpperCase() |
| 385 | + referenceName.substring(1); |
| 386 | pw.print(" bind='set" + z + "'"); |
| 387 | pw.print(" unbind='unset" + z + "'"); |
| 388 | // TODO Verify that the methods exist |
| 389 | } |
| 390 | |
| 391 | if (dynamic.contains(referenceName)) { |
| 392 | pw.print(" policy='dynamic'"); |
| 393 | } |
| 394 | |
| 395 | if (target != null) { |
| 396 | Filter filter = new Filter(target); |
| 397 | if (filter.verify() == null) |
| 398 | pw.print(" target='" + filter.toString() + "'"); |
| 399 | else |
| 400 | error("Target for " + referenceName |
| 401 | + " is not a correct filter: " + target + " " |
| 402 | + filter.verify()); |
| 403 | } |
| 404 | pw.println("/>"); |
| 405 | } |
| 406 | } |
| 407 | } |
| 408 | } |