blob: a1f3a5f430688991d67499f99bf498e350870cf6 [file] [log] [blame]
Brian O'Connoree674952016-09-13 16:31:45 -07001/*
2 * Copyright 2016-present Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.onosjar;
18
19import aQute.bnd.header.Attrs;
20import aQute.bnd.header.Parameters;
21import aQute.bnd.osgi.Analyzer;
22import aQute.bnd.osgi.Builder;
Bharat saraswala899a212017-02-28 13:19:57 +053023import aQute.bnd.osgi.Constants;
24import aQute.bnd.osgi.Descriptors;
Brian O'Connoree674952016-09-13 16:31:45 -070025import aQute.bnd.osgi.FileResource;
26import aQute.bnd.osgi.Jar;
Bharat saraswala899a212017-02-28 13:19:57 +053027import aQute.bnd.osgi.Packages;
28import aQute.bnd.osgi.Processor;
Brian O'Connoree674952016-09-13 16:31:45 -070029import aQute.bnd.osgi.Resource;
30import com.facebook.buck.step.ExecutionContext;
31import com.facebook.buck.step.Step;
32import com.facebook.buck.step.StepExecutionResult;
33import com.google.common.base.MoreObjects;
34import com.google.common.collect.ImmutableSortedSet;
35import com.google.common.collect.Lists;
36import com.google.common.collect.Maps;
37import com.google.common.collect.Sets;
38import org.apache.felix.scrplugin.bnd.SCRDescriptorBndPlugin;
Bharat saraswala899a212017-02-28 13:19:57 +053039import org.codehaus.plexus.util.DirectoryScanner;
Brian O'Connoree674952016-09-13 16:31:45 -070040
41import java.io.File;
42import java.io.IOException;
43import java.io.PrintStream;
44import java.nio.file.FileVisitResult;
45import java.nio.file.FileVisitor;
46import java.nio.file.Path;
47import java.nio.file.Paths;
48import java.nio.file.SimpleFileVisitor;
49import java.nio.file.attribute.BasicFileAttributes;
50import java.util.HashSet;
51import java.util.List;
52import java.util.Map;
Bharat saraswala899a212017-02-28 13:19:57 +053053import java.util.Properties;
Brian O'Connoree674952016-09-13 16:31:45 -070054import java.util.Set;
55import java.util.jar.Manifest;
56import java.util.stream.Collectors;
57
58import static java.nio.file.Files.walkFileTree;
59
60/**
61 * BND-based wrapper to convert Buck JARs to OSGi-compatible JARs.
62 */
63public class OSGiWrapper implements Step {
64
65 private Path inputJar;
66 private Path outputJar;
67 private Path sourcesDir;
68 private Path classesDir;
69 private List<String> classpath;
70
71 private String bundleName;
72 private String groupId;
73 private String bundleSymbolicName;
74 private String bundleVersion;
75
76 private String importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +053077 private String privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -070078 private String dynamicimportPackages;
79
80 private String exportPackages;
81 private String includeResources;
82 private Set<String> includedResources = Sets.newHashSet();
83
84 private String bundleDescription;
85 private String bundleLicense;
86
87 private String webContext;
88
89 private PrintStream stderr = System.err;
90
91 public OSGiWrapper(Path inputJar,
92 Path outputJar,
93 Path sourcesDir,
94 Path classesDir,
95 ImmutableSortedSet<Path> classpath,
96 String bundleName,
97 String groupId,
98 String bundleVersion,
99 String bundleLicense,
100 String importPackages,
101 String exportPackages,
102 String includeResources,
103 String webContext,
104 String dynamicimportPackages,
Bharat saraswala899a212017-02-28 13:19:57 +0530105 String bundleDescription,
106 String privatePackages) {
Brian O'Connoree674952016-09-13 16:31:45 -0700107 this.inputJar = inputJar;
108 this.sourcesDir = sourcesDir;
109 this.classesDir = classesDir;
110 this.classpath = Lists.newArrayList(
111 classpath.stream().map(Path::toString).collect(Collectors.toList()));
112 if (!this.classpath.contains(inputJar.toString())) {
113 this.classpath.add(0, inputJar.toString());
114 }
115 this.outputJar = outputJar;
116
117 this.bundleName = bundleName;
118 this.groupId = groupId;
119 this.bundleSymbolicName = String.format("%s.%s", groupId, bundleName);
120
121 this.bundleVersion = bundleVersion;
122 this.bundleLicense = bundleLicense;
123 this.bundleDescription = bundleDescription;
124
125 this.importPackages = importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +0530126 this.privatePackages = privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -0700127 this.dynamicimportPackages = dynamicimportPackages;
128 this.exportPackages = exportPackages;
129 this.includeResources = includeResources;
130
131 this.webContext = webContext;
132 }
133
134 private void setProperties(Analyzer analyzer) {
135 analyzer.setProperty(Analyzer.BUNDLE_NAME, bundleName);
136 analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME, bundleSymbolicName);
137 analyzer.setProperty(Analyzer.BUNDLE_VERSION, bundleVersion.replace('-', '.'));
138
139 if (bundleDescription != null) {
140 analyzer.setProperty(Analyzer.BUNDLE_DESCRIPTION, bundleDescription);
141 }
142 if (bundleLicense != null) {
143 analyzer.setProperty(Analyzer.BUNDLE_LICENSE, bundleLicense);
144 }
145
146 //TODO consider using stricter version policy
147 //analyzer.setProperty("-provider-policy", "${range;[===,==+)}");
148 //analyzer.setProperty("-consumer-policy", "${range;[===,==+)}");
149
150 // There are no good defaults so make sure you set the Import-Package
151 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages);
Bharat saraswala899a212017-02-28 13:19:57 +0530152 if (privatePackages != null) {
153 analyzer.setProperty(Analyzer.PRIVATE_PACKAGE, privatePackages);
154 }
155 analyzer.setProperty(Analyzer.REMOVEHEADERS, "Private-Package,Include-Resource");
Brian O'Connoree674952016-09-13 16:31:45 -0700156
Bharat saraswala899a212017-02-28 13:19:57 +0530157 analyzer.setProperty(Analyzer.DYNAMICIMPORT_PACKAGE,
158 dynamicimportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700159
160 // TODO include version in export, but not in import
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700161 analyzer.setProperty(Analyzer.EXPORT_PACKAGE, exportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700162
163 // TODO we may need INCLUDE_RESOURCE, or that might be done by Buck
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700164 // FIXME NOTE we handle this manually below
Brian O'Connoree674952016-09-13 16:31:45 -0700165 if (includeResources != null) {
166 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources);
167 }
168
169 if (isWab()) {
170 analyzer.setProperty(Analyzer.WAB, "src/main/webapp/");
171 analyzer.setProperty("Web-ContextPath", webContext);
172 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, "*,org.glassfish.jersey.servlet,org.jvnet.mimepull\n");
173 }
174 }
175
176 public boolean execute() {
Bharat saraswala899a212017-02-28 13:19:57 +0530177 Builder analyzer = new Builder();
Brian O'Connoree674952016-09-13 16:31:45 -0700178 try {
179
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700180 Jar jar = new Jar(inputJar.toFile()); // where our data is
Brian O'Connoree674952016-09-13 16:31:45 -0700181 analyzer.setJar(jar); // give bnd the contents
182
183 // You can provide additional class path entries to allow
184 // bnd to pickup export version from the packageinfo file,
185 // Version annotation, or their manifests.
186 analyzer.addClasspath(classpath);
187
188 setProperties(analyzer);
189
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700190 // Analyze the target JAR first
191 analyzer.analyze();
Brian O'Connoree674952016-09-13 16:31:45 -0700192
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700193 // Scan the JAR for Felix SCR annotations and generate XML files
194 Map<String, String> properties = Maps.newHashMap();
195 properties.put("destdir", classesDir.toAbsolutePath().toString());
196 SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin();
197 scrDescriptorBndPlugin.setProperties(properties);
198 scrDescriptorBndPlugin.setReporter(analyzer);
199 scrDescriptorBndPlugin.analyzeJar(analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700200
Bharat saraswala899a212017-02-28 13:19:57 +0530201 //Add local packges to jar file.
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700202 //FIXME removing this call for now; not sure what exactly it's doing
203 //addLocalPackages(new File(classesDir.toString()), analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700204
Bharat saraswala899a212017-02-28 13:19:57 +0530205 //add resources.
Brian O'Connoree674952016-09-13 16:31:45 -0700206 if (includeResources != null) {
207 doIncludeResources(analyzer);
208 }
209
210 // Repack the JAR as a WAR
211 doWabStaging(analyzer);
212
213 // Calculate the manifest
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700214 Manifest manifest = analyzer.calcManifest();
215
216 //Build the jar files
217 //FIXME this call conflicts with some of the above
218// analyzer.build();
Brian O'Connoree674952016-09-13 16:31:45 -0700219
220 if (analyzer.isOk()) {
Bharat saraswala899a212017-02-28 13:19:57 +0530221 //add calculated manifest file.
Brian O'Connoree674952016-09-13 16:31:45 -0700222 analyzer.getJar().setManifest(manifest);
223 if (analyzer.save(outputJar.toFile(), true)) {
224 log("Saved!\n");
225 } else {
226 warn("Failed to create jar \n");
227 return false;
228 }
229 } else {
230 warn("Analyzer Errors:\n%s\n", analyzer.getErrors());
231 return false;
232 }
233
234 analyzer.close();
235
236 return true;
237 } catch (Exception e) {
238 e.printStackTrace();
239 return false;
240 }
241 }
242
Bharat saraswala899a212017-02-28 13:19:57 +0530243 private static void addLocalPackages(File outputDirectory, Analyzer analyzer) throws IOException {
244 Packages packages = new Packages();
245
246 if (outputDirectory != null && outputDirectory.isDirectory()) {
247 // scan classes directory for potential packages
248 DirectoryScanner scanner = new DirectoryScanner();
249 scanner.setBasedir(outputDirectory);
250 scanner.setIncludes(new String[]
251 {"**/*.class"});
252
253 scanner.addDefaultExcludes();
254 scanner.scan();
255
256 String[] paths = scanner.getIncludedFiles();
257 for (int i = 0; i < paths.length; i++) {
258 packages.put(analyzer.getPackageRef(getPackageName(paths[i])));
259 }
260 }
261
262 Packages exportedPkgs = new Packages();
263 Packages privatePkgs = new Packages();
264
265 boolean noprivatePackages = "!*".equals(analyzer.getProperty(Analyzer.PRIVATE_PACKAGE));
266
267 for (Descriptors.PackageRef pkg : packages.keySet()) {
268 // mark all source packages as private by default (can be overridden by export list)
269 privatePkgs.put(pkg);
270
271 // we can't export the default package (".") and we shouldn't export internal packages
272 String fqn = pkg.getFQN();
273 if (noprivatePackages || !(".".equals(fqn) || fqn.contains(".internal") || fqn.contains(".impl"))) {
274 exportedPkgs.put(pkg);
275 }
276 }
277
278 Properties properties = analyzer.getProperties();
279 String exported = properties.getProperty(Analyzer.EXPORT_PACKAGE);
280 if (exported == null) {
281 if (!properties.containsKey(Analyzer.EXPORT_CONTENTS)) {
282 // no -exportcontents overriding the exports, so use our computed list
283 for (Attrs attrs : exportedPkgs.values()) {
284 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
285 }
286 properties.setProperty(Analyzer.EXPORT_PACKAGE, Processor.printClauses(exportedPkgs));
287 } else {
288 // leave Export-Package empty (but non-null) as we have -exportcontents
289 properties.setProperty(Analyzer.EXPORT_PACKAGE, "");
290 }
291 }
292
293 String internal = properties.getProperty(Analyzer.PRIVATE_PACKAGE);
294 if (internal == null) {
295 if (!privatePkgs.isEmpty()) {
296 for (Attrs attrs : privatePkgs.values()) {
297 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
298 }
299 properties.setProperty(Analyzer.PRIVATE_PACKAGE, Processor.printClauses(privatePkgs));
300 } else {
301 // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
302 properties.setProperty(Analyzer.PRIVATE_PACKAGE, "!*");
303 }
304 }
305 }
306
307 private static String getPackageName(String filename) {
308 int n = filename.lastIndexOf(File.separatorChar);
309 return n < 0 ? "." : filename.substring(0, n).replace(File.separatorChar, '.');
310 }
311
Brian O'Connoree674952016-09-13 16:31:45 -0700312 private boolean isWab() {
313 return webContext != null;
314 }
315
316 private void doWabStaging(Analyzer analyzer) throws Exception {
317 if (!isWab()) {
318 return;
319 }
320 String wab = analyzer.getProperty(analyzer.WAB);
321 Jar dot = analyzer.getJar();
322
323 log("wab %s", wab);
324 analyzer.setBundleClasspath("WEB-INF/classes," +
Bharat saraswala899a212017-02-28 13:19:57 +0530325 analyzer.getProperty(analyzer.BUNDLE_CLASSPATH));
Brian O'Connoree674952016-09-13 16:31:45 -0700326
327 Set<String> paths = new HashSet<>(dot.getResources().keySet());
328
329 for (String path : paths) {
330 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
331 log("wab: moving: %s", path);
332 dot.rename(path, "WEB-INF/classes/" + path);
333 }
334 }
335
336 Path wabRoot = Paths.get(wab);
337 includeFiles(dot, null, wabRoot.toString());
338 }
339
340 /**
341 * Parse the Bundle-Includes header. Files in the bundles Include header are
342 * included in the jar. The source can be a directory or a file.
343 *
344 * @throws Exception
345 */
346 private void doIncludeResources(Analyzer analyzer) throws Exception {
347 String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE);
348 if (includes == null) {
349 return;
350 }
351 Parameters clauses = analyzer.parseHeader(includes);
352 Jar jar = analyzer.getJar();
353
354 for (Map.Entry<String, Attrs> entry : clauses.entrySet()) {
355 String name = entry.getKey();
356 Map<String, String> extra = entry.getValue();
357 // TODO consider doing something with extras
358
359 String[] parts = name.split("\\s*=\\s*");
360 String source = parts[0];
361 String destination = parts[0];
362 if (parts.length == 2) {
363 source = parts[1];
364 }
365
366 includeFiles(jar, destination, source);
367 }
368 }
369
370 private void includeFiles(Jar jar, String destinationRoot, String sourceRoot)
371 throws IOException {
372
373 Path classesBasedPath = classesDir.resolve(sourceRoot);
374 Path sourceBasedPath = sourcesDir.resolve(sourceRoot);
375
376 File classFile = classesBasedPath.toFile();
377 File sourceFile = sourceBasedPath.toFile();
378
379 if (classFile.isFile()) {
380 addFileToJar(jar, destinationRoot, classesBasedPath.toAbsolutePath().toString());
381 } else if (sourceFile.isFile()) {
382 addFileToJar(jar, destinationRoot, sourceBasedPath.toAbsolutePath().toString());
383 } else if (classFile.isDirectory()) {
384 includeDirectory(jar, destinationRoot, classesBasedPath);
385 } else if (sourceFile.isDirectory()) {
386 includeDirectory(jar, destinationRoot, sourceBasedPath);
387 } else {
388 warn("Skipping resource in bundle %s: %s (File Not Found)\n",
389 bundleSymbolicName, sourceRoot);
390 }
391 }
392
393 private void includeDirectory(Jar jar, String destinationRoot, Path sourceRoot)
394 throws IOException {
395 // iterate through sources
396 // put each source on the jar
397 FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
398 @Override
399 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
400 Path relativePath = sourceRoot.relativize(file);
401 String destination = destinationRoot != null ?
402 destinationRoot + "/" + relativePath.toString() : //TODO
403 relativePath.toString();
404
405 addFileToJar(jar, destination, file.toAbsolutePath().toString());
406 return FileVisitResult.CONTINUE;
407 }
408 };
409
410 walkFileTree(sourceRoot, visitor);
411 }
412
413 private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) {
414 if (includedResources.contains(sourceAbsPath)) {
415 log("Skipping already included resource: %s\n", sourceAbsPath);
416 return false;
417 }
418 File file = new File(sourceAbsPath);
419 if (!file.isFile()) {
420 throw new RuntimeException(
421 String.format("Skipping non-existent file: %s\n", sourceAbsPath));
422 }
423 Resource resource = new FileResource(file);
424 if (jar.getResource(destination) != null) {
425 warn("Skipping duplicate resource: %s\n", destination);
426 return false;
427 }
428 jar.putResource(destination, resource);
429 includedResources.add(sourceAbsPath);
430 log("Adding resource: %s\n", destination);
431 return true;
432 }
433
434 private void log(String format, Object... objects) {
435 //System.err.printf(format, objects);
436 }
437
438 private void warn(String format, Object... objects) {
439 stderr.printf(format, objects);
440 }
441
442 @Override
443 public String toString() {
444 return MoreObjects.toStringHelper(this)
445 .add("inputJar", inputJar)
446 .add("outputJar", outputJar)
447 .add("classpath", classpath)
448 .add("bundleName", bundleName)
449 .add("groupId", groupId)
450 .add("bundleSymbolicName", bundleSymbolicName)
451 .add("bundleVersion", bundleVersion)
452 .add("bundleDescription", bundleDescription)
453 .add("bundleLicense", bundleLicense)
454 .toString();
Brian O'Connoree674952016-09-13 16:31:45 -0700455 }
456
457 @Override
458 public StepExecutionResult execute(ExecutionContext executionContext)
459 throws IOException, InterruptedException {
460 stderr = executionContext.getStdErr();
461 boolean success = execute();
462 stderr = System.err;
463 return success ? StepExecutionResult.SUCCESS : StepExecutionResult.ERROR;
464 }
465
466 @Override
467 public String getShortName() {
468 return "osgiwrap";
469 }
470
471 @Override
472 public String getDescription(ExecutionContext executionContext) {
473 return "osgiwrap"; //FIXME
474 }
475}