package io.neba.delivery; import aQute.bnd.header.Attrs; import aQute.bnd.header.Parameters; import aQute.bnd.osgi.Analyzer; import org.apache.commons.io.IOUtils; import java.io.*; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.jar.*; import java.util.logging.Logger; import static aQute.bnd.osgi.Constants.BUNDLE_SYMBOLICNAME; import static aQute.bnd.osgi.Constants.IMPORT_PACKAGE; import static aQute.bnd.osgi.Processor.printClauses; /** * <h1>This implementation addresses the following issues</h1> * * <h2>NEBA-69: Mitigate potential javax.inject import conflicts</h2> * <p> * Sling provides the javax.inject package as a default version (0.0.0). This may lead * to transitive dependency issues when this version collides with another javax.inject version, should * both reside on the dependency chains of the same bundle. To work around this, the javax.inject imports * of the Spring bundles deployed with NEBA are modified to import the default version ([0, 2)). * </p> * * <h2>NEBA-155: Make jackson library dependencies optional</h2> * <p> * AEM 6.x ships with an incomplete export of the jackson library. The jackson package imports of Spring are thus removed in favor of an explicit * relationship to jackson bundles using the "Require-Bundle" header. * </p> * * @author Olaf Otto */ public class SpringBundlesTransformer { public static final String MANIFEST_LOCATION = "META-INF/MANIFEST.MF"; public static void main(String[] args) throws IOException { if (args == null || args.length != 2) { throw new IllegalArgumentException("Expected exactly two arguments, got: " + Arrays.toString(args) + "."); } new SpringBundlesTransformer( asDirectory(args[0]), asDirectory(args[1]) ).run(); } private static File asDirectory(String arg) { File artifactsWorkDir = new File(arg); if (!artifactsWorkDir.exists()) { throw new IllegalArgumentException("The directory " + artifactsWorkDir.getPath() + " does not exist."); } if (!artifactsWorkDir.isDirectory()) { throw new IllegalArgumentException("The directory " + artifactsWorkDir.getPath() + " is not a directory."); } return artifactsWorkDir; } private final Logger logger = Logger.getLogger(getClass().getName()); private final File unpackedArtifactsDir; private final File repackToDirectory; public SpringBundlesTransformer(File unpackedArtifactsDir, File repackToDir) { if (unpackedArtifactsDir == null) { throw new IllegalArgumentException("Method argument unpackedArtifactsDir must not be null."); } if (repackToDir == null) { throw new IllegalArgumentException("Method argument repackToDir must not be null."); } this.unpackedArtifactsDir = unpackedArtifactsDir; this.repackToDirectory = repackToDir; } public void run() throws IOException { for (File dir : listFiles(this.unpackedArtifactsDir)) { logger.info("Transforming manifest in " + dir + " ..."); Manifest manifest = getManifest(dir); Attributes mainAttributes = manifest.getMainAttributes(); String importPackageDirectives = mainAttributes.getValue(IMPORT_PACKAGE); if (importPackageDirectives == null) { continue; } Parameters imports = new Analyzer().parseHeader(importPackageDirectives); if (allowUnstableJavaxImports(imports) || transformJacksonImportsToRequireBundle(mainAttributes, imports)) { updateImportPackageDirectives(mainAttributes, imports); alterSymbolicNameToReflectCustomization(mainAttributes); } write(dir, manifest); repackageArtifact(dir); } } private void alterSymbolicNameToReflectCustomization(Attributes mainAttributes) { String symbolicName = mainAttributes.getValue(BUNDLE_SYMBOLICNAME); mainAttributes.putValue(BUNDLE_SYMBOLICNAME, "io.neba." + symbolicName.substring("org.apache.servicemix.bundles.".length())); } private void updateImportPackageDirectives(Attributes mainAttributes, Parameters imports) throws IOException { mainAttributes.putValue(IMPORT_PACKAGE, printClauses(imports)); } private boolean allowUnstableJavaxImports(Parameters imports) { Attrs attrs = imports.get("javax.inject"); if (attrs == null) { return false; } attrs.put("version", "[0,2)"); return true; } private boolean transformJacksonImportsToRequireBundle(Attributes mainAttributes, Parameters imports) throws IOException { Set<String> jacksonImports = new HashSet<>(); imports.keySet().forEach(key -> { if (key.startsWith("com.fasterxml.jackson")) { jacksonImports.add(key); } }); if (jacksonImports.isEmpty()) { return false; } jacksonImports.forEach(imports::remove); mainAttributes.putValue( "Require-Bundle", "com.fasterxml.jackson.core.jackson-core; bundle-version=\"[2,3)\"; resolution:=optional," + "com.fasterxml.jackson.core.jackson-databind; bundle-version=\"[2,3)\"; resolution:=optional," + "com.fasterxml.jackson.core.jackson-annotations; bundle-version=\"[2,3)\"; resolution:=optional"); return true; } private File[] listFiles(File dir) { File[] files = dir.listFiles(); return files == null ? new File[]{} : files; } private void repackageArtifact(File dir) { File targetJarFile = getTargetJarFile(dir); try { Manifest manifest = getManifest(dir); try (JarOutputStream out = new JarOutputStream(new FileOutputStream(targetJarFile), manifest)) { for (File file : listFiles(dir)) { pack(file, dir, out); } } } catch (IOException e) { throw new IllegalStateException("Unable to create jar file " + targetJarFile.getPath(), e); } } private void pack(File source, File sourceDirectory, JarOutputStream target) throws IOException { // Omit initial "/" in entry names - jar entries must be added in the form directory/file.extension String name = source.getPath().substring(sourceDirectory.getPath().length() + 1).replace("\\", "/"); if (name.isEmpty()) { return; } if (source.isDirectory()) { if (!name.endsWith("/")) { name += "/"; } JarEntry entry = new JarEntry(name); entry.setTime(source.lastModified()); target.putNextEntry(entry); target.closeEntry(); File[] files = listFiles(source); if (files == null) { return; } for (File nestedFile : files) { pack(nestedFile, sourceDirectory, target); } } else { if (JarFile.MANIFEST_NAME.equals(name)) { // Skip manifest: It is provided as the first entry via the jar ouput stream. return; } JarEntry entry = new JarEntry(name); entry.setTime(source.lastModified()); target.putNextEntry(entry); try (InputStream in = new BufferedInputStream(new FileInputStream(source))) { IOUtils.copy(in, target); } target.closeEntry(); } } private File getTargetJarFile(File dir) { return new File(repackToDirectory, dir.getName().replace("-jar", ".jar")); } private void write(File dir, Manifest manifest) { File manifestFile = getManifestFile(dir); try { try (FileOutputStream out = new FileOutputStream(manifestFile)) { manifest.write(out); } } catch (IOException e) { throw new IllegalStateException("Unable to write the manifest " + manifest + " to " + manifestFile.getPath() + ".", e); } } private Manifest getManifest(File dir) { File manifestFile = getManifestFile(dir); try { try (FileInputStream is = new FileInputStream(manifestFile)) { return new Manifest(is); } } catch (IOException e) { throw new IllegalStateException("Could not locate manifest in " + manifestFile.getPath() + ".", e); } } private File getManifestFile(File dir) { return new File(dir, MANIFEST_LOCATION); } }