blob: 80afa3eb8791ce920017b014b0d7d66c3eb9fdc6 [file] [log] [blame]
/* Copyright 2006 aQute SARL
* Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
package aQute.lib.osgi;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import java.util.zip.*;
/**
* Include-Resource: ( [name '=' ] file )+
*
* Private-Package: package-decl ( ',' package-decl )*
*
* Export-Package: package-decl ( ',' package-decl )*
*
* Import-Package: package-decl ( ',' package-decl )*
*
* @version $Revision: 1.4 $
*/
public class Builder extends Analyzer {
private static final int SPLIT_MERGE_LAST = 1;
private static final int SPLIT_MERGE_FIRST = 2;
private static final int SPLIT_ERROR = 3;
private static final int SPLIT_FIRST = 4;
private static final int SPLIT_DEFAULT = 0;
List tempJars = new ArrayList();
boolean sources = false;
File[] sourcePath;
Pattern NAME_URL = Pattern
.compile("(.*)(http://.*)");
public Jar build() throws Exception {
begin();
dot = new Jar("dot");
doPrebuild(dot, parseHeader(getProperty("-prebuild")));
doExpand(dot);
doIncludeResources(dot);
doConditional(dot);
dot.setManifest(calcManifest());
// This must happen after we analyzed so
// we know what it is on the classpath
addSources(dot);
if (getProperty(POM) != null)
doPom(dot);
doVerify(dot);
if (dot.getResources().isEmpty())
error("The JAR is empty");
dot.updateModified(lastModified());
return dot;
}
void begin() {
super.begin();
if (getProperty(IMPORT_PACKAGE) == null)
setProperty(IMPORT_PACKAGE, "*");
sources = getProperty(SOURCES) != null;
}
private void doConditional(Jar dot) throws IOException {
Map conditionals = getHeader(CONDITIONAL_PACKAGE);
if (conditionals != null && conditionals.size() > 0) {
int size;
do {
size = dot.getDirectories().size();
analyze();
analyzed = false;
Map imports = getImports();
Map filtered = merge(CONDITIONAL_PACKAGE, conditionals,
imports, new HashSet());
// remove existing packages to prevent merge errors
filtered.keySet().removeAll(dot.getPackages());
filtered = replaceWithPattern(filtered);
doExpand(dot, CONDITIONAL_PACKAGE, filtered);
} while (dot.getDirectories().size() > size);
}
}
/**
* Intercept the call to analyze and cleanup versions after we have analyzed
* the setup. We do not want to cleanup if we are going to verify.
*/
public void analyze() throws IOException {
super.analyze();
cleanupVersion(imports);
cleanupVersion(exports);
String version = getProperty(BUNDLE_VERSION);
if (version != null)
setProperty(BUNDLE_VERSION, cleanupVersion(version));
}
public void cleanupVersion(Map mapOfMap) {
for (Iterator e = mapOfMap.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
Map attributes = (Map) entry.getValue();
if (attributes.containsKey("version")) {
attributes.put("version", cleanupVersion((String) attributes
.get("version")));
}
}
}
/**
* Clean up version parameters. Other builders use more fuzzy definitions of
* the version syntax. This method cleans up such a version to match an OSGi
* version.
*
* @param version
* @return
*/
static Pattern fuzzyVersion = Pattern
.compile(
"(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
Pattern.DOTALL);
static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)",
Pattern.DOTALL);
static Pattern nummeric = Pattern.compile("\\d*");
static public String cleanupVersion(String version) {
Matcher m = fuzzyVersion.matcher(version);
if (m.matches()) {
StringBuffer result = new StringBuffer();
String d1 = m.group(1);
String d2 = m.group(3);
String d3 = m.group(5);
String qualifier = m.group(7);
if (d1 != null) {
result.append(d1);
if (d2 != null) {
result.append(".");
result.append(d2);
if (d3 != null) {
result.append(".");
result.append(d3);
if (qualifier != null) {
result.append(".");
cleanupModifier(result, qualifier);
}
} else if (qualifier != null) {
result.append(".0.");
cleanupModifier(result, qualifier);
}
} else if (qualifier != null) {
result.append(".0.0.");
cleanupModifier(result, qualifier);
}
return result.toString();
}
}
return version;
}
static void cleanupModifier(StringBuffer result, String modifier) {
Matcher m = fuzzyModifier.matcher(modifier);
if (m.matches())
modifier = m.group(2);
for (int i = 0; i < modifier.length(); i++) {
char c = modifier.charAt(i);
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
result.append(c);
}
}
/**
*
*/
private void addSources(Jar dot) {
if (!sources)
return;
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getProperties().store(out, "Generated by BND, at " + new Date());
dot.putResource("OSGI-OPT/bnd.bnd", new EmbeddedResource(out
.toByteArray(), 0));
out.close();
} catch (Exception e) {
error("Can not embed bnd file in JAR: " + e);
}
for (Iterator cpe = classspace.keySet().iterator(); cpe.hasNext();) {
String path = (String) cpe.next();
path = path.substring(0, path.length() - ".class".length())
+ ".java";
for (int i = 0; i < sourcePath.length; i++) {
File root = sourcePath[i];
File f = getFile(root, path);
if (f.exists()) {
dot
.putResource("OSGI-OPT/src/" + path,
new FileResource(f));
}
}
}
}
private void doVerify(Jar dot) throws Exception {
Verifier verifier = new Verifier(dot, getProperties());
verifier.setPedantic(isPedantic());
verifier.verify();
errors.addAll(verifier.getErrors());
warnings.addAll(verifier.getWarnings());
}
private void doExpand(Jar jar) throws IOException {
if (classpath.size() == 0
&& (getProperty(EXPORT_PACKAGE) != null || getProperty(PRIVATE_PACKAGE) != null))
warning("Classpath is empty. Private-Package and Export-Package can only expand from the classpath when there is one");
Map prive = replaceWithPattern(getHeader(Analyzer.PRIVATE_PACKAGE));
Map export = replaceWithPattern(getHeader(Analyzer.EXPORT_PACKAGE));
if (prive.isEmpty() && export.isEmpty()) {
warnings
.add("Neither Export-Package nor Private-Package is set, therefore no packages will be included");
}
doExpand(jar, EXPORT_PACKAGE, export);
doExpand(jar, PRIVATE_PACKAGE, prive);
}
private void doExpand(Jar jar, String name, Map instructions) {
Set superfluous = new HashSet(instructions.keySet());
for (Iterator c = classpath.iterator(); c.hasNext();) {
Jar now = (Jar) c.next();
doExpand(jar, instructions, now, superfluous);
}
if (superfluous.size() > 0) {
StringBuffer sb = new StringBuffer();
String del = "Instructions for " + name + " that are never used: ";
for (Iterator i = superfluous.iterator(); i.hasNext();) {
Instruction p = (Instruction) i.next();
sb.append(del);
sb.append(p.getPattern());
del = ", ";
}
warning(sb.toString());
}
}
/**
* Iterate over each directory in the class path entry and check if that
* directory is a desired package.
*
* @param included
* @param classpathEntry
*/
private void doExpand(Jar jar, Map included, Jar classpathEntry,
Set superfluous) {
loop: for (Iterator p = classpathEntry.getDirectories().entrySet()
.iterator(); p.hasNext();) {
Map.Entry directory = (Map.Entry) p.next();
String path = (String) directory.getKey();
if (doNotCopy.matcher(getName(path)).matches())
continue;
if (directory.getValue() == null)
continue;
String pack = path.replace('/', '.');
Instruction instr = matches(included, pack, superfluous);
if (instr != null) {
// System.out.println("Pattern match: " + pack + " " +
// instr.getPattern() + " " + instr.isNegated());
if (!instr.isNegated()) {
Map contents = (Map) directory.getValue();
// What to do with split packages? Well if this
// directory already exists, we will check the strategy
// and react accordingly.
boolean overwriteResource = true;
if (jar.hasDirectory(path)) {
Map directives = (Map) included.get(instr);
switch (getSplitStrategy((String) directives
.get(SPLIT_PACKAGE_DIRECTIVE))) {
case SPLIT_MERGE_LAST:
overwriteResource = true;
break;
case SPLIT_MERGE_FIRST:
overwriteResource = false;
break;
case SPLIT_ERROR:
error("Split package generates error: " + pack);
continue loop;
case SPLIT_FIRST:
continue loop;
default:
// Default is like merge-first, but with a warning
warning("There are split packages, use directive -split-package:=(merge-first|merge-last) on instruction to get rid of this warning: "
+ pack
+ ", classpath: "
+ classpath
+ " from: " + classpathEntry.source);
overwriteResource = false;
break;
}
}
jar.addDirectory(contents, overwriteResource);
}
}
}
}
private int getSplitStrategy(String type) {
if (type == null)
return SPLIT_DEFAULT;
if (type.equals("merge-last"))
return SPLIT_MERGE_LAST;
if (type.equals("merge-first"))
return SPLIT_MERGE_FIRST;
if (type.equals("error"))
return SPLIT_ERROR;
if (type.equals("first"))
return SPLIT_FIRST;
error("Invalid strategy for split-package: " + type);
return SPLIT_DEFAULT;
}
private Map replaceWithPattern(Map header) {
Map map = new LinkedHashMap();
for (Iterator e = header.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
String pattern = (String) entry.getKey();
Instruction instr = Instruction.getPattern(pattern);
map.put(instr, entry.getValue());
}
return map;
}
private Instruction matches(Map instructions, String pack,
Set superfluousPatterns) {
for (Iterator i = instructions.keySet().iterator(); i.hasNext();) {
Instruction pattern = (Instruction) i.next();
if (pattern.matches(pack)) {
superfluousPatterns.remove(pattern);
return pattern;
}
}
return null;
}
private Map getHeader(String string) {
if (string == null)
return new LinkedHashMap();
return parseHeader(getProperty(string));
}
/**
* 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 IOException
* @throws FileNotFoundException
*/
private void doIncludeResources(Jar jar) throws Exception {
Macro macro = new Macro(getProperties(), this);
String includes = getProperty("Bundle-Includes");
if (includes == null)
includes = getProperty("Include-Resource");
else
warnings
.add("Please use Include-Resource instead of Bundle-Includes");
if (includes == null)
return;
for (Iterator i = getClauses(includes).iterator(); i.hasNext();) {
boolean preprocess = false;
String clause = (String) i.next();
if (clause.startsWith("{") && clause.endsWith("}")) {
preprocess = true;
clause = clause.substring(1, clause.length() - 1).trim();
}
Map extra = new HashMap();
int n = clause.indexOf(';');
if (n > 0) {
String attributes = clause.substring(n + 1);
String parts[] = attributes.split("\\s*;\\s*");
for (int j = 0; j < parts.length; j++) {
String assignment[] = parts[j].split("\\s*=\\s*");
if (assignment.length == 2)
extra.put(assignment[0], assignment[1]);
else
error("Invalid attribute on Include-Resource: "
+ clause);
}
clause = clause.substring(0, n);
}
if (clause.startsWith("@")) {
extractFromJar(jar, clause.substring(1));
} else {
String parts[] = clause.split("\\s*=\\s*");
String source;
File sourceFile;
String destinationPath;
if (parts.length == 1) {
// Just a copy, destination path defined by
// source path.
source = parts[0];
sourceFile = getFile(base, source);
// Directories should be copied to the root
// but files to their file name ...
if (sourceFile.isDirectory())
destinationPath = "";
else
destinationPath = sourceFile.getName();
} else {
source = parts[1];
sourceFile = getFile(base, source);
destinationPath = parts[0];
}
// Some people insist on ending a directory with
// a slash ... it now also works if you do /=dir
if (destinationPath.endsWith("/"))
destinationPath = destinationPath.substring(0,
destinationPath.length() - 1);
if (!sourceFile.exists()) {
Jar src = getJarFromName(source, "Include-Resource "
+ source);
if (src != null) {
JarResource jarResource = new JarResource(src);
jar.putResource(destinationPath, jarResource);
} else {
error("Input file does not exist: " + source);
}
} else
copy(jar, destinationPath, sourceFile, preprocess ? macro
: null, extra);
}
}
}
/**
* Extra resources from a Jar and add them to the given jar. The clause is
* the
*
* @param jar
* @param clauses
* @param i
* @throws ZipException
* @throws IOException
*/
private void extractFromJar(Jar jar, String name) throws ZipException,
IOException {
// Inline all resources and classes from another jar
// optionally appended with a modified regular expression
// like @zip.jar!/META-INF/MANIFEST.MF
int n = name.lastIndexOf("!/");
Pattern filter = null;
if (n > 0) {
String fstring = name.substring(n + 2);
name = name.substring(0, n);
filter = wildcard(fstring);
}
Jar sub = getJarFromName(name, "extract from jar");
if (sub == null)
error("Can not find JAR file " + name);
else {
jar.addAll(sub, filter);
tempJars.add(sub);
}
}
private Pattern wildcard(String spec) {
StringBuffer sb = new StringBuffer();
for (int j = 0; j < spec.length(); j++) {
char c = spec.charAt(j);
switch (c) {
case '.':
sb.append("\\.");
break;
case '*':
// test for ** (all directories)
if (j < spec.length() - 1 && spec.charAt(j + 1) == '*') {
sb.append(".*");
j++;
} else
sb.append("[^/]*");
break;
default:
sb.append(c);
break;
}
}
String s = sb.toString();
try {
return Pattern.compile(s);
} catch (Exception e) {
error("Invalid regular expression on wildcarding: " + spec
+ " used *");
}
return null;
}
private void copy(Jar jar, String path, File from, Macro macro, Map extra)
throws Exception {
if (doNotCopy.matcher(from.getName()).matches())
return;
if (from.isDirectory()) {
String next = path;
if (next.length() != 0)
next += '/';
File files[] = from.listFiles();
for (int i = 0; i < files.length; i++) {
copy(jar, next + files[i].getName(), files[i], macro, extra);
}
} else {
if (from.exists()) {
if (macro != null) {
String content = read(from);
content = macro.process(content);
Resource resource = new EmbeddedResource(content
.getBytes("UTF-8"), from.lastModified());
String x = (String) extra.get("extra");
if (x != null)
resource.setExtra(x);
jar.putResource(path, resource);
} else
jar.putResource(path, new FileResource(from));
} else {
error("Input file does not exist: " + from);
}
}
}
private String read(File from) throws Exception {
long size = from.length();
byte[] buffer = new byte[(int) size];
FileInputStream in = new FileInputStream(from);
in.read(buffer);
in.close();
return new String(buffer, "UTF-8");
}
private String getName(String where) {
int n = where.lastIndexOf('/');
if (n < 0)
return where;
return where.substring(n + 1);
}
public void setSourcepath(File[] files) {
sourcePath = files;
}
/**
* Create a POM reseource for Maven containing as much information as
* possible from the manifest.
*
* @param output
* @param builder
* @throws FileNotFoundException
* @throws IOException
*/
public void doPom(Jar dot) throws FileNotFoundException, IOException {
{
Manifest manifest = dot.getManifest();
String name = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_NAME);
String description = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_DESCRIPTION);
String docUrl = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_DOCURL);
String version = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_VERSION);
String bundleVendor = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_VENDOR);
ByteArrayOutputStream s = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(s);
String bsn = manifest.getMainAttributes().getValue(
Analyzer.BUNDLE_SYMBOLICNAME);
String licenses = manifest.getMainAttributes().getValue(
BUNDLE_LICENSE);
if (bsn == null) {
errors
.add("Can not create POM unless Bundle-SymbolicName is set");
return;
}
bsn = bsn.trim();
int n = bsn.lastIndexOf('.');
if (n <= 0) {
errors
.add("Can not create POM unless Bundle-SymbolicName contains a .");
ps.close();
s.close();
return;
}
String groupId = bsn.substring(0, n);
String artifactId = bsn.substring(n + 1);
ps
.println("<project xmlns='http://maven.apache.org/POM/4.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd'>");
ps.println(" <modelVersion>4.0.0</modelVersion>");
ps.println(" <groupId>" + groupId + "</groupId>");
n = artifactId.indexOf(';');
if (n > 0)
artifactId = artifactId.substring(0, n).trim();
ps.println(" <artifactId>" + artifactId + "</artifactId>");
ps.println(" <version>" + version + "</version>");
if (description != null) {
ps.println(" <description>");
ps.print(" ");
ps.println(description);
ps.println(" </description>");
}
if (name != null) {
ps.print(" <name>");
ps.print(name);
ps.println("</name>");
}
if (docUrl != null) {
ps.print(" <url>");
ps.print(docUrl);
ps.println("</url>");
}
if (bundleVendor != null) {
Matcher m = NAME_URL.matcher(bundleVendor);
String namePart = bundleVendor;
String urlPart = null;
if (m.matches()) {
namePart = m.group(1);
urlPart = m.group(2);
}
ps.println(" <organization>");
ps.print(" <name>");
ps.print(namePart.trim());
ps.println("</name>");
if (urlPart != null) {
ps.print(" <url>");
ps.print(urlPart.trim());
ps.println("</url>");
}
ps.println(" </organization>");
}
if (licenses != null) {
ps.println(" <licenses>");
Map map = parseHeader(licenses);
for (Iterator e = map.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
ps.println(" <license>");
Map values = (Map) entry.getValue();
print(ps, values, "name", "name", (String) values
.get("url"));
print(ps, values, "url", "url", null);
print(ps, values, "distribution", "distribution", "repo");
ps.println(" </license>");
}
ps.println(" </licenses>");
}
ps.println("</project>");
ps.close();
s.close();
dot
.putResource("pom.xml", new EmbeddedResource(s
.toByteArray(), 0));
}
}
/**
* NEW
*
*/
void doPrebuild(Jar dot, Map clauses) {
for (Iterator i = clauses.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
String name = (String) entry.getKey();
Map args = (Map) entry.getValue();
File file = getFile(base, name);
File dir = file.getParentFile();
if (!dir.exists())
error("No directory for prebuild: " + file.getAbsolutePath());
else {
Instruction instr = Instruction.getPattern(file.getName());
File[] children = dir.listFiles();
for (int c = 0; c < children.length; c++) {
File child = children[c];
if (instr.matches(child.getName()) && !instr.isNegated()) {
try {
progress("Prebuilding "+child);
doPrebuild(dot, child, args);
} catch (Exception e) {
error("Can not build: "+child);
}
}
}
}
}
}
void doPrebuild(Jar dot, File f, Map args) throws Exception {
Builder builder = new Builder();
builder.setBase(base);
builder.setProperties(f);
Properties p = new Properties();
p.putAll(getProperties());
p.keySet().removeAll(builder.getProperties().keySet());
builder.getProperties().putAll(p);
Jar jar = builder.build();
String path = (String) args.get("path");
if (path != null)
path = f.getName();
dot.putResource(path, new JarResource(jar));
}
/**
* Utility function to print a tag from a map
*
* @param ps
* @param values
* @param string
* @param tag
* @param object
*/
private void print(PrintStream ps, Map values, String string, String tag,
String object) {
String value = (String) values.get(string);
if (value == null)
value = object;
if (value == null)
return;
ps.println(" <" + tag + ">" + value.trim() + "</" + tag + ">");
}
public void close() {
for (Iterator j = tempJars.iterator(); j.hasNext();) {
Jar jar = (Jar) j.next();
jar.close();
}
super.close();
}
}