/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.aries.spifly.statictool;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.apache.aries.spifly.ConsumerHeaderProcessor;
import org.apache.aries.spifly.SpiFlyConstants;
import org.apache.aries.spifly.Streams;
import org.apache.aries.spifly.Util;
import org.apache.aries.spifly.WeavingData;
import org.apache.aries.spifly.weaver.TCCLSetterVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
public class Main {
private static final String MODIFIED_BUNDLE_SUFFIX = "_spifly.jar";
private static final String IMPORT_PACKAGE = "Import-Package";
public static void usage() {
System.err.println("This tool processes OSGi Bundles that use java.util.ServiceLoader.load() to");
System.err.println("obtain implementations via META-INF/services. The byte code in the bundles is");
System.err.println("modified so that the ThreadContextClassLoader is set appropriately for the ");
System.err.println("duration of the java.util.ServiceLoader.load() call.");
System.err.println("To opt-in to this process, bundles need to have the following MANIFEST.MF");
System.err.println("header set:");
System.err.println(" " + SpiFlyConstants.SPI_CONSUMER_HEADER + ": *");
System.err.println("Modified bundles are written out under the following name:");
System.err.println(" <original-bundle-name>" + MODIFIED_BUNDLE_SUFFIX);
System.err.println();
System.err.println("Usage: java " + Main.class.getName() + " bundle1.jar bundle2.jar ...");
System.exit(-1);
}
public static void main(String ... args) throws Exception {
if (args.length < 1)
usage();
for (String arg : args) {
weaveJar(arg);
}
}
private static void weaveJar(String jarPath) throws Exception {
System.out.println("[SPI Fly Static Tool] Processing: " + jarPath);
File jarFile = new File(jarPath);
File tempDir = new File(System.getProperty("java.io.tmpdir") + File.separator + jarFile.getName() + "_" + System.currentTimeMillis());
Manifest manifest = unJar(jarFile, tempDir);
String consumerHeaderVal = manifest.getMainAttributes().getValue(SpiFlyConstants.SPI_CONSUMER_HEADER);
String consumerHeaderKey = null;
if (consumerHeaderVal != null) {
consumerHeaderKey = SpiFlyConstants.SPI_CONSUMER_HEADER;
} else {
consumerHeaderVal = manifest.getMainAttributes().getValue(SpiFlyConstants.REQUIRE_CAPABILITY);
if (consumerHeaderVal != null) {
consumerHeaderKey = SpiFlyConstants.REQUIRE_CAPABILITY;
}
}
if (consumerHeaderVal != null) {
String bcp = manifest.getMainAttributes().getValue(Constants.BUNDLE_CLASSPATH);
weaveDir(tempDir, consumerHeaderKey, consumerHeaderVal, bcp);
if (SpiFlyConstants.SPI_CONSUMER_HEADER.equals(consumerHeaderKey)) {
manifest.getMainAttributes().remove(new Attributes.Name(SpiFlyConstants.SPI_CONSUMER_HEADER));
manifest.getMainAttributes().putValue(SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER, consumerHeaderVal);
} else {
// It's SpiFlyConstants.REQUIRE_CAPABILITY
// Take out the processor requirement, this probably needs to be improved a little bit
String newConsumerHeaderVal = consumerHeaderVal.replaceAll(
"osgi[.]extender;\\s*filter[:][=][\"]?[(]osgi[.]extender[=]osgi[.]serviceloader[.]processor[)][\"]?", "").
trim();
if (newConsumerHeaderVal.startsWith(","))
newConsumerHeaderVal = newConsumerHeaderVal.substring(1);
if (newConsumerHeaderVal.endsWith(","))
newConsumerHeaderVal = newConsumerHeaderVal.substring(0, newConsumerHeaderVal.length()-1);
manifest.getMainAttributes().putValue(SpiFlyConstants.REQUIRE_CAPABILITY, newConsumerHeaderVal);
manifest.getMainAttributes().putValue("X-SpiFly-Processed-Require-Capability", consumerHeaderVal);
}
// TODO if new packages needed then...
extendImportPackage(manifest);
File newJar = getNewJarFile(jarFile);
jar(newJar, tempDir, manifest);
} else {
System.out.println("[SPI Fly Static Tool] This file is not marked as an SPI Consumer.");
}
delTree(tempDir);
}
private static void extendImportPackage(Manifest manifest) throws IOException {
String utilPkgVersion = getPackageVersion(Util.class);
Version osgiVersion = Version.parseVersion(utilPkgVersion);
Version minVersion = new Version(osgiVersion.getMajor(), osgiVersion.getMinor(), osgiVersion.getMicro());
Version maxVersion = new Version(osgiVersion.getMajor(), osgiVersion.getMinor() + 1, 0);
String ip = manifest.getMainAttributes().getValue(IMPORT_PACKAGE);
if (ip == null)
ip = "";
StringBuilder sb = new StringBuilder(ip);
if (ip.length() > 0)
sb.append(",");
sb.append(Util.class.getPackage().getName());
sb.append(";version=\"[");
sb.append(minVersion);
sb.append(",");
sb.append(maxVersion);
sb.append(")\"");
manifest.getMainAttributes().putValue(IMPORT_PACKAGE, sb.toString());
}
private static String getPackageVersion(Class<?> clazz) throws IOException {
URL url = clazz.getResource("packageinfo");
if (url == null) {
throw new RuntimeException("'packageinfo' file with version information not found for package: "
+ clazz.getPackage().getName());
}
byte[] bytes = Streams.suck(url.openStream());
Properties p = new Properties();
p.load(new ByteArrayInputStream(bytes));
return p.getProperty("version");
}
private static File getNewJarFile(File jarFile) {
String s = jarFile.getAbsolutePath();
int idx = s.lastIndexOf('.');
s = s.substring(0, idx);
s += MODIFIED_BUNDLE_SUFFIX;
return new File(s);
}
private static void weaveDir(File dir, String consumerHeaderKey, String consumerHeaderValue, String bundleClassPath) throws Exception {
Set<WeavingData> wd = ConsumerHeaderProcessor.processHeader(consumerHeaderKey, consumerHeaderValue);
URLClassLoader cl = new URLClassLoader(new URL [] {dir.toURI().toURL()}, Main.class.getClassLoader());
String dirName = dir.getAbsolutePath();
DirTree dt = new DirTree(dir);
for (File f : dt.getFiles()) {
if (!f.getName().endsWith(".class"))
continue;
String className = f.getAbsolutePath().substring(dirName.length());
if (className.startsWith(File.separator))
className = className.substring(1);
className = className.substring(0, className.length() - ".class".length());
className = className.replace(File.separator, ".");
InputStream is = new FileInputStream(f);
byte[] b;
try {
ClassReader cr = new ClassReader(is);
ClassWriter cw = new StaticToolClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES, cl);
TCCLSetterVisitor cv = new TCCLSetterVisitor(cw, className, wd);
cr.accept(cv, ClassReader.SKIP_FRAMES);
if (cv.isWoven()) {
b = cw.toByteArray();
} else {
// if not woven, store the original bytes
b = Streams.suck(new FileInputStream(f));
}
} finally {
is.close();
}
OutputStream os = new FileOutputStream(f);
try {
os.write(b);
} finally {
os.close();
}
}
if (bundleClassPath != null) {
for (String entry : bundleClassPath.split(",")) {
File jarFile = new File(dir, entry.trim());
if (jarFile.isFile()) {
weaveBCPJar(jarFile, consumerHeaderKey, consumerHeaderValue);
}
}
}
}
private static void weaveBCPJar(File jarFile, String consumerHeaderKey, String consumerHeaderVal) throws Exception {
File tempDir = new File(System.getProperty("java.io.tmpdir") + File.separator + jarFile.getName() + "_" + System.currentTimeMillis());
try {
Manifest manifest = unJar(jarFile, tempDir);
weaveDir(tempDir, consumerHeaderKey, consumerHeaderVal, null);
if (!jarFile.delete()) {
throw new IOException("Could not replace file: " + jarFile);
}
jar(jarFile, tempDir, manifest);
} finally {
delTree(tempDir);
}
}
static Manifest unJar(File jarFile, File tempDir) throws IOException {
ensureDirectory(tempDir);
JarInputStream jis = new JarInputStream(new FileInputStream(jarFile));
JarEntry je = null;
while((je = jis.getNextJarEntry()) != null) {
if (je.isDirectory()) {
File outDir = new File(tempDir, je.getName());
ensureDirectory(outDir);
continue;
}
File outFile = new File(tempDir, je.getName());
File outDir = outFile.getParentFile();
ensureDirectory(outDir);
OutputStream out = new FileOutputStream(outFile);
try {
Streams.pump(jis, out);
} finally {
out.flush();
out.close();
jis.closeEntry();
}
outFile.setLastModified(je.getTime());
}
Manifest manifest = jis.getManifest();
if (manifest != null) {
File mf = new File(tempDir, "META-INF/MANIFEST.MF");
File mfDir = mf.getParentFile();
ensureDirectory(mfDir);
OutputStream out = new FileOutputStream(mf);
try {
manifest.write(out);
} finally {
out.flush();
out.close();
}
}
jis.close();
return manifest;
}
static void jar(File jarFile, File rootFile, Manifest manifest) throws IOException {
JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile), manifest);
try {
addToJarRecursively(jos, rootFile.getAbsoluteFile(), rootFile.getAbsolutePath());
} finally {
jos.close();
}
}
static void addToJarRecursively(JarOutputStream jar, File source, String rootDirectory) throws IOException {
String sourceName = source.getAbsolutePath().replace("\\", "/");
sourceName = sourceName.substring(rootDirectory.length());
if (sourceName.startsWith("/")) {
sourceName = sourceName.substring(1);
}
if ("META-INF/MANIFEST.MF".equals(sourceName))
return;
if (source.isDirectory()) {
/* Is there any point in adding a directory beyond just taking up space?
if (!sourceName.isEmpty()) {
if (!sourceName.endsWith("/")) {
sourceName += "/";
}
JarEntry entry = new JarEntry(sourceName);
jar.putNextEntry(entry);
jar.closeEntry();
}
*/
for (File nested : source.listFiles()) {
addToJarRecursively(jar, nested, rootDirectory);
}
return;
}
JarEntry entry = new JarEntry(sourceName);
jar.putNextEntry(entry);
InputStream is = new FileInputStream(source);
try {
Streams.pump(is, jar);
} finally {
jar.closeEntry();
is.close();
}
}
static void delTree(File tempDir) throws IOException {
for (File f : new DirTree(tempDir).getFiles()) {
if (!f.delete())
throw new IOException("Problem deleting file: " + tempDir.getAbsolutePath());
}
}
private static void ensureDirectory(File outDir) throws IOException {
if (!outDir.isDirectory())
if (!outDir.mkdirs())
throw new IOException("Unable to create directory " + outDir.getAbsolutePath());
}
}