blob: 5a7de19be5b451be5af455aa1b0a3ee4853f2cfe [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
66 private Path inputJar;
67 private Path outputJar;
68 private Path sourcesDir;
69 private Path classesDir;
70 private List<String> classpath;
71
72 private String bundleName;
73 private String groupId;
74 private String bundleSymbolicName;
75 private String bundleVersion;
76
77 private String importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +053078 private String privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -070079 private String dynamicimportPackages;
Viswanath KSP3568df72017-05-11 13:52:25 +053080 private String embeddedDependencies;
Brian O'Connoree674952016-09-13 16:31:45 -070081
82 private String exportPackages;
83 private String includeResources;
84 private Set<String> includedResources = Sets.newHashSet();
85
86 private String bundleDescription;
87 private String bundleLicense;
88
89 private String webContext;
90
91 private PrintStream stderr = System.err;
92
93 public OSGiWrapper(Path inputJar,
94 Path outputJar,
95 Path sourcesDir,
96 Path classesDir,
97 ImmutableSortedSet<Path> classpath,
98 String bundleName,
99 String groupId,
100 String bundleVersion,
101 String bundleLicense,
102 String importPackages,
103 String exportPackages,
104 String includeResources,
105 String webContext,
106 String dynamicimportPackages,
Viswanath KSP3568df72017-05-11 13:52:25 +0530107 String embeddedDependencies,
Bharat saraswala899a212017-02-28 13:19:57 +0530108 String bundleDescription,
109 String privatePackages) {
Brian O'Connoree674952016-09-13 16:31:45 -0700110 this.inputJar = inputJar;
111 this.sourcesDir = sourcesDir;
112 this.classesDir = classesDir;
113 this.classpath = Lists.newArrayList(
114 classpath.stream().map(Path::toString).collect(Collectors.toList()));
115 if (!this.classpath.contains(inputJar.toString())) {
116 this.classpath.add(0, inputJar.toString());
117 }
118 this.outputJar = outputJar;
119
120 this.bundleName = bundleName;
121 this.groupId = groupId;
122 this.bundleSymbolicName = String.format("%s.%s", groupId, bundleName);
123
124 this.bundleVersion = bundleVersion;
125 this.bundleLicense = bundleLicense;
126 this.bundleDescription = bundleDescription;
127
128 this.importPackages = importPackages;
Bharat saraswala899a212017-02-28 13:19:57 +0530129 this.privatePackages = privatePackages;
Brian O'Connoree674952016-09-13 16:31:45 -0700130 this.dynamicimportPackages = dynamicimportPackages;
Viswanath KSP3568df72017-05-11 13:52:25 +0530131 this.embeddedDependencies = embeddedDependencies;
Brian O'Connoree674952016-09-13 16:31:45 -0700132 this.exportPackages = exportPackages;
133 this.includeResources = includeResources;
134
135 this.webContext = webContext;
136 }
137
138 private void setProperties(Analyzer analyzer) {
139 analyzer.setProperty(Analyzer.BUNDLE_NAME, bundleName);
140 analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME, bundleSymbolicName);
141 analyzer.setProperty(Analyzer.BUNDLE_VERSION, bundleVersion.replace('-', '.'));
142
143 if (bundleDescription != null) {
144 analyzer.setProperty(Analyzer.BUNDLE_DESCRIPTION, bundleDescription);
145 }
146 if (bundleLicense != null) {
147 analyzer.setProperty(Analyzer.BUNDLE_LICENSE, bundleLicense);
148 }
149
150 //TODO consider using stricter version policy
151 //analyzer.setProperty("-provider-policy", "${range;[===,==+)}");
152 //analyzer.setProperty("-consumer-policy", "${range;[===,==+)}");
153
154 // There are no good defaults so make sure you set the Import-Package
155 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages);
Bharat saraswala899a212017-02-28 13:19:57 +0530156 if (privatePackages != null) {
157 analyzer.setProperty(Analyzer.PRIVATE_PACKAGE, privatePackages);
158 }
159 analyzer.setProperty(Analyzer.REMOVEHEADERS, "Private-Package,Include-Resource");
Brian O'Connoree674952016-09-13 16:31:45 -0700160
Bharat saraswala899a212017-02-28 13:19:57 +0530161 analyzer.setProperty(Analyzer.DYNAMICIMPORT_PACKAGE,
162 dynamicimportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700163
164 // TODO include version in export, but not in import
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700165 analyzer.setProperty(Analyzer.EXPORT_PACKAGE, exportPackages);
Brian O'Connoree674952016-09-13 16:31:45 -0700166
167 // TODO we may need INCLUDE_RESOURCE, or that might be done by Buck
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700168 // FIXME NOTE we handle this manually below
Brian O'Connoree674952016-09-13 16:31:45 -0700169 if (includeResources != null) {
170 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources);
171 }
172
Viswanath KSP3568df72017-05-11 13:52:25 +0530173 if(embeddedDependencies != null) {
174 analyzer.setProperty(Analyzer.BUNDLE_CLASSPATH,
175 embeddedDependencies);
176 String finalIncludes = Strings.isNullOrEmpty(includeResources) ?
177 embeddedDependencies : (includeResources+","+embeddedDependencies);
178 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE,
179 finalIncludes);
180 }
181
Brian O'Connoree674952016-09-13 16:31:45 -0700182 if (isWab()) {
183 analyzer.setProperty(Analyzer.WAB, "src/main/webapp/");
184 analyzer.setProperty("Web-ContextPath", webContext);
185 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, "*,org.glassfish.jersey.servlet,org.jvnet.mimepull\n");
186 }
187 }
188
189 public boolean execute() {
Bharat saraswala899a212017-02-28 13:19:57 +0530190 Builder analyzer = new Builder();
Brian O'Connoree674952016-09-13 16:31:45 -0700191 try {
192
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700193 Jar jar = new Jar(inputJar.toFile()); // where our data is
Brian O'Connoree674952016-09-13 16:31:45 -0700194 analyzer.setJar(jar); // give bnd the contents
195
196 // You can provide additional class path entries to allow
197 // bnd to pickup export version from the packageinfo file,
198 // Version annotation, or their manifests.
199 analyzer.addClasspath(classpath);
200
201 setProperties(analyzer);
202
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700203 // Analyze the target JAR first
204 analyzer.analyze();
Brian O'Connoree674952016-09-13 16:31:45 -0700205
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700206 // Scan the JAR for Felix SCR annotations and generate XML files
207 Map<String, String> properties = Maps.newHashMap();
208 properties.put("destdir", classesDir.toAbsolutePath().toString());
209 SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin();
210 scrDescriptorBndPlugin.setProperties(properties);
211 scrDescriptorBndPlugin.setReporter(analyzer);
212 scrDescriptorBndPlugin.analyzeJar(analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700213
Bharat saraswala899a212017-02-28 13:19:57 +0530214 //Add local packges to jar file.
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700215 //FIXME removing this call for now; not sure what exactly it's doing
216 //addLocalPackages(new File(classesDir.toString()), analyzer);
Brian O'Connoree674952016-09-13 16:31:45 -0700217
Bharat saraswala899a212017-02-28 13:19:57 +0530218 //add resources.
Viswanath KSP3568df72017-05-11 13:52:25 +0530219 if (includeResources != null || embeddedDependencies != null) {
Brian O'Connoree674952016-09-13 16:31:45 -0700220 doIncludeResources(analyzer);
221 }
222
223 // Repack the JAR as a WAR
224 doWabStaging(analyzer);
225
226 // Calculate the manifest
Brian O'Connor1b55bfb2017-03-21 15:30:38 -0700227 Manifest manifest = analyzer.calcManifest();
228
229 //Build the jar files
230 //FIXME this call conflicts with some of the above
231// analyzer.build();
Brian O'Connoree674952016-09-13 16:31:45 -0700232
233 if (analyzer.isOk()) {
Bharat saraswala899a212017-02-28 13:19:57 +0530234 //add calculated manifest file.
Brian O'Connoree674952016-09-13 16:31:45 -0700235 analyzer.getJar().setManifest(manifest);
236 if (analyzer.save(outputJar.toFile(), true)) {
237 log("Saved!\n");
238 } else {
239 warn("Failed to create jar \n");
240 return false;
241 }
242 } else {
243 warn("Analyzer Errors:\n%s\n", analyzer.getErrors());
244 return false;
245 }
246
247 analyzer.close();
248
249 return true;
250 } catch (Exception e) {
251 e.printStackTrace();
252 return false;
253 }
254 }
255
Bharat saraswala899a212017-02-28 13:19:57 +0530256 private static void addLocalPackages(File outputDirectory, Analyzer analyzer) throws IOException {
257 Packages packages = new Packages();
258
259 if (outputDirectory != null && outputDirectory.isDirectory()) {
260 // scan classes directory for potential packages
261 DirectoryScanner scanner = new DirectoryScanner();
262 scanner.setBasedir(outputDirectory);
263 scanner.setIncludes(new String[]
264 {"**/*.class"});
265
266 scanner.addDefaultExcludes();
267 scanner.scan();
268
269 String[] paths = scanner.getIncludedFiles();
270 for (int i = 0; i < paths.length; i++) {
271 packages.put(analyzer.getPackageRef(getPackageName(paths[i])));
272 }
273 }
274
275 Packages exportedPkgs = new Packages();
276 Packages privatePkgs = new Packages();
277
278 boolean noprivatePackages = "!*".equals(analyzer.getProperty(Analyzer.PRIVATE_PACKAGE));
279
280 for (Descriptors.PackageRef pkg : packages.keySet()) {
281 // mark all source packages as private by default (can be overridden by export list)
282 privatePkgs.put(pkg);
283
284 // we can't export the default package (".") and we shouldn't export internal packages
285 String fqn = pkg.getFQN();
286 if (noprivatePackages || !(".".equals(fqn) || fqn.contains(".internal") || fqn.contains(".impl"))) {
287 exportedPkgs.put(pkg);
288 }
289 }
290
291 Properties properties = analyzer.getProperties();
292 String exported = properties.getProperty(Analyzer.EXPORT_PACKAGE);
293 if (exported == null) {
294 if (!properties.containsKey(Analyzer.EXPORT_CONTENTS)) {
295 // no -exportcontents overriding the exports, so use our computed list
296 for (Attrs attrs : exportedPkgs.values()) {
297 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
298 }
299 properties.setProperty(Analyzer.EXPORT_PACKAGE, Processor.printClauses(exportedPkgs));
300 } else {
301 // leave Export-Package empty (but non-null) as we have -exportcontents
302 properties.setProperty(Analyzer.EXPORT_PACKAGE, "");
303 }
304 }
305
306 String internal = properties.getProperty(Analyzer.PRIVATE_PACKAGE);
307 if (internal == null) {
308 if (!privatePkgs.isEmpty()) {
309 for (Attrs attrs : privatePkgs.values()) {
310 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
311 }
312 properties.setProperty(Analyzer.PRIVATE_PACKAGE, Processor.printClauses(privatePkgs));
313 } else {
314 // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
315 properties.setProperty(Analyzer.PRIVATE_PACKAGE, "!*");
316 }
317 }
318 }
319
320 private static String getPackageName(String filename) {
321 int n = filename.lastIndexOf(File.separatorChar);
322 return n < 0 ? "." : filename.substring(0, n).replace(File.separatorChar, '.');
323 }
324
Brian O'Connoree674952016-09-13 16:31:45 -0700325 private boolean isWab() {
326 return webContext != null;
327 }
328
329 private void doWabStaging(Analyzer analyzer) throws Exception {
330 if (!isWab()) {
331 return;
332 }
333 String wab = analyzer.getProperty(analyzer.WAB);
334 Jar dot = analyzer.getJar();
335
336 log("wab %s", wab);
337 analyzer.setBundleClasspath("WEB-INF/classes," +
Bharat saraswala899a212017-02-28 13:19:57 +0530338 analyzer.getProperty(analyzer.BUNDLE_CLASSPATH));
Brian O'Connoree674952016-09-13 16:31:45 -0700339
340 Set<String> paths = new HashSet<>(dot.getResources().keySet());
341
342 for (String path : paths) {
343 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
344 log("wab: moving: %s", path);
345 dot.rename(path, "WEB-INF/classes/" + path);
346 }
347 }
348
349 Path wabRoot = Paths.get(wab);
350 includeFiles(dot, null, wabRoot.toString());
351 }
352
353 /**
354 * Parse the Bundle-Includes header. Files in the bundles Include header are
355 * included in the jar. The source can be a directory or a file.
356 *
357 * @throws Exception
358 */
359 private void doIncludeResources(Analyzer analyzer) throws Exception {
360 String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE);
361 if (includes == null) {
362 return;
363 }
364 Parameters clauses = analyzer.parseHeader(includes);
365 Jar jar = analyzer.getJar();
366
367 for (Map.Entry<String, Attrs> entry : clauses.entrySet()) {
368 String name = entry.getKey();
369 Map<String, String> extra = entry.getValue();
370 // TODO consider doing something with extras
371
372 String[] parts = name.split("\\s*=\\s*");
373 String source = parts[0];
374 String destination = parts[0];
375 if (parts.length == 2) {
376 source = parts[1];
377 }
378
379 includeFiles(jar, destination, source);
380 }
381 }
382
383 private void includeFiles(Jar jar, String destinationRoot, String sourceRoot)
384 throws IOException {
385
386 Path classesBasedPath = classesDir.resolve(sourceRoot);
387 Path sourceBasedPath = sourcesDir.resolve(sourceRoot);
388
389 File classFile = classesBasedPath.toFile();
390 File sourceFile = sourceBasedPath.toFile();
391
392 if (classFile.isFile()) {
393 addFileToJar(jar, destinationRoot, classesBasedPath.toAbsolutePath().toString());
394 } else if (sourceFile.isFile()) {
395 addFileToJar(jar, destinationRoot, sourceBasedPath.toAbsolutePath().toString());
396 } else if (classFile.isDirectory()) {
397 includeDirectory(jar, destinationRoot, classesBasedPath);
398 } else if (sourceFile.isDirectory()) {
399 includeDirectory(jar, destinationRoot, sourceBasedPath);
400 } else {
401 warn("Skipping resource in bundle %s: %s (File Not Found)\n",
402 bundleSymbolicName, sourceRoot);
403 }
404 }
405
406 private void includeDirectory(Jar jar, String destinationRoot, Path sourceRoot)
407 throws IOException {
408 // iterate through sources
409 // put each source on the jar
410 FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
411 @Override
412 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
413 Path relativePath = sourceRoot.relativize(file);
414 String destination = destinationRoot != null ?
415 destinationRoot + "/" + relativePath.toString() : //TODO
416 relativePath.toString();
417
418 addFileToJar(jar, destination, file.toAbsolutePath().toString());
419 return FileVisitResult.CONTINUE;
420 }
421 };
422
423 walkFileTree(sourceRoot, visitor);
424 }
425
426 private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) {
427 if (includedResources.contains(sourceAbsPath)) {
428 log("Skipping already included resource: %s\n", sourceAbsPath);
429 return false;
430 }
431 File file = new File(sourceAbsPath);
432 if (!file.isFile()) {
433 throw new RuntimeException(
434 String.format("Skipping non-existent file: %s\n", sourceAbsPath));
435 }
436 Resource resource = new FileResource(file);
437 if (jar.getResource(destination) != null) {
438 warn("Skipping duplicate resource: %s\n", destination);
439 return false;
440 }
441 jar.putResource(destination, resource);
442 includedResources.add(sourceAbsPath);
443 log("Adding resource: %s\n", destination);
444 return true;
445 }
446
447 private void log(String format, Object... objects) {
448 //System.err.printf(format, objects);
449 }
450
451 private void warn(String format, Object... objects) {
452 stderr.printf(format, objects);
453 }
454
455 @Override
456 public String toString() {
457 return MoreObjects.toStringHelper(this)
458 .add("inputJar", inputJar)
459 .add("outputJar", outputJar)
460 .add("classpath", classpath)
461 .add("bundleName", bundleName)
462 .add("groupId", groupId)
463 .add("bundleSymbolicName", bundleSymbolicName)
464 .add("bundleVersion", bundleVersion)
465 .add("bundleDescription", bundleDescription)
466 .add("bundleLicense", bundleLicense)
467 .toString();
Brian O'Connoree674952016-09-13 16:31:45 -0700468 }
469
470 @Override
471 public StepExecutionResult execute(ExecutionContext executionContext)
472 throws IOException, InterruptedException {
473 stderr = executionContext.getStdErr();
474 boolean success = execute();
475 stderr = System.err;
476 return success ? StepExecutionResult.SUCCESS : StepExecutionResult.ERROR;
477 }
478
479 @Override
480 public String getShortName() {
481 return "osgiwrap";
482 }
483
484 @Override
485 public String getDescription(ExecutionContext executionContext) {
486 return "osgiwrap"; //FIXME
487 }
488}