blob: 86c693819210495d1426d5edc053aa231d9ec71c [file] [log] [blame]
Brian O'Connoree674952016-09-13 16:31:45 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016-present Open Networking Foundation
Brian O'Connoree674952016-09-13 16:31:45 -07003 *
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;
Viswanath KSP3568df72017-05-11 13:52:25 +053034import com.google.common.base.Strings;
Brian O'Connoree674952016-09-13 16:31:45 -070035import com.google.common.collect.ImmutableSortedSet;
36import com.google.common.collect.Lists;
37import com.google.common.collect.Maps;
38import com.google.common.collect.Sets;
39import org.apache.felix.scrplugin.bnd.SCRDescriptorBndPlugin;
Bharat saraswala899a212017-02-28 13:19:57 +053040import org.codehaus.plexus.util.DirectoryScanner;
Brian O'Connoree674952016-09-13 16:31:45 -070041
42import java.io.File;
43import java.io.IOException;
44import java.io.PrintStream;
45import java.nio.file.FileVisitResult;
46import java.nio.file.FileVisitor;
47import java.nio.file.Path;
48import java.nio.file.Paths;
49import java.nio.file.SimpleFileVisitor;
50import java.nio.file.attribute.BasicFileAttributes;
51import java.util.HashSet;
52import java.util.List;
53import java.util.Map;
Bharat saraswala899a212017-02-28 13:19:57 +053054import java.util.Properties;
Brian O'Connoree674952016-09-13 16:31:45 -070055import java.util.Set;
56import java.util.jar.Manifest;
57import java.util.stream.Collectors;
58
59import static java.nio.file.Files.walkFileTree;
60
61/**
62 * BND-based wrapper to convert Buck JARs to OSGi-compatible JARs.
63 */
64public class OSGiWrapper implements Step {
65
Jian Li91b47392018-02-19 02:15:41 +090066 private static final String EMBED_DEPENDENCY = "Embed-Dependency";
67 private static final String EMBEDDED_ARTIFACTS = "Embedded-Artifacts";
68
Brian O'Connoree674952016-09-13 16:31:45 -070069 private Path inputJar;
70 private Path outputJar;
71 private Path sourcesDir;
72 private Path classesDir;
73 private List<String> classpath;
74
75 private String bundleName;
76 private String groupId;
77 private String bundleSymbolicName;
78 private String bundleVersion;
79
80 private String importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +053081 private String privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -070082 private String dynamicimportPackages;
Viswanath KSP3568df72017-05-11 13:52:25 +053083 private String embeddedDependencies;
Brian O'Connoree674952016-09-13 16:31:45 -070084
85 private String exportPackages;
86 private String includeResources;
87 private Set<String> includedResources = Sets.newHashSet();
88
89 private String bundleDescription;
90 private String bundleLicense;
91
Jian Li91b47392018-02-19 02:15:41 +090092 private String bundleClasspath;
93
Brian O'Connoree674952016-09-13 16:31:45 -070094 private String webContext;
95
96 private PrintStream stderr = System.err;
97
98 public OSGiWrapper(Path inputJar,
99 Path outputJar,
100 Path sourcesDir,
101 Path classesDir,
102 ImmutableSortedSet<Path> classpath,
103 String bundleName,
104 String groupId,
105 String bundleVersion,
106 String bundleLicense,
107 String importPackages,
108 String exportPackages,
109 String includeResources,
110 String webContext,
111 String dynamicimportPackages,
Viswanath KSP3568df72017-05-11 13:52:25 +0530112 String embeddedDependencies,
Bharat saraswala899a212017-02-28 13:19:57 +0530113 String bundleDescription,
Jian Li91b47392018-02-19 02:15:41 +0900114 String privatePackages,
115 String bundleClasspath) {
Brian O'Connoree674952016-09-13 16:31:45 -0700116 this.inputJar = inputJar;
117 this.sourcesDir = sourcesDir;
118 this.classesDir = classesDir;
119 this.classpath = Lists.newArrayList(
120 classpath.stream().map(Path::toString).collect(Collectors.toList()));
121 if (!this.classpath.contains(inputJar.toString())) {
122 this.classpath.add(0, inputJar.toString());
123 }
124 this.outputJar = outputJar;
125
126 this.bundleName = bundleName;
127 this.groupId = groupId;
128 this.bundleSymbolicName = String.format("%s.%s", groupId, bundleName);
129
130 this.bundleVersion = bundleVersion;
131 this.bundleLicense = bundleLicense;
132 this.bundleDescription = bundleDescription;
133
134 this.importPackages = importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +0530135 this.privatePackages = privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -0700136 this.dynamicimportPackages = dynamicimportPackages;
Viswanath KSP3568df72017-05-11 13:52:25 +0530137 this.embeddedDependencies = embeddedDependencies;
Brian O'Connoree674952016-09-13 16:31:45 -0700138 this.exportPackages = exportPackages;
139 this.includeResources = includeResources;
140
141 this.webContext = webContext;
Jian Li91b47392018-02-19 02:15:41 +0900142
143 this.bundleClasspath = bundleClasspath;
Brian O'Connoree674952016-09-13 16:31:45 -0700144 }
145
146 private void setProperties(Analyzer analyzer) {
Jian Li91b47392018-02-19 02:15:41 +0900147
Brian O'Connoree674952016-09-13 16:31:45 -0700148 analyzer.setProperty(Analyzer.BUNDLE_NAME, bundleName);
149 analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME, bundleSymbolicName);
150 analyzer.setProperty(Analyzer.BUNDLE_VERSION, bundleVersion.replace('-', '.'));
151
152 if (bundleDescription != null) {
153 analyzer.setProperty(Analyzer.BUNDLE_DESCRIPTION, bundleDescription);
154 }
155 if (bundleLicense != null) {
156 analyzer.setProperty(Analyzer.BUNDLE_LICENSE, bundleLicense);
157 }
158
159 //TODO consider using stricter version policy
160 //analyzer.setProperty("-provider-policy", "${range;[===,==+)}");
161 //analyzer.setProperty("-consumer-policy", "${range;[===,==+)}");
162
Bharat saraswala899a212017-02-28 13:19:57 +0530163 if (privatePackages != null) {
164 analyzer.setProperty(Analyzer.PRIVATE_PACKAGE, privatePackages);
165 }
166 analyzer.setProperty(Analyzer.REMOVEHEADERS, "Private-Package,Include-Resource");
Brian O'Connoree674952016-09-13 16:31:45 -0700167
Bharat saraswala899a212017-02-28 13:19:57 +0530168 analyzer.setProperty(Analyzer.DYNAMICIMPORT_PACKAGE,
169 dynamicimportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700170
171 // TODO include version in export, but not in import
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700172 analyzer.setProperty(Analyzer.EXPORT_PACKAGE, exportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700173
174 // TODO we may need INCLUDE_RESOURCE, or that might be done by Buck
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700175 // FIXME NOTE we handle this manually below
Brian O'Connoree674952016-09-13 16:31:45 -0700176 if (includeResources != null) {
177 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources);
178 }
179
Jian Li91b47392018-02-19 02:15:41 +0900180 if (embeddedDependencies != null) {
181 analyzer.setProperty(EMBED_DEPENDENCY, embeddedDependencies);
Viswanath KSP3568df72017-05-11 13:52:25 +0530182 }
183
Jian Li91b47392018-02-19 02:15:41 +0900184 // There are no good defaults so make sure you set the Import-Package
185 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages);
186
Brian O'Connoree674952016-09-13 16:31:45 -0700187 if (isWab()) {
188 analyzer.setProperty(Analyzer.WAB, "src/main/webapp/");
189 analyzer.setProperty("Web-ContextPath", webContext);
Jian Li91b47392018-02-19 02:15:41 +0900190 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages +
191 ",org.glassfish.jersey.servlet,org.jvnet.mimepull\n");
Brian O'Connoree674952016-09-13 16:31:45 -0700192 }
193 }
194
195 public boolean execute() {
Bharat saraswala899a212017-02-28 13:19:57 +0530196 Builder analyzer = new Builder();
Brian O'Connoree674952016-09-13 16:31:45 -0700197 try {
198
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700199 Jar jar = new Jar(inputJar.toFile()); // where our data is
Brian O'Connoree674952016-09-13 16:31:45 -0700200 analyzer.setJar(jar); // give bnd the contents
201
202 // You can provide additional class path entries to allow
203 // bnd to pickup export version from the packageinfo file,
204 // Version annotation, or their manifests.
205 analyzer.addClasspath(classpath);
206
207 setProperties(analyzer);
208
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700209 // Analyze the target JAR first
210 analyzer.analyze();
Brian O'Connoree674952016-09-13 16:31:45 -0700211
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700212 // Scan the JAR for Felix SCR annotations and generate XML files
213 Map<String, String> properties = Maps.newHashMap();
214 properties.put("destdir", classesDir.toAbsolutePath().toString());
215 SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin();
216 scrDescriptorBndPlugin.setProperties(properties);
217 scrDescriptorBndPlugin.setReporter(analyzer);
218 scrDescriptorBndPlugin.analyzeJar(analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700219
Bharat saraswala899a212017-02-28 13:19:57 +0530220 //Add local packges to jar file.
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700221 //FIXME removing this call for now; not sure what exactly it's doing
222 //addLocalPackages(new File(classesDir.toString()), analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700223
Bharat saraswala899a212017-02-28 13:19:57 +0530224 //add resources.
Viswanath KSP3568df72017-05-11 13:52:25 +0530225 if (includeResources != null || embeddedDependencies != null) {
Brian O'Connoree674952016-09-13 16:31:45 -0700226 doIncludeResources(analyzer);
227 }
228
229 // Repack the JAR as a WAR
230 doWabStaging(analyzer);
231
232 // Calculate the manifest
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700233 Manifest manifest = analyzer.calcManifest();
234
235 //Build the jar files
236 //FIXME this call conflicts with some of the above
237// analyzer.build();
Brian O'Connoree674952016-09-13 16:31:45 -0700238
239 if (analyzer.isOk()) {
Bharat saraswala899a212017-02-28 13:19:57 +0530240 //add calculated manifest file.
Brian O'Connoree674952016-09-13 16:31:45 -0700241 analyzer.getJar().setManifest(manifest);
242 if (analyzer.save(outputJar.toFile(), true)) {
243 log("Saved!\n");
244 } else {
245 warn("Failed to create jar \n");
246 return false;
247 }
248 } else {
249 warn("Analyzer Errors:\n%s\n", analyzer.getErrors());
250 return false;
251 }
252
253 analyzer.close();
254
255 return true;
256 } catch (Exception e) {
257 e.printStackTrace();
258 return false;
259 }
260 }
261
Bharat saraswala899a212017-02-28 13:19:57 +0530262 private static void addLocalPackages(File outputDirectory, Analyzer analyzer) throws IOException {
263 Packages packages = new Packages();
264
265 if (outputDirectory != null && outputDirectory.isDirectory()) {
266 // scan classes directory for potential packages
267 DirectoryScanner scanner = new DirectoryScanner();
268 scanner.setBasedir(outputDirectory);
269 scanner.setIncludes(new String[]
270 {"**/*.class"});
271
272 scanner.addDefaultExcludes();
273 scanner.scan();
274
275 String[] paths = scanner.getIncludedFiles();
276 for (int i = 0; i < paths.length; i++) {
277 packages.put(analyzer.getPackageRef(getPackageName(paths[i])));
278 }
279 }
280
281 Packages exportedPkgs = new Packages();
282 Packages privatePkgs = new Packages();
283
284 boolean noprivatePackages = "!*".equals(analyzer.getProperty(Analyzer.PRIVATE_PACKAGE));
285
286 for (Descriptors.PackageRef pkg : packages.keySet()) {
287 // mark all source packages as private by default (can be overridden by export list)
288 privatePkgs.put(pkg);
289
290 // we can't export the default package (".") and we shouldn't export internal packages
291 String fqn = pkg.getFQN();
292 if (noprivatePackages || !(".".equals(fqn) || fqn.contains(".internal") || fqn.contains(".impl"))) {
293 exportedPkgs.put(pkg);
294 }
295 }
296
297 Properties properties = analyzer.getProperties();
298 String exported = properties.getProperty(Analyzer.EXPORT_PACKAGE);
299 if (exported == null) {
300 if (!properties.containsKey(Analyzer.EXPORT_CONTENTS)) {
301 // no -exportcontents overriding the exports, so use our computed list
302 for (Attrs attrs : exportedPkgs.values()) {
303 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
304 }
305 properties.setProperty(Analyzer.EXPORT_PACKAGE, Processor.printClauses(exportedPkgs));
306 } else {
307 // leave Export-Package empty (but non-null) as we have -exportcontents
308 properties.setProperty(Analyzer.EXPORT_PACKAGE, "");
309 }
310 }
311
312 String internal = properties.getProperty(Analyzer.PRIVATE_PACKAGE);
313 if (internal == null) {
314 if (!privatePkgs.isEmpty()) {
315 for (Attrs attrs : privatePkgs.values()) {
316 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
317 }
318 properties.setProperty(Analyzer.PRIVATE_PACKAGE, Processor.printClauses(privatePkgs));
319 } else {
320 // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
321 properties.setProperty(Analyzer.PRIVATE_PACKAGE, "!*");
322 }
323 }
324 }
325
326 private static String getPackageName(String filename) {
327 int n = filename.lastIndexOf(File.separatorChar);
328 return n < 0 ? "." : filename.substring(0, n).replace(File.separatorChar, '.');
329 }
330
Brian O'Connoree674952016-09-13 16:31:45 -0700331 private boolean isWab() {
332 return webContext != null;
333 }
334
335 private void doWabStaging(Analyzer analyzer) throws Exception {
336 if (!isWab()) {
337 return;
338 }
339 String wab = analyzer.getProperty(analyzer.WAB);
340 Jar dot = analyzer.getJar();
341
342 log("wab %s", wab);
Jian Li91b47392018-02-19 02:15:41 +0900343
344 if (bundleClasspath != null) {
345 analyzer.setBundleClasspath("WEB-INF/classes," + bundleClasspath);
346 }
Brian O'Connoree674952016-09-13 16:31:45 -0700347
348 Set<String> paths = new HashSet<>(dot.getResources().keySet());
349
350 for (String path : paths) {
351 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
352 log("wab: moving: %s", path);
353 dot.rename(path, "WEB-INF/classes/" + path);
354 }
355 }
356
357 Path wabRoot = Paths.get(wab);
358 includeFiles(dot, null, wabRoot.toString());
359 }
360
361 /**
362 * Parse the Bundle-Includes header. Files in the bundles Include header are
363 * included in the jar. The source can be a directory or a file.
364 *
365 * @throws Exception
366 */
367 private void doIncludeResources(Analyzer analyzer) throws Exception {
368 String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE);
369 if (includes == null) {
370 return;
371 }
372 Parameters clauses = analyzer.parseHeader(includes);
373 Jar jar = analyzer.getJar();
374
375 for (Map.Entry<String, Attrs> entry : clauses.entrySet()) {
376 String name = entry.getKey();
377 Map<String, String> extra = entry.getValue();
378 // TODO consider doing something with extras
379
380 String[] parts = name.split("\\s*=\\s*");
381 String source = parts[0];
382 String destination = parts[0];
383 if (parts.length == 2) {
384 source = parts[1];
385 }
386
387 includeFiles(jar, destination, source);
388 }
389 }
390
391 private void includeFiles(Jar jar, String destinationRoot, String sourceRoot)
392 throws IOException {
393
394 Path classesBasedPath = classesDir.resolve(sourceRoot);
395 Path sourceBasedPath = sourcesDir.resolve(sourceRoot);
396
397 File classFile = classesBasedPath.toFile();
398 File sourceFile = sourceBasedPath.toFile();
399
400 if (classFile.isFile()) {
401 addFileToJar(jar, destinationRoot, classesBasedPath.toAbsolutePath().toString());
402 } else if (sourceFile.isFile()) {
403 addFileToJar(jar, destinationRoot, sourceBasedPath.toAbsolutePath().toString());
404 } else if (classFile.isDirectory()) {
405 includeDirectory(jar, destinationRoot, classesBasedPath);
406 } else if (sourceFile.isDirectory()) {
407 includeDirectory(jar, destinationRoot, sourceBasedPath);
408 } else {
409 warn("Skipping resource in bundle %s: %s (File Not Found)\n",
410 bundleSymbolicName, sourceRoot);
411 }
412 }
413
414 private void includeDirectory(Jar jar, String destinationRoot, Path sourceRoot)
415 throws IOException {
416 // iterate through sources
417 // put each source on the jar
418 FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
419 @Override
420 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
421 Path relativePath = sourceRoot.relativize(file);
422 String destination = destinationRoot != null ?
423 destinationRoot + "/" + relativePath.toString() : //TODO
424 relativePath.toString();
425
426 addFileToJar(jar, destination, file.toAbsolutePath().toString());
427 return FileVisitResult.CONTINUE;
428 }
429 };
430
431 walkFileTree(sourceRoot, visitor);
432 }
433
434 private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) {
435 if (includedResources.contains(sourceAbsPath)) {
436 log("Skipping already included resource: %s\n", sourceAbsPath);
437 return false;
438 }
439 File file = new File(sourceAbsPath);
440 if (!file.isFile()) {
441 throw new RuntimeException(
442 String.format("Skipping non-existent file: %s\n", sourceAbsPath));
443 }
444 Resource resource = new FileResource(file);
445 if (jar.getResource(destination) != null) {
446 warn("Skipping duplicate resource: %s\n", destination);
447 return false;
448 }
449 jar.putResource(destination, resource);
450 includedResources.add(sourceAbsPath);
451 log("Adding resource: %s\n", destination);
452 return true;
453 }
454
455 private void log(String format, Object... objects) {
456 //System.err.printf(format, objects);
457 }
458
459 private void warn(String format, Object... objects) {
460 stderr.printf(format, objects);
461 }
462
463 @Override
464 public String toString() {
465 return MoreObjects.toStringHelper(this)
466 .add("inputJar", inputJar)
467 .add("outputJar", outputJar)
468 .add("classpath", classpath)
469 .add("bundleName", bundleName)
470 .add("groupId", groupId)
471 .add("bundleSymbolicName", bundleSymbolicName)
472 .add("bundleVersion", bundleVersion)
473 .add("bundleDescription", bundleDescription)
474 .add("bundleLicense", bundleLicense)
475 .toString();
Brian O'Connoree674952016-09-13 16:31:45 -0700476 }
477
478 @Override
479 public StepExecutionResult execute(ExecutionContext executionContext)
480 throws IOException, InterruptedException {
481 stderr = executionContext.getStdErr();
482 boolean success = execute();
483 stderr = System.err;
484 return success ? StepExecutionResult.SUCCESS : StepExecutionResult.ERROR;
485 }
486
487 @Override
488 public String getShortName() {
489 return "osgiwrap";
490 }
491
492 @Override
493 public String getDescription(ExecutionContext executionContext) {
494 return "osgiwrap"; //FIXME
495 }
496}