blob: 821bf40aae617ec8de04f281b51d62a052476d32 [file] [log] [blame]
Brian O'Connor42c38cf2016-04-05 17:05:57 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016-present Open Networking Foundation
Brian O'Connor42c38cf2016-04-05 17:05:57 -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.onlab.osgiwrap;
18
Brian O'Connore5817c92016-04-06 15:41:48 -070019import aQute.bnd.header.Attrs;
20import aQute.bnd.header.Parameters;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070021import aQute.bnd.osgi.Analyzer;
Brian O'Connor8aec1a12016-04-06 14:10:43 -070022import aQute.bnd.osgi.Builder;
Brian O'Connore5817c92016-04-06 15:41:48 -070023import aQute.bnd.osgi.FileResource;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070024import aQute.bnd.osgi.Jar;
Brian O'Connore5817c92016-04-06 15:41:48 -070025import aQute.bnd.osgi.Resource;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070026import com.google.common.base.Joiner;
27import com.google.common.base.MoreObjects;
28import com.google.common.collect.Lists;
29import com.google.common.collect.Maps;
Brian O'Connore5817c92016-04-06 15:41:48 -070030import com.google.common.collect.Sets;
Thomas Vachuskae4436942018-08-07 19:27:10 -070031import com.google.common.io.ByteStreams;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070032import org.apache.felix.scrplugin.bnd.SCRDescriptorBndPlugin;
33
34import java.io.File;
Thomas Vachuskae4436942018-08-07 19:27:10 -070035import java.io.FileInputStream;
Brian O'Connore5817c92016-04-06 15:41:48 -070036import java.io.IOException;
37import java.nio.file.FileVisitResult;
38import java.nio.file.FileVisitor;
39import java.nio.file.Path;
40import java.nio.file.Paths;
41import java.nio.file.SimpleFileVisitor;
42import java.nio.file.attribute.BasicFileAttributes;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070043import java.util.Arrays;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Map;
47import java.util.Objects;
48import java.util.Set;
Thomas Vachuskae4436942018-08-07 19:27:10 -070049import java.util.jar.JarEntry;
50import java.util.jar.JarInputStream;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070051import java.util.jar.Manifest;
52
Thomas Vachuskae4436942018-08-07 19:27:10 -070053import static com.google.common.io.Files.createParentDirs;
54import static com.google.common.io.Files.write;
Brian O'Connore5817c92016-04-06 15:41:48 -070055import static java.nio.file.Files.walkFileTree;
56
Brian O'Connor42c38cf2016-04-05 17:05:57 -070057/**
58 * BND-based wrapper to convert Buck JARs to OSGi-compatible JARs.
59 */
60public class OSGiWrapper {
Brian O'Connore5817c92016-04-06 15:41:48 -070061 private static final String NONE = "NONE";
Brian O'Connor42c38cf2016-04-05 17:05:57 -070062
63 private String inputJar;
64 private String outputJar;
65 private List<String> classpath;
66
67 private String bundleName;
68 private String groupId;
69 private String bundleSymbolicName;
70 private String bundleVersion;
71
Brian O'Connore5817c92016-04-06 15:41:48 -070072 private String importPackages;
Yuta HIGUCHIf05db402016-08-12 18:36:33 -070073 private String dynamicimportPackages;
74
Brian O'Connore5817c92016-04-06 15:41:48 -070075 private String exportPackages;
76 private String includeResources;
77 private Set<String> includedResources = Sets.newHashSet();
78
Brian O'Connor42c38cf2016-04-05 17:05:57 -070079 private String bundleDescription;
80 private String bundleLicense;
81
82 private String webContext;
Ray Milkey25747d82018-06-13 14:12:51 -070083 private String webXmlRoot;
Ray Milkey7dac7da2017-08-01 16:56:05 -070084 private String destdir;
Brian O'Connor42c38cf2016-04-05 17:05:57 -070085
Jian Li4ad86872018-08-05 03:34:35 +090086 private String bundleClasspath;
87
Yuta HIGUCHIf05db402016-08-12 18:36:33 -070088 // FIXME should consider using Commons CLI, etc.
Brian O'Connor42c38cf2016-04-05 17:05:57 -070089 public static void main(String[] args) {
Jian Li4ad86872018-08-05 03:34:35 +090090 if (args.length < 14) {
Brian O'Connor42c38cf2016-04-05 17:05:57 -070091 System.err.println("Not enough args");
92 System.exit(1);
93 }
94
95 String jar = args[0];
96 String output = args[1];
97 String cp = args[2];
98 String name = args[3];
99 String group = args[4];
100 String version = args[5];
101 String license = args[6];
Brian O'Connore5817c92016-04-06 15:41:48 -0700102 String importPackages = args[7];
103 String exportPackages = args[8];
104 String includeResources = args[9];
105 String webContext = args[10];
Ray Milkey25747d82018-06-13 14:12:51 -0700106 String webXmlRoot = args[11];
107 String dynamicimportPackages = args[12];
108 String destdir = args[13];
Jian Li4ad86872018-08-05 03:34:35 +0900109 String bundleClasspath = args[14];
Yuta HIGUCHIf05db402016-08-12 18:36:33 -0700110 String desc = Joiner.on(' ').join(Arrays.copyOfRange(args, 12, args.length));
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700111
112 OSGiWrapper wrapper = new OSGiWrapper(jar, output, cp,
Jian Li4ad86872018-08-05 03:34:35 +0900113 name, group,
114 version, license,
115 importPackages, exportPackages,
116 includeResources,
117 webContext,
118 webXmlRoot,
119 dynamicimportPackages,
120 desc,
121 destdir,
122 bundleClasspath);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700123 wrapper.log(wrapper + "\n");
124 if (!wrapper.execute()) {
Brian O'Connore5817c92016-04-06 15:41:48 -0700125 System.err.printf("Error generating %s\n", name);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700126 System.exit(2);
127 }
128 }
129
130
131 public OSGiWrapper(String inputJar,
132 String outputJar,
133 String classpath,
134 String bundleName,
135 String groupId,
136 String bundleVersion,
137 String bundleLicense,
Brian O'Connore5817c92016-04-06 15:41:48 -0700138 String importPackages,
139 String exportPackages,
140 String includeResources,
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700141 String webContext,
Ray Milkey25747d82018-06-13 14:12:51 -0700142 String webXmlRoot,
Yuta HIGUCHIf05db402016-08-12 18:36:33 -0700143 String dynamicimportPackages,
Ray Milkey7dac7da2017-08-01 16:56:05 -0700144 String bundleDescription,
Jian Li4ad86872018-08-05 03:34:35 +0900145 String destdir,
146 String bundleClasspath) {
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700147 this.inputJar = inputJar;
148 this.classpath = Lists.newArrayList(classpath.split(":"));
149 if (!this.classpath.contains(inputJar)) {
150 this.classpath.add(0, inputJar);
151 }
152 this.outputJar = outputJar;
153
154 this.bundleName = bundleName;
155 this.groupId = groupId;
156 this.bundleSymbolicName = String.format("%s.%s", groupId, bundleName);
157
158 this.bundleVersion = bundleVersion;
159 this.bundleLicense = bundleLicense;
160 this.bundleDescription = bundleDescription;
161
Brian O'Connore5817c92016-04-06 15:41:48 -0700162 this.importPackages = importPackages;
Yuta HIGUCHIf05db402016-08-12 18:36:33 -0700163 this.dynamicimportPackages = dynamicimportPackages;
164 if (Objects.equals(dynamicimportPackages, "''")) {
165 this.dynamicimportPackages = null;
166 }
Brian O'Connore5817c92016-04-06 15:41:48 -0700167 this.exportPackages = exportPackages;
168 if (!Objects.equals(includeResources, NONE)) {
169 this.includeResources = includeResources;
170 }
171
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700172 this.webContext = webContext;
Ray Milkey25747d82018-06-13 14:12:51 -0700173 this.webXmlRoot = webXmlRoot;
Ray Milkey7dac7da2017-08-01 16:56:05 -0700174 this.destdir = destdir;
Jian Li4ad86872018-08-05 03:34:35 +0900175
176 this.bundleClasspath = bundleClasspath;
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700177 }
178
179 private void setProperties(Analyzer analyzer) {
180 analyzer.setProperty(Analyzer.BUNDLE_NAME, bundleName);
181 analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME, bundleSymbolicName);
182 analyzer.setProperty(Analyzer.BUNDLE_VERSION, bundleVersion.replace('-', '.'));
183
184 analyzer.setProperty(Analyzer.BUNDLE_DESCRIPTION, bundleDescription);
185 analyzer.setProperty(Analyzer.BUNDLE_LICENSE, bundleLicense);
186
187 //TODO consider using stricter version policy
188 //analyzer.setProperty("-provider-policy", "${range;[===,==+)}");
189 //analyzer.setProperty("-consumer-policy", "${range;[===,==+)}");
190
Yuta HIGUCHIf05db402016-08-12 18:36:33 -0700191 analyzer.setProperty(Analyzer.DYNAMICIMPORT_PACKAGE, dynamicimportPackages);
192
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700193 // TODO include version in export, but not in import
Brian O'Connore5817c92016-04-06 15:41:48 -0700194 analyzer.setProperty(Analyzer.EXPORT_PACKAGE, exportPackages);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700195
196 // TODO we may need INCLUDE_RESOURCE, or that might be done by Buck
Brian O'Connore5817c92016-04-06 15:41:48 -0700197 if (includeResources != null) {
198 analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources);
199 }
200
Jian Li4ad86872018-08-05 03:34:35 +0900201 // There are no good defaults so make sure you set the Import-Package
202 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages);
203
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700204 if (isWab()) {
Ray Milkey25747d82018-06-13 14:12:51 -0700205 analyzer.setProperty(Analyzer.WAB, webXmlRoot);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700206 analyzer.setProperty("Web-ContextPath", webContext);
Jian Li4ad86872018-08-05 03:34:35 +0900207 analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages +
208 ",org.glassfish.jersey.servlet,org.jvnet.mimepull\n");
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700209 }
210 }
211
212 public boolean execute() {
Brian O'Connor8aec1a12016-04-06 14:10:43 -0700213 Analyzer analyzer = new Builder();
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700214 try {
Thomas Vachuskae4436942018-08-07 19:27:10 -0700215 // Extract the input jar contents into the specified output directory
216 expandJar(inputJar, new File(destdir));
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700217
218 Jar jar = new Jar(new File(inputJar)); // where our data is
219 analyzer.setJar(jar); // give bnd the contents
220
221 // You can provide additional class path entries to allow
222 // bnd to pickup export version from the packageinfo file,
223 // Version annotation, or their manifests.
224 analyzer.addClasspath(classpath);
225
226 setProperties(analyzer);
227
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700228 // ------------- let's begin... -------------------------
229
230 // Analyze the target JAR first
231 analyzer.analyze();
232
233 // Scan the JAR for Felix SCR annotations and generate XML files
234 Map<String, String> properties = Maps.newHashMap();
Ray Milkey7dac7da2017-08-01 16:56:05 -0700235 // destdir hack
236 properties.put("destdir", destdir);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700237 SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin();
238 scrDescriptorBndPlugin.setProperties(properties);
239 scrDescriptorBndPlugin.setReporter(analyzer);
240 scrDescriptorBndPlugin.analyzeJar(analyzer);
241
Brian O'Connore5817c92016-04-06 15:41:48 -0700242 if (includeResources != null) {
243 doIncludeResources(analyzer);
244 }
245
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700246 // Repack the JAR as a WAR
247 doWabStaging(analyzer);
248
249 // Calculate the manifest
250 Manifest manifest = analyzer.calcManifest();
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700251
252 if (analyzer.isOk()) {
253 analyzer.getJar().setManifest(manifest);
Brian O'Connore5817c92016-04-06 15:41:48 -0700254 if (analyzer.save(new File(outputJar), true)) {
255 log("Saved!\n");
256 } else {
257 warn("Failed to create jar \n");
258 return false;
259 }
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700260 } else {
Brian O'Connore5817c92016-04-06 15:41:48 -0700261 warn("Analyzer Errors:\n%s\n", analyzer.getErrors());
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700262 return false;
263 }
264
265 analyzer.close();
266
267 return true;
268 } catch (Exception e) {
269 e.printStackTrace();
270 return false;
271 }
272 }
273
Thomas Vachuskae4436942018-08-07 19:27:10 -0700274 // Expands the specified jar file into the given directory
275 private void expandJar(String inputJar, File intoDir) throws IOException {
276 try (JarInputStream jis = new JarInputStream(new FileInputStream(inputJar))) {
277 JarEntry entry;
278 while ((entry = jis.getNextJarEntry()) != null) {
279 if (!entry.isDirectory()) {
280 byte[] data = ByteStreams.toByteArray(jis);
281 jis.closeEntry();
282 if (!entry.getName().contains("..")) {
283 File file = new File(intoDir, entry.getName());
284 createParentDirs(file);
285 write(data, file);
286 } else {
287 throw new IOException("Corrupt jar file");
288 }
289 }
290 }
291 }
292 }
293
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700294 private boolean isWab() {
Brian O'Connore5817c92016-04-06 15:41:48 -0700295 return !Objects.equals(webContext, NONE);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700296 }
297
298 private void doWabStaging(Analyzer analyzer) throws Exception {
299 if (!isWab()) {
300 return;
301 }
302 String wab = analyzer.getProperty(analyzer.WAB);
303 Jar dot = analyzer.getJar();
304
305 log("wab %s", wab);
Ray Milkey6b3775a2018-06-28 11:18:44 -0700306
Jian Li4ad86872018-08-05 03:34:35 +0900307 String specifiedClasspath = this.bundleClasspath;
Ray Milkey6b3775a2018-06-28 11:18:44 -0700308 String bundleClasspath = "WEB-INF/classes";
309 if (specifiedClasspath != null) {
310 bundleClasspath += "," + specifiedClasspath;
311 }
312 analyzer.setBundleClasspath(bundleClasspath);
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700313
Yuta HIGUCHIf05db402016-08-12 18:36:33 -0700314 Set<String> paths = new HashSet<>(dot.getResources().keySet());
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700315
316 for (String path : paths) {
317 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
318 log("wab: moving: %s", path);
319 dot.rename(path, "WEB-INF/classes/" + path);
320 }
321 }
Brian O'Connore5817c92016-04-06 15:41:48 -0700322
323 Path wabRoot = Paths.get(wab);
Ray Milkey25747d82018-06-13 14:12:51 -0700324 log("wab root " + wabRoot.toString());
Brian O'Connore5817c92016-04-06 15:41:48 -0700325 includeFiles(dot, null, wabRoot.toString());
326 }
327
328 /**
329 * Parse the Bundle-Includes header. Files in the bundles Include header are
330 * included in the jar. The source can be a directory or a file.
331 *
332 * @throws Exception
333 */
334 private void doIncludeResources(Analyzer analyzer) throws Exception {
335 String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE);
336 if (includes == null) {
337 return;
338 }
339 Parameters clauses = analyzer.parseHeader(includes);
340 Jar jar = analyzer.getJar();
341
342 for (Map.Entry<String, Attrs> entry : clauses.entrySet()) {
343 String name = entry.getKey();
344 Map<String, String> extra = entry.getValue();
345 // TODO consider doing something with extras
346
347 String[] parts = name.split("\\s*=\\s*");
348 String source = parts[0];
349 String destination = parts[0];
350 if (parts.length == 2) {
351 source = parts[1];
352 }
353
354 includeFiles(jar, destination, source);
355 }
356 }
357
358 private void includeFiles(Jar jar, String destinationRoot, String sourceRoot)
359 throws IOException {
360 Path sourceRootPath = Paths.get(sourceRoot);
361 // iterate through sources
362 // put each source on the jar
363 FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
364 @Override
365 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
366 Path relativePath = sourceRootPath.relativize(file);
367 String destination = destinationRoot != null ?
368 destinationRoot + "/" + relativePath.toString() : //TODO
369 relativePath.toString();
370
371 addFileToJar(jar, destination, file.toAbsolutePath().toString());
372 return FileVisitResult.CONTINUE;
373 }
374 };
375 File dir = new File(sourceRoot);
376 if (dir.isFile()) {
377 addFileToJar(jar, destinationRoot, dir.getAbsolutePath());
378 } else if (dir.isDirectory()) {
379 walkFileTree(sourceRootPath, visitor);
380 } else {
381 warn("Skipping resource in bundle %s: %s (File Not Found)\n",
Jian Li4ad86872018-08-05 03:34:35 +0900382 bundleSymbolicName, sourceRoot);
Brian O'Connore5817c92016-04-06 15:41:48 -0700383 }
384 }
385
Ray Milkey51bee8d2018-09-10 10:01:35 -0700386 private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) throws IOException {
Brian O'Connore5817c92016-04-06 15:41:48 -0700387 if (includedResources.contains(sourceAbsPath)) {
388 log("Skipping already included resource: %s\n", sourceAbsPath);
389 return false;
390 }
391 File file = new File(sourceAbsPath);
392 if (!file.isFile()) {
393 throw new RuntimeException(
394 String.format("Skipping non-existent file: %s\n", sourceAbsPath));
395 }
396 Resource resource = new FileResource(file);
397 if (jar.getResource(destination) != null) {
398 warn("Skipping duplicate resource: %s\n", destination);
399 return false;
400 }
401 jar.putResource(destination, resource);
402 includedResources.add(sourceAbsPath);
403 log("Adding resource: %s\n", destination);
404 return true;
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700405 }
406
407 private void log(String format, Object... objects) {
408 //System.err.printf(format, objects);
409 }
410
411 private void warn(String format, Object... objects) {
412 System.err.printf(format, objects);
413 }
414
415 @Override
416 public String toString() {
417 return MoreObjects.toStringHelper(this)
418 .add("inputJar", inputJar)
419 .add("outputJar", outputJar)
420 .add("classpath", classpath)
421 .add("bundleName", bundleName)
422 .add("groupId", groupId)
423 .add("bundleSymbolicName", bundleSymbolicName)
424 .add("bundleVersion", bundleVersion)
425 .add("bundleDescription", bundleDescription)
426 .add("bundleLicense", bundleLicense)
Jian Li4ad86872018-08-05 03:34:35 +0900427 .add("bundleClassPath", bundleClasspath)
Brian O'Connor42c38cf2016-04-05 17:05:57 -0700428 .toString();
429
430 }
431}