| /* |
| * Copyright 2016-present Open Networking Foundation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.onosproject.onosjar; |
| |
| import aQute.bnd.header.Attrs; |
| import aQute.bnd.header.Parameters; |
| import aQute.bnd.osgi.Analyzer; |
| import aQute.bnd.osgi.Builder; |
| import aQute.bnd.osgi.Constants; |
| import aQute.bnd.osgi.Descriptors; |
| import aQute.bnd.osgi.FileResource; |
| import aQute.bnd.osgi.Jar; |
| import aQute.bnd.osgi.Packages; |
| import aQute.bnd.osgi.Processor; |
| import aQute.bnd.osgi.Resource; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.step.Step; |
| import com.facebook.buck.step.StepExecutionResult; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import org.apache.felix.scrplugin.bnd.SCRDescriptorBndPlugin; |
| import org.codehaus.plexus.util.DirectoryScanner; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.FileVisitor; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.jar.Manifest; |
| import java.util.stream.Collectors; |
| |
| import static java.nio.file.Files.walkFileTree; |
| |
| /** |
| * BND-based wrapper to convert Buck JARs to OSGi-compatible JARs. |
| */ |
| public class OSGiWrapper implements Step { |
| |
| private Path inputJar; |
| private Path outputJar; |
| private Path sourcesDir; |
| private Path classesDir; |
| private List<String> classpath; |
| |
| private String bundleName; |
| private String groupId; |
| private String bundleSymbolicName; |
| private String bundleVersion; |
| |
| private String importPackages; |
| private String privatePackages; |
| private String dynamicimportPackages; |
| |
| private String exportPackages; |
| private String includeResources; |
| private Set<String> includedResources = Sets.newHashSet(); |
| |
| private String bundleDescription; |
| private String bundleLicense; |
| |
| private String webContext; |
| |
| private PrintStream stderr = System.err; |
| |
| public OSGiWrapper(Path inputJar, |
| Path outputJar, |
| Path sourcesDir, |
| Path classesDir, |
| ImmutableSortedSet<Path> classpath, |
| String bundleName, |
| String groupId, |
| String bundleVersion, |
| String bundleLicense, |
| String importPackages, |
| String exportPackages, |
| String includeResources, |
| String webContext, |
| String dynamicimportPackages, |
| String bundleDescription, |
| String privatePackages) { |
| this.inputJar = inputJar; |
| this.sourcesDir = sourcesDir; |
| this.classesDir = classesDir; |
| this.classpath = Lists.newArrayList( |
| classpath.stream().map(Path::toString).collect(Collectors.toList())); |
| if (!this.classpath.contains(inputJar.toString())) { |
| this.classpath.add(0, inputJar.toString()); |
| } |
| this.outputJar = outputJar; |
| |
| this.bundleName = bundleName; |
| this.groupId = groupId; |
| this.bundleSymbolicName = String.format("%s.%s", groupId, bundleName); |
| |
| this.bundleVersion = bundleVersion; |
| this.bundleLicense = bundleLicense; |
| this.bundleDescription = bundleDescription; |
| |
| this.importPackages = importPackages; |
| this.privatePackages = privatePackages; |
| this.dynamicimportPackages = dynamicimportPackages; |
| this.exportPackages = exportPackages; |
| this.includeResources = includeResources; |
| |
| this.webContext = webContext; |
| } |
| |
| private void setProperties(Analyzer analyzer) { |
| analyzer.setProperty(Analyzer.BUNDLE_NAME, bundleName); |
| analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME, bundleSymbolicName); |
| analyzer.setProperty(Analyzer.BUNDLE_VERSION, bundleVersion.replace('-', '.')); |
| |
| if (bundleDescription != null) { |
| analyzer.setProperty(Analyzer.BUNDLE_DESCRIPTION, bundleDescription); |
| } |
| if (bundleLicense != null) { |
| analyzer.setProperty(Analyzer.BUNDLE_LICENSE, bundleLicense); |
| } |
| |
| //TODO consider using stricter version policy |
| //analyzer.setProperty("-provider-policy", "${range;[===,==+)}"); |
| //analyzer.setProperty("-consumer-policy", "${range;[===,==+)}"); |
| |
| // There are no good defaults so make sure you set the Import-Package |
| analyzer.setProperty(Analyzer.IMPORT_PACKAGE, importPackages); |
| if (privatePackages != null) { |
| analyzer.setProperty(Analyzer.PRIVATE_PACKAGE, privatePackages); |
| } |
| analyzer.setProperty(Analyzer.REMOVEHEADERS, "Private-Package,Include-Resource"); |
| |
| analyzer.setProperty(Analyzer.DYNAMICIMPORT_PACKAGE, |
| dynamicimportPackages); |
| |
| // TODO include version in export, but not in import |
| analyzer.setProperty(Analyzer.EXPORT_PACKAGE, exportPackages); |
| |
| // TODO we may need INCLUDE_RESOURCE, or that might be done by Buck |
| // FIXME NOTE we handle this manually below |
| if (includeResources != null) { |
| analyzer.setProperty(Analyzer.INCLUDE_RESOURCE, includeResources); |
| } |
| |
| if (isWab()) { |
| analyzer.setProperty(Analyzer.WAB, "src/main/webapp/"); |
| analyzer.setProperty("Web-ContextPath", webContext); |
| analyzer.setProperty(Analyzer.IMPORT_PACKAGE, "*,org.glassfish.jersey.servlet,org.jvnet.mimepull\n"); |
| } |
| } |
| |
| public boolean execute() { |
| Builder analyzer = new Builder(); |
| try { |
| |
| Jar jar = new Jar(inputJar.toFile()); // where our data is |
| analyzer.setJar(jar); // give bnd the contents |
| |
| // You can provide additional class path entries to allow |
| // bnd to pickup export version from the packageinfo file, |
| // Version annotation, or their manifests. |
| analyzer.addClasspath(classpath); |
| |
| setProperties(analyzer); |
| |
| // Analyze the target JAR first |
| analyzer.analyze(); |
| |
| // Scan the JAR for Felix SCR annotations and generate XML files |
| Map<String, String> properties = Maps.newHashMap(); |
| properties.put("destdir", classesDir.toAbsolutePath().toString()); |
| SCRDescriptorBndPlugin scrDescriptorBndPlugin = new SCRDescriptorBndPlugin(); |
| scrDescriptorBndPlugin.setProperties(properties); |
| scrDescriptorBndPlugin.setReporter(analyzer); |
| scrDescriptorBndPlugin.analyzeJar(analyzer); |
| |
| //Add local packges to jar file. |
| //FIXME removing this call for now; not sure what exactly it's doing |
| //addLocalPackages(new File(classesDir.toString()), analyzer); |
| |
| //add resources. |
| if (includeResources != null) { |
| doIncludeResources(analyzer); |
| } |
| |
| // Repack the JAR as a WAR |
| doWabStaging(analyzer); |
| |
| // Calculate the manifest |
| Manifest manifest = analyzer.calcManifest(); |
| |
| //Build the jar files |
| //FIXME this call conflicts with some of the above |
| // analyzer.build(); |
| |
| if (analyzer.isOk()) { |
| //add calculated manifest file. |
| analyzer.getJar().setManifest(manifest); |
| if (analyzer.save(outputJar.toFile(), true)) { |
| log("Saved!\n"); |
| } else { |
| warn("Failed to create jar \n"); |
| return false; |
| } |
| } else { |
| warn("Analyzer Errors:\n%s\n", analyzer.getErrors()); |
| return false; |
| } |
| |
| analyzer.close(); |
| |
| return true; |
| } catch (Exception e) { |
| e.printStackTrace(); |
| return false; |
| } |
| } |
| |
| private static void addLocalPackages(File outputDirectory, Analyzer analyzer) throws IOException { |
| Packages packages = new Packages(); |
| |
| if (outputDirectory != null && outputDirectory.isDirectory()) { |
| // scan classes directory for potential packages |
| DirectoryScanner scanner = new DirectoryScanner(); |
| scanner.setBasedir(outputDirectory); |
| scanner.setIncludes(new String[] |
| {"**/*.class"}); |
| |
| scanner.addDefaultExcludes(); |
| scanner.scan(); |
| |
| String[] paths = scanner.getIncludedFiles(); |
| for (int i = 0; i < paths.length; i++) { |
| packages.put(analyzer.getPackageRef(getPackageName(paths[i]))); |
| } |
| } |
| |
| Packages exportedPkgs = new Packages(); |
| Packages privatePkgs = new Packages(); |
| |
| boolean noprivatePackages = "!*".equals(analyzer.getProperty(Analyzer.PRIVATE_PACKAGE)); |
| |
| for (Descriptors.PackageRef pkg : packages.keySet()) { |
| // mark all source packages as private by default (can be overridden by export list) |
| privatePkgs.put(pkg); |
| |
| // we can't export the default package (".") and we shouldn't export internal packages |
| String fqn = pkg.getFQN(); |
| if (noprivatePackages || !(".".equals(fqn) || fqn.contains(".internal") || fqn.contains(".impl"))) { |
| exportedPkgs.put(pkg); |
| } |
| } |
| |
| Properties properties = analyzer.getProperties(); |
| String exported = properties.getProperty(Analyzer.EXPORT_PACKAGE); |
| if (exported == null) { |
| if (!properties.containsKey(Analyzer.EXPORT_CONTENTS)) { |
| // no -exportcontents overriding the exports, so use our computed list |
| for (Attrs attrs : exportedPkgs.values()) { |
| attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first"); |
| } |
| properties.setProperty(Analyzer.EXPORT_PACKAGE, Processor.printClauses(exportedPkgs)); |
| } else { |
| // leave Export-Package empty (but non-null) as we have -exportcontents |
| properties.setProperty(Analyzer.EXPORT_PACKAGE, ""); |
| } |
| } |
| |
| String internal = properties.getProperty(Analyzer.PRIVATE_PACKAGE); |
| if (internal == null) { |
| if (!privatePkgs.isEmpty()) { |
| for (Attrs attrs : privatePkgs.values()) { |
| attrs.put(Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first"); |
| } |
| properties.setProperty(Analyzer.PRIVATE_PACKAGE, Processor.printClauses(privatePkgs)); |
| } else { |
| // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy |
| properties.setProperty(Analyzer.PRIVATE_PACKAGE, "!*"); |
| } |
| } |
| } |
| |
| private static String getPackageName(String filename) { |
| int n = filename.lastIndexOf(File.separatorChar); |
| return n < 0 ? "." : filename.substring(0, n).replace(File.separatorChar, '.'); |
| } |
| |
| private boolean isWab() { |
| return webContext != null; |
| } |
| |
| private void doWabStaging(Analyzer analyzer) throws Exception { |
| if (!isWab()) { |
| return; |
| } |
| String wab = analyzer.getProperty(analyzer.WAB); |
| Jar dot = analyzer.getJar(); |
| |
| log("wab %s", wab); |
| analyzer.setBundleClasspath("WEB-INF/classes," + |
| analyzer.getProperty(analyzer.BUNDLE_CLASSPATH)); |
| |
| Set<String> paths = new HashSet<>(dot.getResources().keySet()); |
| |
| for (String path : paths) { |
| if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) { |
| log("wab: moving: %s", path); |
| dot.rename(path, "WEB-INF/classes/" + path); |
| } |
| } |
| |
| Path wabRoot = Paths.get(wab); |
| includeFiles(dot, null, wabRoot.toString()); |
| } |
| |
| /** |
| * Parse the Bundle-Includes header. Files in the bundles Include header are |
| * included in the jar. The source can be a directory or a file. |
| * |
| * @throws Exception |
| */ |
| private void doIncludeResources(Analyzer analyzer) throws Exception { |
| String includes = analyzer.getProperty(Analyzer.INCLUDE_RESOURCE); |
| if (includes == null) { |
| return; |
| } |
| Parameters clauses = analyzer.parseHeader(includes); |
| Jar jar = analyzer.getJar(); |
| |
| for (Map.Entry<String, Attrs> entry : clauses.entrySet()) { |
| String name = entry.getKey(); |
| Map<String, String> extra = entry.getValue(); |
| // TODO consider doing something with extras |
| |
| String[] parts = name.split("\\s*=\\s*"); |
| String source = parts[0]; |
| String destination = parts[0]; |
| if (parts.length == 2) { |
| source = parts[1]; |
| } |
| |
| includeFiles(jar, destination, source); |
| } |
| } |
| |
| private void includeFiles(Jar jar, String destinationRoot, String sourceRoot) |
| throws IOException { |
| |
| Path classesBasedPath = classesDir.resolve(sourceRoot); |
| Path sourceBasedPath = sourcesDir.resolve(sourceRoot); |
| |
| File classFile = classesBasedPath.toFile(); |
| File sourceFile = sourceBasedPath.toFile(); |
| |
| if (classFile.isFile()) { |
| addFileToJar(jar, destinationRoot, classesBasedPath.toAbsolutePath().toString()); |
| } else if (sourceFile.isFile()) { |
| addFileToJar(jar, destinationRoot, sourceBasedPath.toAbsolutePath().toString()); |
| } else if (classFile.isDirectory()) { |
| includeDirectory(jar, destinationRoot, classesBasedPath); |
| } else if (sourceFile.isDirectory()) { |
| includeDirectory(jar, destinationRoot, sourceBasedPath); |
| } else { |
| warn("Skipping resource in bundle %s: %s (File Not Found)\n", |
| bundleSymbolicName, sourceRoot); |
| } |
| } |
| |
| private void includeDirectory(Jar jar, String destinationRoot, Path sourceRoot) |
| throws IOException { |
| // iterate through sources |
| // put each source on the jar |
| FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
| Path relativePath = sourceRoot.relativize(file); |
| String destination = destinationRoot != null ? |
| destinationRoot + "/" + relativePath.toString() : //TODO |
| relativePath.toString(); |
| |
| addFileToJar(jar, destination, file.toAbsolutePath().toString()); |
| return FileVisitResult.CONTINUE; |
| } |
| }; |
| |
| walkFileTree(sourceRoot, visitor); |
| } |
| |
| private boolean addFileToJar(Jar jar, String destination, String sourceAbsPath) { |
| if (includedResources.contains(sourceAbsPath)) { |
| log("Skipping already included resource: %s\n", sourceAbsPath); |
| return false; |
| } |
| File file = new File(sourceAbsPath); |
| if (!file.isFile()) { |
| throw new RuntimeException( |
| String.format("Skipping non-existent file: %s\n", sourceAbsPath)); |
| } |
| Resource resource = new FileResource(file); |
| if (jar.getResource(destination) != null) { |
| warn("Skipping duplicate resource: %s\n", destination); |
| return false; |
| } |
| jar.putResource(destination, resource); |
| includedResources.add(sourceAbsPath); |
| log("Adding resource: %s\n", destination); |
| return true; |
| } |
| |
| private void log(String format, Object... objects) { |
| //System.err.printf(format, objects); |
| } |
| |
| private void warn(String format, Object... objects) { |
| stderr.printf(format, objects); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("inputJar", inputJar) |
| .add("outputJar", outputJar) |
| .add("classpath", classpath) |
| .add("bundleName", bundleName) |
| .add("groupId", groupId) |
| .add("bundleSymbolicName", bundleSymbolicName) |
| .add("bundleVersion", bundleVersion) |
| .add("bundleDescription", bundleDescription) |
| .add("bundleLicense", bundleLicense) |
| .toString(); |
| } |
| |
| @Override |
| public StepExecutionResult execute(ExecutionContext executionContext) |
| throws IOException, InterruptedException { |
| stderr = executionContext.getStdErr(); |
| boolean success = execute(); |
| stderr = System.err; |
| return success ? StepExecutionResult.SUCCESS : StepExecutionResult.ERROR; |
| } |
| |
| @Override |
| public String getShortName() { |
| return "osgiwrap"; |
| } |
| |
| @Override |
| public String getDescription(ExecutionContext executionContext) { |
| return "osgiwrap"; //FIXME |
| } |
| } |