blob: 8cf91af7f7d3434a905faaf1cdb9eb3ca854505b [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
Bharat saraswala899a212017-02-28 13:19:57 +0530161 // 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
164 if (includeResources != null) {
165 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources);
166 }
167
168 if (isWab()) {
169 analyzer.setProperty(Analyzer.WAB, "src/main/webapp/");
170 analyzer.setProperty("Web-ContextPath", webContext);
171 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, "*,org.glassfish.jersey.servlet,org.jvnet.mimepull\n");
172 }
173 }
174
175 public boolean execute() {
Bharat saraswala899a212017-02-28 13:19:57 +0530176 Builder analyzer = new Builder();
Brian O'Connoree674952016-09-13 16:31:45 -0700177 try {
178
179 Jar jar = new Jar(inputJar.toFile()); // where our data is
180 analyzer.setJar(jar); // give bnd the contents
181
182 // You can provide additional class path entries to allow
183 // bnd to pickup export version from the packageinfo file,
184 // Version annotation, or their manifests.
185 analyzer.addClasspath(classpath);
186
187 setProperties(analyzer);
188
189 //analyzer.setBase(classesDir.toFile());
190
191// analyzer.setProperty("DESTDIR");
192// analyzer.setBase();
193
194 // ------------- let's begin... -------------------------
195
Bharat saraswala899a212017-02-28 13:19:57 +0530196 //Add local packges to jar file.
197 addLocalPackages(new File(classesDir.toString()), analyzer);
198// analyzer.analyze();
Brian O'Connoree674952016-09-13 16:31:45 -0700199
Bharat saraswala899a212017-02-28 13:19:57 +0530200 //add resources.
Brian O'Connoree674952016-09-13 16:31:45 -0700201 if (includeResources != null) {
202 doIncludeResources(analyzer);
203 }
204
205 // Repack the JAR as a WAR
206 doWabStaging(analyzer);
207
208 // Calculate the manifest
Bharat saraswala899a212017-02-28 13:19:57 +0530209// Manifest manifest = analyzer.calcManifest();
Brian O'Connoree674952016-09-13 16:31:45 -0700210 //OutputStream s = new FileOutputStream("/tmp/foo2.txt");
211 //manifest.write(s);
212 //s.close();
213
214 if (analyzer.isOk()) {
Bharat saraswala899a212017-02-28 13:19:57 +0530215 //Build the jar files
216 analyzer.build();
217 Map<String, String> properties = Maps.newHashMap();
218
219 // Scan the JAR for Felix SCR annotations and generate XML files
220 properties.put("destdir", classesDir.toAbsolutePath().toString());
221 SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin();
222 scrDescriptorBndPlugin.setProperties(properties);
223 scrDescriptorBndPlugin.setReporter(analyzer);
224 scrDescriptorBndPlugin.analyzeJar(analyzer);
225
226 //add calculated manifest file.
227 Manifest manifest = analyzer.calcManifest();
Brian O'Connoree674952016-09-13 16:31:45 -0700228 analyzer.getJar().setManifest(manifest);
Bharat saraswala899a212017-02-28 13:19:57 +0530229
Brian O'Connoree674952016-09-13 16:31:45 -0700230 if (analyzer.save(outputJar.toFile(), true)) {
231 log("Saved!\n");
232 } else {
233 warn("Failed to create jar \n");
234 return false;
235 }
236 } else {
237 warn("Analyzer Errors:\n%s\n", analyzer.getErrors());
238 return false;
239 }
240
241 analyzer.close();
242
243 return true;
244 } catch (Exception e) {
245 e.printStackTrace();
246 return false;
247 }
248 }
249
Bharat saraswala899a212017-02-28 13:19:57 +0530250 private static void addLocalPackages(File outputDirectory, Analyzer analyzer) throws IOException {
251 Packages packages = new Packages();
252
253 if (outputDirectory != null && outputDirectory.isDirectory()) {
254 // scan classes directory for potential packages
255 DirectoryScanner scanner = new DirectoryScanner();
256 scanner.setBasedir(outputDirectory);
257 scanner.setIncludes(new String[]
258 {"**/*.class"});
259
260 scanner.addDefaultExcludes();
261 scanner.scan();
262
263 String[] paths = scanner.getIncludedFiles();
264 for (int i = 0; i < paths.length; i++) {
265 packages.put(analyzer.getPackageRef(getPackageName(paths[i])));
266 }
267 }
268
269 Packages exportedPkgs = new Packages();
270 Packages privatePkgs = new Packages();
271
272 boolean noprivatePackages = "!*".equals(analyzer.getProperty(Analyzer.PRIVATE_PACKAGE));
273
274 for (Descriptors.PackageRef pkg : packages.keySet()) {
275 // mark all source packages as private by default (can be overridden by export list)
276 privatePkgs.put(pkg);
277
278 // we can't export the default package (".") and we shouldn't export internal packages
279 String fqn = pkg.getFQN();
280 if (noprivatePackages || !(".".equals(fqn) || fqn.contains(".internal") || fqn.contains(".impl"))) {
281 exportedPkgs.put(pkg);
282 }
283 }
284
285 Properties properties = analyzer.getProperties();
286 String exported = properties.getProperty(Analyzer.EXPORT_PACKAGE);
287 if (exported == null) {
288 if (!properties.containsKey(Analyzer.EXPORT_CONTENTS)) {
289 // no -exportcontents overriding the exports, so use our computed list
290 for (Attrs attrs : exportedPkgs.values()) {
291 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
292 }
293 properties.setProperty(Analyzer.EXPORT_PACKAGE, Processor.printClauses(exportedPkgs));
294 } else {
295 // leave Export-Package empty (but non-null) as we have -exportcontents
296 properties.setProperty(Analyzer.EXPORT_PACKAGE, "");
297 }
298 }
299
300 String internal = properties.getProperty(Analyzer.PRIVATE_PACKAGE);
301 if (internal == null) {
302 if (!privatePkgs.isEmpty()) {
303 for (Attrs attrs : privatePkgs.values()) {
304 attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first");
305 }
306 properties.setProperty(Analyzer.PRIVATE_PACKAGE, Processor.printClauses(privatePkgs));
307 } else {
308 // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
309 properties.setProperty(Analyzer.PRIVATE_PACKAGE, "!*");
310 }
311 }
312 }
313
314 private static String getPackageName(String filename) {
315 int n = filename.lastIndexOf(File.separatorChar);
316 return n < 0 ? "." : filename.substring(0, n).replace(File.separatorChar, '.');
317 }
318
Brian O'Connoree674952016-09-13 16:31:45 -0700319 private boolean isWab() {
320 return webContext != null;
321 }
322
323 private void doWabStaging(Analyzer analyzer) throws Exception {
324 if (!isWab()) {
325 return;
326 }
327 String wab = analyzer.getProperty(analyzer.WAB);
328 Jar dot = analyzer.getJar();
329
330 log("wab %s", wab);
331 analyzer.setBundleClasspath("WEB-INF/classes," +
Bharat saraswala899a212017-02-28 13:19:57 +0530332 analyzer.getProperty(analyzer.BUNDLE_CLASSPATH));
Brian O'Connoree674952016-09-13 16:31:45 -0700333
334 Set<String> paths = new HashSet<>(dot.getResources().keySet());
335
336 for (String path : paths) {
337 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
338 log("wab: moving: %s", path);
339 dot.rename(path, "WEB-INF/classes/" + path);
340 }
341 }
342
343 Path wabRoot = Paths.get(wab);
344 includeFiles(dot, null, wabRoot.toString());
345 }
346
347 /**
348 * Parse the Bundle-Includes header. Files in the bundles Include header are
349 * included in the jar. The source can be a directory or a file.
350 *
351 * @throws Exception
352 */
353 private void doIncludeResources(Analyzer analyzer) throws Exception {
354 String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE);
355 if (includes == null) {
356 return;
357 }
358 Parameters clauses = analyzer.parseHeader(includes);
359 Jar jar = analyzer.getJar();
360
361 for (Map.Entry<String, Attrs> entry : clauses.entrySet()) {
362 String name = entry.getKey();
363 Map<String, String> extra = entry.getValue();
364 // TODO consider doing something with extras
365
366 String[] parts = name.split("\\s*=\\s*");
367 String source = parts[0];
368 String destination = parts[0];
369 if (parts.length == 2) {
370 source = parts[1];
371 }
372
373 includeFiles(jar, destination, source);
374 }
375 }
376
377 private void includeFiles(Jar jar, String destinationRoot, String sourceRoot)
378 throws IOException {
379
380 Path classesBasedPath = classesDir.resolve(sourceRoot);
381 Path sourceBasedPath = sourcesDir.resolve(sourceRoot);
382
383 File classFile = classesBasedPath.toFile();
384 File sourceFile = sourceBasedPath.toFile();
385
386 if (classFile.isFile()) {
387 addFileToJar(jar, destinationRoot, classesBasedPath.toAbsolutePath().toString());
388 } else if (sourceFile.isFile()) {
389 addFileToJar(jar, destinationRoot, sourceBasedPath.toAbsolutePath().toString());
390 } else if (classFile.isDirectory()) {
391 includeDirectory(jar, destinationRoot, classesBasedPath);
392 } else if (sourceFile.isDirectory()) {
393 includeDirectory(jar, destinationRoot, sourceBasedPath);
394 } else {
395 warn("Skipping resource in bundle %s: %s (File Not Found)\n",
396 bundleSymbolicName, sourceRoot);
397 }
398 }
399
400 private void includeDirectory(Jar jar, String destinationRoot, Path sourceRoot)
401 throws IOException {
402 // iterate through sources
403 // put each source on the jar
404 FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
405 @Override
406 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
407 Path relativePath = sourceRoot.relativize(file);
408 String destination = destinationRoot != null ?
409 destinationRoot + "/" + relativePath.toString() : //TODO
410 relativePath.toString();
411
412 addFileToJar(jar, destination, file.toAbsolutePath().toString());
413 return FileVisitResult.CONTINUE;
414 }
415 };
416
417 walkFileTree(sourceRoot, visitor);
418 }
419
420 private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) {
421 if (includedResources.contains(sourceAbsPath)) {
422 log("Skipping already included resource: %s\n", sourceAbsPath);
423 return false;
424 }
425 File file = new File(sourceAbsPath);
426 if (!file.isFile()) {
427 throw new RuntimeException(
428 String.format("Skipping non-existent file: %s\n", sourceAbsPath));
429 }
430 Resource resource = new FileResource(file);
431 if (jar.getResource(destination) != null) {
432 warn("Skipping duplicate resource: %s\n", destination);
433 return false;
434 }
435 jar.putResource(destination, resource);
436 includedResources.add(sourceAbsPath);
437 log("Adding resource: %s\n", destination);
438 return true;
439 }
440
441 private void log(String format, Object... objects) {
442 //System.err.printf(format, objects);
443 }
444
445 private void warn(String format, Object... objects) {
446 stderr.printf(format, objects);
447 }
448
449 @Override
450 public String toString() {
451 return MoreObjects.toStringHelper(this)
452 .add("inputJar", inputJar)
453 .add("outputJar", outputJar)
454 .add("classpath", classpath)
455 .add("bundleName", bundleName)
456 .add("groupId", groupId)
457 .add("bundleSymbolicName", bundleSymbolicName)
458 .add("bundleVersion", bundleVersion)
459 .add("bundleDescription", bundleDescription)
460 .add("bundleLicense", bundleLicense)
461 .toString();
Brian O'Connoree674952016-09-13 16:31:45 -0700462 }
463
464 @Override
465 public StepExecutionResult execute(ExecutionContext executionContext)
466 throws IOException, InterruptedException {
467 stderr = executionContext.getStdErr();
468 boolean success = execute();
469 stderr = System.err;
470 return success ? StepExecutionResult.SUCCESS : StepExecutionResult.ERROR;
471 }
472
473 @Override
474 public String getShortName() {
475 return "osgiwrap";
476 }
477
478 @Override
479 public String getDescription(ExecutionContext executionContext) {
480 return "osgiwrap"; //FIXME
481 }
482}