/* 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;
/**
* This class can calculate the required headers for a (potential) JAR file. It
* analyzes a directory or JAR for the packages that are contained and that are
* referred to by the bytecodes. The user can the use regular expressions to
* define the attributes and directives. The matching is not fully regex for
* convenience. A * and ? get a . prefixed and dots are escaped.
*
* <pre>
* *;auto=true any
* org.acme.*;auto=true org.acme.xyz
* org.[abc]*;auto=true org.acme.xyz
* </pre>
*
* Additional, the package instruction can start with a '=' or a '!'. The '!'
* indicates negation. Any matching package is removed. The '=' is literal, the
* expression will be copied verbatim and no matching will take place.
*
* Any headers in the given properties are used in the output properties.
*/
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes.*;
import java.util.regex.*;
import aQute.lib.filter.*;
public class Analyzer extends Processor {
public final static String BUNDLE_CLASSPATH = "Bundle-ClassPath";
public final static String BUNDLE_COPYRIGHT = "Bundle-Copyright";
public final static String BUNDLE_DESCRIPTION = "Bundle-Description";
public final static String BUNDLE_NAME = "Bundle-Name";
public final static String BUNDLE_NATIVECODE = "Bundle-NativeCode";
public final static String EXPORT_PACKAGE = "Export-Package";
public final static String EXPORT_SERVICE = "Export-Service";
public final static String IMPORT_PACKAGE = "Import-Package";
public final static String DYNAMICIMPORT_PACKAGE = "DynamicImport-Package";
public final static String IMPORT_SERVICE = "Import-Service";
public final static String BUNDLE_VENDOR = "Bundle-Vendor";
public final static String BUNDLE_VERSION = "Bundle-Version";
public final static String BUNDLE_DOCURL = "Bundle-DocURL";
public final static String BUNDLE_CONTACTADDRESS = "Bundle-ContactAddress";
public final static String BUNDLE_ACTIVATOR = "Bundle-Activator";
public final static String BUNDLE_REQUIREDEXECUTIONENVIRONMENT = "Bundle-RequiredExecutionEnvironment";
public final static String BUNDLE_SYMBOLICNAME = "Bundle-SymbolicName";
public final static String BUNDLE_LOCALIZATION = "Bundle-Localization";
public final static String REQUIRE_BUNDLE = "Require-Bundle";
public final static String FRAGMENT_HOST = "Fragment-Host";
public final static String BUNDLE_MANIFESTVERSION = "Bundle-ManifestVersion";
public final static String SERVICE_COMPONENT = "Service-Component";
public final static String BUNDLE_LICENSE = "Bundle-License";
public static final String PRIVATE_PACKAGE = "Private-Package";
public static final String IGNORE_PACKAGE = "Ignore-Package";
public static final String INCLUDE_RESOURCE = "Include-Resource";
public static final String CONDITIONAL_PACKAGE = "Conditional-Package";
public static final String BND_LASTMODIFIED = "Bnd-LastModified";
public static final String CREATED_BY = "Created-By";
public static final String TOOL = "Tool";
public final static String headers[] = {
BUNDLE_ACTIVATOR, BUNDLE_CONTACTADDRESS, BUNDLE_COPYRIGHT,
BUNDLE_DESCRIPTION, BUNDLE_DOCURL, BUNDLE_LOCALIZATION,
BUNDLE_NATIVECODE, BUNDLE_VENDOR, BUNDLE_VERSION, BUNDLE_LICENSE,
BUNDLE_CLASSPATH, SERVICE_COMPONENT, EXPORT_PACKAGE,
IMPORT_PACKAGE, BUNDLE_LOCALIZATION, BUNDLE_MANIFESTVERSION,
BUNDLE_NAME, BUNDLE_NATIVECODE,
BUNDLE_REQUIREDEXECUTIONENVIRONMENT, BUNDLE_SYMBOLICNAME,
BUNDLE_VERSION, FRAGMENT_HOST, PRIVATE_PACKAGE, IGNORE_PACKAGE,
INCLUDE_RESOURCE, REQUIRE_BUNDLE, IMPORT_SERVICE, EXPORT_SERVICE,
CONDITIONAL_PACKAGE, BND_LASTMODIFIED };
public static final String REMOVE_HEADERS = "-removeheaders";
public static final String SOURCES = "-sources";
public static final String FAIL_OK = "-failok";
public static final String DONOTCOPY = "-donotcopy";
public static final String EXPORT_CONTENTS = "-exportcontents";
public static final String CLASSPATH = "-classpath";
public static final String PEDANTIC = "-pedantic";
public static final String NOEXTRAHEADERS = "-noextraheaders";
public static final String POM = "-pom";
public static final String INCLUDE = "-include";
public static final String NOUSES = "-nouses";
public static final String options[] = {
REMOVE_HEADERS, SOURCES, FAIL_OK, DONOTCOPY, EXPORT_CONTENTS,
CLASSPATH, PEDANTIC, NOEXTRAHEADERS, POM, INCLUDE, NOUSES };
public static final String SPLIT_PACKAGE_DIRECTIVE = "-split-package:";
public static final String NO_IMPORT_DIRECTIVE = "-noimport:";
public static final String directives[] = {
SPLIT_PACKAGE_DIRECTIVE, NO_IMPORT_DIRECTIVE, "resolution:",
"include:", "uses:", "exclude:",
// TODO
};
public static final String[] componentDirectives = new String[] {
"factory:", "immediate:", "enabled:", "dynamic:", "multiple:",
"provide:", "optional:", "properties:" };
static Map EES = new HashMap();
static Set SET_COMPONENT_DIRECTIVES = new HashSet(
Arrays
.asList(componentDirectives));
static final Pattern VALID_PROPERTY_TYPES = Pattern
.compile("(String|Long|Double|Float|Integer|Byte|Character|Boolean|Short)");
static Pattern doNotCopy = Pattern
.compile("CVS|.svn");
static String version;
/**
* For each import, find the exporter and see what you can learn from it.
*/
static Pattern versionPattern = Pattern
.compile("(\\d+\\.\\d+)\\.\\d+");
private Properties properties /* String->String */= new Properties();
File base = new File(
"")
.getAbsoluteFile();
Map contained /* String->Map */= new HashMap(); // package
Map referred /* String->Map */= new HashMap(); // package
Map uses /* String->Map */= new HashMap(); // package
Map classspace;
boolean analyzed;
Map exports;
Map imports;
Map bundleClasspath; // Bundle
Map ignored /*
* String ->
* Map
*/= new HashMap(); // Ignored
// packages
Jar dot; // The
Map cpExports = new HashMap();
String activator;
List classpath = new ArrayList();
Macro replacer = new Macro(
this);
long lastModified;
Manifest bndManifest;
boolean noExtraHeaders;
boolean fixedupProperties = false;
/**
* Specifically for Maven
*
* @param properties
* the properties
*/
public static Properties getManifest(File dirOrJar) throws IOException {
Analyzer analyzer = new Analyzer();
analyzer.setJar(dirOrJar);
Properties properties = new Properties();
properties.put(IMPORT_PACKAGE, "*");
properties.put(EXPORT_PACKAGE, "*");
analyzer.setProperties(properties);
Manifest m = analyzer.calcManifest();
Properties result = new Properties();
for (Iterator i = m.getMainAttributes().keySet().iterator(); i
.hasNext();) {
Attributes.Name name = (Attributes.Name) i.next();
result.put(name.toString(), m.getMainAttributes().getValue(name));
}
return result;
}
/**
* Calcualtes the data structures for generating a manifest.
*
* @throws IOException
*/
public void analyze() throws IOException {
if (!analyzed) {
begin();
analyzed = true;
cpExports = new HashMap();
activator = getProperty(BUNDLE_ACTIVATOR);
bundleClasspath = parseHeader(getProperty(BUNDLE_CLASSPATH));
analyzeClasspath();
classspace = analyzeBundleClasspath(dot, bundleClasspath,
contained, referred, uses);
if (activator != null) {
// Add the package of the activator to the set
// of referred classes. This must be done before we remove
// contained set.
int n = activator.lastIndexOf('.');
if (n > 0) {
referred
.put(activator.substring(0, n), new LinkedHashMap());
}
}
referred.keySet().removeAll(contained.keySet());
Map exportInstructions = parseHeader(getProperty(EXPORT_PACKAGE));
Map additionalExportInstructions = parseHeader(getProperty(EXPORT_CONTENTS));
exportInstructions.putAll(additionalExportInstructions);
Map importInstructions = parseHeader(getProperty(IMPORT_PACKAGE));
Map dynamicImports = parseHeader(getProperty(DYNAMICIMPORT_PACKAGE));
if (dynamicImports != null) {
// Remove any dynamic imports from the referred set.
referred.keySet().removeAll(dynamicImports.keySet());
}
Set superfluous = new TreeSet();
// Tricky!
for (Iterator i = exportInstructions.keySet().iterator(); i
.hasNext();) {
String instr = (String) i.next();
if (!instr.startsWith("!"))
superfluous.add(instr);
}
exports = merge("export-package", exportInstructions, contained,
superfluous);
if (!superfluous.isEmpty()) {
warnings.add("Superfluous export-package instructions: "
+ superfluous);
}
// Add all exports that do not have an -noimport: directive
// to the imports.
Map referredAndExported = new HashMap(referred);
referredAndExported.putAll(addExportsToImports(exports));
// match the imports to the referred and exported packages,
// merge the info for matching packages
Set extra = new TreeSet(importInstructions.keySet());
imports = merge("import-package", importInstructions,
referredAndExported, extra);
// Instructions that have not been used could be superfluous
// or if they do not contain wildcards, should be added
// as extra imports, the user knows best.
for (Iterator i = extra.iterator(); i.hasNext();) {
String p = (String) i.next();
if (p.startsWith("!") || p.indexOf('*') >= 0
|| p.indexOf('?') >= 0 || p.indexOf('[') >= 0) {
warning("Did not find matching referal for " + p);
} else {
Map map = (Map) importInstructions.get(p);
imports.put(p, map);
}
}
// See what information we can find to augment the
// imports. I.e. look on the classpath
augmentImports();
// Add the uses clause to the exports
doUses(exports, uses, imports);
}
}
/**
* One of the main workhorses of this class. This will analyze the current
* setp and calculate a new manifest according to this setup. This method
* will also set the manifest on the main jar dot
*
* @return
* @throws IOException
*/
public Manifest calcManifest() throws IOException {
analyze();
Manifest manifest = new Manifest();
Attributes main = manifest.getMainAttributes();
main.putValue(BUNDLE_MANIFESTVERSION, "2");
if (!noExtraHeaders) {
main.putValue(CREATED_BY, System.getProperty("java.version") + " ("
+ System.getProperty("java.vendor") + ")");
main.putValue(TOOL, "Bnd-" + getVersion());
main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
}
String exportHeader = printClauses(exports,
"uses:|include:|exclude:|mandatory:");
if (exportHeader.length() > 0)
main.putValue(EXPORT_PACKAGE, exportHeader);
else
main.remove(EXPORT_PACKAGE);
Map temp = removeKeys(imports, "java.");
if (!temp.isEmpty()) {
main.putValue(IMPORT_PACKAGE, printClauses(temp, "resolution:"));
} else {
main.remove(IMPORT_PACKAGE);
}
temp = new TreeMap(contained);
temp.keySet().removeAll(exports.keySet());
if (!temp.isEmpty())
main.putValue(PRIVATE_PACKAGE, printClauses(temp, ""));
else
main.remove(PRIVATE_PACKAGE);
if (!ignored.isEmpty()) {
main.putValue(IGNORE_PACKAGE, printClauses(ignored, ""));
} else {
main.remove(IGNORE_PACKAGE);
}
if (bundleClasspath != null && !bundleClasspath.isEmpty())
main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath, ""));
else
main.remove(BUNDLE_CLASSPATH);
Map l = doServiceComponent(getProperty(SERVICE_COMPONENT));
if (!l.isEmpty())
main.putValue(SERVICE_COMPONENT, printClauses(l, ""));
else
main.remove(SERVICE_COMPONENT);
for (Enumeration h = getProperties().propertyNames(); h
.hasMoreElements();) {
String header = (String) h.nextElement();
if (header.trim().length() == 0) {
warning("Empty property set with value: "
+ getProperties().getProperty(header));
continue;
}
if (!Character.isUpperCase(header.charAt(0)))
continue;
if (header.equals(BUNDLE_CLASSPATH)
|| header.equals(EXPORT_PACKAGE)
|| header.equals(IMPORT_PACKAGE))
continue;
if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
String value = getProperty(header);
if (value != null && main.getValue(header) == null) {
if (value.trim().length() == 0)
main.remove(header);
else
main.putValue(header, value);
}
} else {
// TODO should we report?
}
}
main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
// Copy old values into new manifest, when they
// exist in the old one, but not in the new one
merge(manifest, dot.getManifest());
// Check for some defaults
String p = getProperty("p");
if (p != null) {
if (main.getValue(BUNDLE_NAME) == null) {
if (main.getValue(BUNDLE_SYMBOLICNAME) != null) {
main.putValue(BUNDLE_NAME, main
.getValue(BUNDLE_SYMBOLICNAME));
} else
main.putValue(BUNDLE_NAME, p);
}
if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
main.putValue(BUNDLE_SYMBOLICNAME, p);
}
}
if (main.getValue(BUNDLE_VERSION) == null)
main.putValue(BUNDLE_VERSION, "0");
// Remove all the headers mentioned in -removeheaders
Map removes = parseHeader(getProperty(REMOVE_HEADERS));
for (Iterator i = removes.keySet().iterator(); i.hasNext();) {
String header = (String) i.next();
for (Iterator j = main.keySet().iterator(); j.hasNext();) {
Attributes.Name attr = (Attributes.Name) j.next();
if (attr.toString().matches(header)) {
j.remove();
progress("Removing header: " + header);
}
}
}
dot.setManifest(manifest);
return manifest;
}
/**
* Calculate an export header solely based on the contents of a JAR file
*
* @param bundle
* The jar file to analyze
* @return
*/
public String calculateExportsFromContents(Jar bundle) {
String ddel = "";
StringBuffer sb = new StringBuffer();
Map map = bundle.getDirectories();
for (Iterator i = map.keySet().iterator(); i.hasNext();) {
String directory = (String) i.next();
if (directory.equals("META-INF")
|| directory.startsWith("META-INF/"))
continue;
if (directory.equals("OSGI-OPT")
|| directory.startsWith("OSGI-OPT/"))
continue;
if (directory.equals("/"))
continue;
if (directory.endsWith("/"))
directory = directory.substring(0, directory.length() - 1);
directory = directory.replace('/', '.');
sb.append(ddel);
sb.append(directory);
ddel = ",";
}
return sb.toString();
}
/**
* Check if a service component header is actually referring to a class. If
* so, replace the reference with an XML file reference. This makes it
* easier to create and use components.
*
* @throws UnsupportedEncodingException
*
*/
public Map doServiceComponent(String serviceComponent) throws IOException {
Map list = new LinkedHashMap();
Map sc = parseHeader(serviceComponent);
if (!sc.isEmpty()) {
for (Iterator i = sc.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
String name = (String) entry.getKey();
Map info = (Map) entry.getValue();
if (name == null) {
error("No name in Service-Component header: " + info);
continue;
}
if (dot.exists(name)) {
// Normal service component
list.put(name, info);
} else {
if (!checkClass(name))
error("Not found Service-Component header: " + name);
else {
// We have a definition, so make an XML resources
Resource resource = createComponentResource(name, info);
dot.putResource("OSGI-INF/" + name + ".xml", resource);
list.put("OSGI-INF/" + name + ".xml", new HashMap());
}
}
}
}
return list;
}
public Map getBundleClasspath() {
return bundleClasspath;
}
public Map getContained() {
return contained;
}
public Map getExports() {
return exports;
}
public Map getImports() {
return imports;
}
public Jar getJar() {
return dot;
}
public Properties getProperties() {
if (!fixedupProperties)
begin();
return properties;
}
public String getProperty(String headerName) {
String value = getProperties().getProperty(headerName);
if (value != null)
return replacer.process(value);
else
return null;
}
public Map getReferred() {
return referred;
}
/**
* Return the set of unreachable code depending on exports and the bundle
* activator.
*
* @return
*/
public Set getUnreachable() {
Set unreachable = new HashSet(uses.keySet()); // all
for (Iterator r = exports.keySet().iterator(); r.hasNext();) {
String packageName = (String) r.next();
removeTransitive(packageName, unreachable);
}
if (activator != null) {
String pack = activator.substring(0, activator.lastIndexOf('.'));
removeTransitive(pack, unreachable);
}
return unreachable;
}
public Map getUses() {
return uses;
}
/**
* Get the version from the manifest, a lot of work!
*
* @return version or unknown.
*/
public String getVersion() {
getBndManifest();
String version = null;
if (bndManifest != null)
version = (String) bndManifest.getMainAttributes().getValue(
BUNDLE_VERSION);
if (version != null)
return version;
return "unknown version";
}
public long getBndLastModified() {
getBndManifest();
if (bndManifest != null) {
String v = bndManifest.getMainAttributes().getValue(
BND_LASTMODIFIED);
if (v != null)
try {
return Long.parseLong(v);
} catch (Exception e) {
warning(BND_LASTMODIFIED
+ " header of bnd jar is not a long " + v);
}
}
if (isPedantic())
warning("Can not find manifest for bnd program, assuming it is not modified");
return 0;
}
public void getBndManifest() {
if (bndManifest != null)
return;
try {
Enumeration e = getClass().getClassLoader().getResources(
"META-INF/MANIFEST.MF");
while (e.hasMoreElements()) {
URL url = (URL) e.nextElement();
InputStream in = url.openStream();
Manifest manifest = new Manifest(in);
in.close();
if (manifest != null) {
String bsn = manifest.getMainAttributes().getValue(
BUNDLE_SYMBOLICNAME);
if (bsn != null && bsn.indexOf("biz.aQute.bnd") >= 0) {
bndManifest = manifest;
return;
}
}
}
} catch (IOException e) {
// Well, too bad
warning("bnd jar file is corrupted, can not find manifest " + e);
return;
}
// Looking again has no purpose, so use an
// empty manifest.
bndManifest = new Manifest();
}
/**
* Merge the existing manifest with the instructions.
*
* @param manifest
* The manifest to merge with
* @throws IOException
*/
public void mergeManifest(Manifest manifest) throws IOException {
if (manifest != null) {
Attributes attributes = manifest.getMainAttributes();
for (Iterator i = attributes.keySet().iterator(); i.hasNext();) {
Name name = (Name) i.next();
String key = name.toString();
// Dont want instructions
if (key.startsWith("-"))
continue;
if (getProperty(key) == null)
setProperty(key, (String) attributes.get(name));
}
}
}
// public Signer getSigner() {
// String sign = getProperty("-sign");
// if (sign == null) return null;
//
// Map parsed = parseHeader(sign);
// Signer signer = new Signer();
// String password = (String) parsed.get("password");
// if (password != null) {
// signer.setPassword(password);
// }
//
// String keystore = (String) parsed.get("keystore");
// if (keystore != null) {
// File f = new File(keystore);
// if (!f.isAbsolute()) f = new File(base, keystore);
// signer.setKeystore(f);
// } else {
// error("Signing requires a keystore");
// return null;
// }
//
// String alias = (String) parsed.get("alias");
// if (alias != null) {
// signer.setAlias(alias);
// } else {
// error("Signing requires an alias for the key");
// return null;
// }
// return signer;
// }
public void setBase(File file) {
base = file;
getProperties().put("project.dir", base.getAbsolutePath());
}
/**
* Set the classpath for this analyzer by file.
*
* @param classpath
* @throws IOException
*/
public void setClasspath(File[] classpath) throws IOException {
List list = new ArrayList();
for (int i = 0; i < classpath.length; i++) {
if (classpath[i].exists()) {
Jar current = new Jar(classpath[i]);
list.add(current);
} else {
errors.add("Missing file on classpath: " + classpath[i]);
}
}
for (Iterator i = list.iterator(); i.hasNext();) {
addClasspath((Jar) i.next());
}
}
public void setClasspath(Jar[] classpath) {
for (int i = 0; i < classpath.length; i++) {
addClasspath(classpath[i]);
}
}
public void setClasspath(String[] classpath) {
List list = new ArrayList();
for (int i = 0; i < classpath.length; i++) {
Jar jar = getJarFromName(classpath[i], " setting classpath");
if (jar != null)
list.add(jar);
}
this.classpath.addAll(list);
}
/**
* Set the JAR file we are going to work in. This will read the JAR in
* memory.
*
* @param jar
* @return
* @throws IOException
*/
public Jar setJar(File jar) throws IOException {
return setJar(new Jar(jar));
}
/**
* Set the JAR directly we are going to work on.
*
* @param jar
* @return
*/
public Jar setJar(Jar jar) {
this.dot = jar;
return jar;
}
/**
* Set the properties by file. Setting the properties this way will also set
* the base for this analyzer. After reading the properties, this will call
* setProperties(Properties) which will handle the includes.
*
* @param propertiesFile
* @throws FileNotFoundException
* @throws IOException
*/
public void setProperties(File propertiesFile)
throws FileNotFoundException, IOException {
propertiesFile = propertiesFile.getAbsoluteFile();
updateModified(propertiesFile.lastModified());
setBase(propertiesFile.getParentFile());
Properties local = loadProperties(propertiesFile);
setProperties(local);
setProperty("project.file", propertiesFile.getAbsolutePath());
if (getProperty("p") == null)
setProperty("p", propertiesFile.getParentFile().getName());
if (getProperty(BUNDLE_SYMBOLICNAME) == null) {
// Calculate a default symbolic name
// from the file name.
String name = propertiesFile.getName();
int n = name.lastIndexOf('.');
if (n > 0)
name = name.substring(0, n);
local.setProperty(BUNDLE_SYMBOLICNAME, name);
}
}
public void mergeProperties(File file, boolean override) {
if (file.isFile()) {
try {
Properties properties = loadProperties(file);
mergeProperties(properties, override);
} catch (Exception e) {
error("Error loading properties file: " + file);
}
} else {
if (!file.exists())
error("Properties file does not exist: " + file);
else
error("Properties file must a file, not a directory: " + file);
}
}
public void mergeProperties(Properties properties, boolean override) {
if (override)
this.properties = merge(this.properties, properties);
else
this.properties = merge(properties, this.properties);
fixedupProperties = false;
}
Properties merge(Properties a, Properties b) {
Properties result = new Properties();
result.putAll(a);
result.putAll(b);
return result;
}
public void setProperties(Properties properties) {
this.properties = properties;
doPropertyIncludes(getBaseURL(), properties, new HashSet());
fixedupProperties = false;
}
void begin() {
if (fixedupProperties)
return;
fixedupProperties = true;
replacer = new Macro(properties, this);
String doNotCopy = getProperty(DONOTCOPY);
if (doNotCopy != null)
Analyzer.doNotCopy = Pattern.compile(doNotCopy);
String cp = properties.getProperty(CLASSPATH);
if (cp != null)
doClasspath(cp);
verifyManifestHeadersCase(properties);
if ("true".equalsIgnoreCase(getProperty(PEDANTIC)))
setPedantic(true);
noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
}
private URL getBaseURL() {
try {
return base.toURL();
} catch (Exception e) {
// who cares, can not happen
}
return null;
}
/**
* Add or override a new property.
*
* @param key
* @param value
*/
public void setProperty(String key, String value) {
checkheader: for (int i = 0; i < headers.length; i++) {
if (headers[i].equalsIgnoreCase(value)) {
value = headers[i];
break checkheader;
}
}
getProperties().put(key, value);
}
/**
* Check if the given class or interface name is contained in the jar.
*
* @param interfaceName
* @return
*/
boolean checkClass(String interfaceName) {
String path = interfaceName.replace('.', '/') + ".class";
if (classspace.containsKey(path))
return true;
String pack = interfaceName;
int n = pack.lastIndexOf('.');
if (n > 0)
pack = pack.substring(0, n);
else
pack = ".";
return imports.containsKey(pack);
}
/**
* Create the resource for a DS component.
*
* @param list
* @param name
* @param info
* @throws UnsupportedEncodingException
*/
Resource createComponentResource(String name, Map info) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
pw.println("<?xml version='1.0' encoding='utf-8'?>");
pw.print("<component name='" + name + "'");
String factory = (String) info.get("factory:");
if (factory != null)
pw.print(" factory='" + factory + "'");
String immediate = (String) info.get("immediate:");
if (immediate != null)
pw.print(" immediate='" + immediate + "'");
String enabled = (String) info.get("enabled:");
if (enabled != null)
pw.print(" enabled='" + enabled + "'");
pw.println(">");
pw.println(" <implementation class='" + name + "'/>");
String provides = (String) info.get("provide:");
boolean servicefactory = Boolean.getBoolean(info.get("servicefactory:")
+ "");
provides(pw, provides, servicefactory);
properties(pw, info);
reference(info, pw);
pw.println("</component>");
pw.close();
byte[] data = out.toByteArray();
out.close();
return new EmbeddedResource(data, 0);
}
/**
* Parse the -classpath header. This is a comma separated list of urls or
* file names.
*
* @param cp
*/
void doClasspath(String cp) {
for (Iterator i = getClauses(cp).iterator(); i.hasNext();) {
Jar jar = getJarFromName((String) i.next(), "getting classpath");
if (jar != null)
addClasspath(jar);
}
}
void addClasspath(Jar jar) {
if (isPedantic() && jar.getResources().isEmpty())
warning("There is an empty jar or directory on the classpath: "
+ jar.getName());
classpath.add(jar);
}
/**
* Try to get a Jar from a file name/path or a url, or in last resort from
* the classpath name part of their files.
*
* @param name
* URL or filename relative to the base
* @param from
* Message identifying the caller for errors
* @return null or a Jar with the contents for the name
*/
Jar getJarFromName(String name, String from) {
File file = new File(name);
if (!file.isAbsolute())
file = new File(base, name);
if (file.exists())
try {
Jar jar = new Jar(file.getName(), file);
return jar;
} catch (Exception e) {
error("Exception in parsing jar file for " + from + ": " + name
+ " " + e);
}
// It is not a file ...
try {
// Lets try a URL
URL url = new URL(name);
Jar jar = new Jar(fileName(url.getPath()));
URLConnection connection = url.openConnection();
InputStream in = connection.getInputStream();
long lastModified = connection.getLastModified();
if (lastModified == 0)
// We assume the worst :-(
lastModified = System.currentTimeMillis();
EmbeddedResource.build(jar, in, lastModified);
in.close();
return jar;
} catch (IOException ee) {
// Check if we have files on the classpath
// that have the right name, allows us to specify those
// names instead of the full path.
for (Iterator cp = classpath.iterator(); cp.hasNext();) {
Jar entry = (Jar) cp.next();
if (entry.source != null && entry.source.getName().equals(name)) {
return entry;
}
}
// error("Can not find jar file for " + from + ": " + name);
}
return null;
}
private String fileName(String path) {
int n = path.lastIndexOf('/');
if (n > 0)
return path.substring(n + 1);
return path;
}
/**
* Read a manifest but return a properties object.
*
* @param in
* @return
* @throws IOException
*/
Properties getManifestAsProperties(InputStream in) throws IOException {
Properties p = new Properties();
Manifest manifest = new Manifest(in);
for (Iterator it = manifest.getMainAttributes().keySet().iterator(); it
.hasNext();) {
Attributes.Name key = (Attributes.Name) it.next();
String value = manifest.getMainAttributes().getValue(key);
p.put(key.toString(), value);
}
return p;
}
/**
* Helper routine to create a set of a comma separated string.
*
* @param list
* @return
*/
Set getClauses(String list) {
if (list == null)
return new HashSet();
list = list.trim();
String[] parts = list.split("\\s*,\\s*");
return new LinkedHashSet(Arrays.asList(parts));
}
/**
*
* @param manifest
* @throws Exception
*/
void merge(Manifest result, Manifest old) throws IOException {
if (old != null) {
for (Iterator e = old.getMainAttributes().entrySet().iterator(); e
.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
Attributes.Name name = (Attributes.Name) entry.getKey();
String value = (String) entry.getValue();
if (name.toString().equalsIgnoreCase("Created-By"))
name = new Attributes.Name("Originally-Created-By");
if (!result.getMainAttributes().containsKey(name))
result.getMainAttributes().put(name, value);
}
// do not overwrite existing entries
Map oldEntries = old.getEntries();
Map newEntries = result.getEntries();
for (Iterator e = oldEntries.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
if (!newEntries.containsKey(entry.getKey())) {
newEntries.put(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Print the Service-Component properties element
*
* @param pw
* @param info
*/
void properties(PrintWriter pw, Map info) {
Set properties = getClauses((String) info.get("properties:"));
for (Iterator p = properties.iterator(); p.hasNext();) {
String clause = (String) p.next();
int n = clause.indexOf('=');
if (n <= 0) {
error("Not a valid property in service component: " + clause);
} else {
String type = null;
String name = clause.substring(0, n);
if (name.indexOf('@') >= 0) {
String parts[] = name.split("@");
name = parts[1];
type = parts[0];
}
String value = clause.substring(n + 1);
// TODO verify validity of name and value.
pw.print("<property name='");
pw.print(name);
pw.print("'");
if (type != null) {
if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
pw.print(" type='");
pw.print(type);
pw.print("'");
} else {
warnings.add("Invalid property type '" + type
+ "' for property " + name);
}
}
if (value.indexOf("\\n") >= 0) {
pw.print("'>");
pw.print(value.replaceAll("\\n", "\n"));
pw.println("</property>");
} else {
pw.print(" value='");
pw.print(value);
pw.print("'/>");
}
}
}
}
/**
* @param pw
* @param provides
*/
void provides(PrintWriter pw, String provides, boolean servicefactory) {
if (provides != null) {
if (!servicefactory)
pw.println(" <service>");
else
pw.println(" <service servicefactory='true'>");
StringTokenizer st = new StringTokenizer(provides, ",");
while (st.hasMoreTokens()) {
String interfaceName = st.nextToken();
pw.println(" <provide interface='" + interfaceName + "'/>");
if (!checkClass(interfaceName))
error("Component definition provides a class that is neither imported nor contained: "
+ interfaceName);
}
pw.println(" </service>");
}
}
final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
/**
* @param info
* @param pw
*/
void reference(Map info, PrintWriter pw) {
Set dynamic = getClauses((String) info.get("dynamic:"));
Set optional = getClauses((String) info.get("optional:"));
Set multiple = getClauses((String) info.get("multiple:"));
for (Iterator r = info.entrySet().iterator(); r.hasNext();) {
Map.Entry ref = (Map.Entry) r.next();
String referenceName = (String) ref.getKey();
String target = null;
String interfaceName = (String) ref.getValue();
// TODO check if the interface is contained or imported
if (referenceName.endsWith(":")) {
if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
error("Unrecognized directive in Service-Component header: "
+ referenceName);
continue;
}
Matcher m = REFERENCE.matcher(interfaceName);
if (m.matches()) {
interfaceName = m.group(1);
target = m.group(2);
}
if (!checkClass(interfaceName))
error("Component definition refers to a class that is neither imported nor contained: "
+ interfaceName);
pw.print(" <reference name='" + referenceName + "' interface='"
+ interfaceName + "'");
String cardinality = optional.contains(referenceName) ? "0" : "1";
cardinality += "..";
cardinality += multiple.contains(referenceName) ? "n" : "1";
if (!cardinality.equals("1..1"))
pw.print(" cardinality='" + cardinality + "'");
if (Character.isLowerCase(referenceName.charAt(0))) {
String z = referenceName.substring(0, 1).toUpperCase()
+ referenceName.substring(1);
pw.print(" bind='set" + z + "'");
// TODO Verify that the methods exist
// TODO ProSyst requires both a bind and unbind :-(
// if ( dynamic.contains(referenceName) )
pw.print(" unbind='unset" + z + "'");
// TODO Verify that the methods exist
}
if (dynamic.contains(referenceName)) {
pw.print(" policy='dynamic'");
}
if (target != null) {
Filter filter = new Filter(target);
if (filter.verify() == null)
pw.print(" target='" + filter.toString() + "'");
else
error("Target for " + referenceName
+ " is not a correct filter: " + target + " "
+ filter.verify());
}
pw.println("/>");
}
}
String stem(String name) {
int n = name.lastIndexOf('.');
if (n > 0)
return name.substring(0, n);
else
return name;
}
/**
* Bnd is case sensitive for the instructions so we better check people are
* not using an invalid case. We do allow this to set headers that should
* not be processed by us but should be used by the framework.
*
* @param properties
* Properties to verify.
*/
void verifyManifestHeadersCase(Properties properties) {
for (Iterator i = properties.keySet().iterator(); i.hasNext();) {
String header = (String) i.next();
for (int j = 0; j < headers.length; j++) {
if (!headers[j].equals(header)
&& headers[j].equalsIgnoreCase(header)) {
warnings
.add("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
+ header + " and expecting: " + headers[j]);
break;
}
}
}
}
/**
* We will add all exports to the imports unless there is a -noimport
* directive specified on an export. This directive is skipped for the
* manifest.
*
*/
Map addExportsToImports(Map exports) {
Map importsFromExports = new HashMap();
for (Iterator export = exports.entrySet().iterator(); export.hasNext();) {
Map.Entry entry = (Map.Entry) export.next();
String packageName = (String) entry.getKey();
Map parameters = (Map) entry.getValue();
String noimport = (String) parameters.get(NO_IMPORT_DIRECTIVE);
if (noimport == null || !noimport.equalsIgnoreCase("true")) {
Map importParameters = (Map) importsFromExports
.get(packageName);
if (importParameters == null)
importsFromExports.put(packageName, parameters);
}
}
return importsFromExports;
}
/**
* Create the imports/exports by parsing
*
* @throws IOException
*/
void analyzeClasspath() throws IOException {
cpExports = new HashMap();
for (Iterator c = classpath.iterator(); c.hasNext();) {
Jar current = (Jar) c.next();
checkManifest(current);
for (Iterator j = current.getDirectories().keySet().iterator(); j
.hasNext();) {
String dir = (String) j.next();
Resource resource = current.getResource(dir + "/packageinfo");
if (resource != null) {
InputStream in = resource.openInputStream();
String version = parsePackageInfo(in);
in.close();
setPackageInfo(dir, "version", version);
}
}
}
}
/**
*
* @param jar
*/
void checkManifest(Jar jar) {
try {
Manifest m = jar.getManifest();
if (m != null) {
String exportHeader = m.getMainAttributes().getValue(
EXPORT_PACKAGE);
if (exportHeader != null) {
Map exported = (Map) parseHeader(exportHeader);
if (exported != null)
cpExports.putAll(exported);
}
}
} catch (Exception e) {
warning("Erroneous Manifest for " + jar + " " + e);
}
}
/**
* Find some more information about imports in manifest and other places.
*/
void augmentImports() {
for (Iterator imp = imports.keySet().iterator(); imp.hasNext();) {
String packageName = (String) imp.next();
Map currentAttributes = (Map) imports.get(packageName);
Map exporter = (Map) cpExports.get(packageName);
if (exporter != null) {
augmentVersion(currentAttributes, exporter);
augmentMandatory(currentAttributes, exporter);
}
String remove = (String) currentAttributes.get("remove-attribute:");
Instruction removeInstr = null;
if ( remove != null )
removeInstr = Instruction.getPattern(remove);
for ( Iterator i = currentAttributes.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry)i.next();
if ( entry.getValue().equals("!"))
i.remove();
if ( removeInstr !=null && removeInstr.matches((String)entry.getKey()))
i.remove();
}
}
}
/**
* If we use an import with mandatory attributes we better all use them
*
* @param currentAttributes
* @param exporter
*/
private void augmentMandatory(Map currentAttributes, Map exporter) {
String mandatory = (String) exporter.get("mandatory:");
if (mandatory != null) {
String[] attrs = mandatory.split("\\s*,\\s*");
for (int i = 0; i < attrs.length; i++) {
if ( !currentAttributes.containsKey(attrs[i]))
currentAttributes.put(attrs[i], exporter.get(attrs[i]));
}
}
}
/**
* Check if we can augment the version from the exporter.
*
* We allow the version in the import to specify a @ which is replaced with
* the exporter's version.
*
* @param currentAttributes
* @param exporter
*/
private void augmentVersion(Map currentAttributes, Map exporter) {
String currentVersion = (String) currentAttributes.get("version");
if (currentVersion == null || currentVersion.indexOf("${") >= 0) {
// See if we can borrow the version
String version = (String) exporter.get("version");
if (version == null)
version = (String) exporter.get("specification-version");
if (version != null) {
if (currentVersion != null) {
// we mist replace the ${@} with the version we
// found this can be useful if you want a range to start
// with the found version.
setProperty("@", version);
version = replacer.process(currentVersion);
unsetProperty("@");
} else {
// We remove the micro part of the version
// to a bit more lenient
Matcher m = versionPattern.matcher(version);
if (m.matches())
version = m.group(1);
}
currentAttributes.put("version", version);
}
}
}
public void unsetProperty(String string) {
getProperties().remove(string);
}
/**
* Inspect the properties and if you find -includes parse the line included
* manifest files or propertie files. The files are relative from the given
* base, this is normally the base for the analyzer.
*
* @param ubase
* @param p
* @param done
* @throws IOException
*/
void doPropertyIncludes(URL ubase, Properties p, Set done) {
String includes = p.getProperty(INCLUDE);
if (includes != null) {
includes = replacer.process(includes);
Set clauses = getClauses(includes);
outer: for (Iterator i = clauses.iterator(); i.hasNext();) {
String value = (String) i.next();
boolean fileMustExist = true;
if (value.startsWith("-")) {
fileMustExist = false;
value = value.substring(1).trim();
}
try {
URL next = null;
try {
next = new URL(ubase, value);
} catch (MalformedURLException e) {
File f = new File(value);
if (!f.isAbsolute())
f = new File(base, value);
if (f.exists()) {
next = f.getAbsoluteFile().toURL();
updateModified(f.lastModified());
} else {
if (fileMustExist)
error("Can not find include file: " + value);
continue outer;
}
}
String urlString = next.toExternalForm();
if (done.contains(urlString))
return;
done.add(urlString);
URLConnection connection = next.openConnection();
long time = connection.getLastModified();
if (time > 0)
updateModified(time);
InputStream in = connection.getInputStream();
Properties sub;
if (next.getFile().toLowerCase().endsWith(".mf")) {
sub = getManifestAsProperties(in);
} else
sub = loadProperties(in, next.toExternalForm());
doPropertyIncludes(next, sub, done);
p.putAll(sub);
in.close();
} catch (FileNotFoundException e) {
if (fileMustExist)
error("Can not find included file: " + value);
} catch (IOException e) {
if (fileMustExist)
error("Error in processing included file: " + value
+ "(" + e + ")");
}
}
}
}
/**
* Add the uses clauses
*
* @param exports
* @param uses
* @throws MojoExecutionException
*/
void doUses(Map exports, Map uses, Map imports) {
if ("true".equalsIgnoreCase(getProperty(NOUSES)))
return;
for (Iterator i = exports.keySet().iterator(); i.hasNext();) {
String packageName = (String) i.next();
Map clause = (Map) exports.get(packageName);
String override = (String) clause.get("uses:");
if ( override == null )
override = "<<USES>>";
Set usedPackages = (Set) uses.get(packageName);
if (usedPackages != null) {
// Only do a uses on exported or imported packages
// and uses should also not contain our own package
// name
Set sharedPackages = new HashSet();
sharedPackages.addAll(imports.keySet());
sharedPackages.addAll(exports.keySet());
usedPackages.retainAll(sharedPackages);
usedPackages.remove(packageName);
StringBuffer sb = new StringBuffer();
String del = "";
for (Iterator u = usedPackages.iterator(); u.hasNext();) {
String usedPackage = (String) u.next();
if (!usedPackage.startsWith("java.")) {
sb.append(del);
sb.append(usedPackage);
del = ",";
}
}
override = override.replaceAll("<<USES>>", sb.toString()).trim();
if ( override.endsWith(","))
override = override.substring(0,override.length()-1);
if ( override.startsWith(","))
override = override.substring(1);
if (override.length() > 0) {
clause.put("uses:", override);
}
}
}
}
/**
* Get a property with a proper default
*
* @param headerName
* @param deflt
* @return
*/
public String getProperty(String headerName, String deflt) {
String v = getProperty(headerName);
return v == null ? deflt : v;
}
/**
* Helper to load a properties file from disk.
*
* @param file
* @return
* @throws IOException
*/
Properties loadProperties(File file) throws IOException {
InputStream in = new FileInputStream(file);
Properties p = loadProperties(in, file.getAbsolutePath());
in.close();
return p;
}
Properties loadProperties(InputStream in, String name) throws IOException {
try {
Properties p = new Properties();
p.load(in);
return p;
} catch (Exception e) {
error("Error during loading properties file: " + name + ", error:"
+ e);
return new Properties();
}
}
/**
* Merge the attributes of two maps, where the first map can contain
* wildcarded names. The idea is that the first map contains patterns (for
* example *) with a set of attributes. These patterns are matched against
* the found packages in actual. If they match, the result is set with the
* merged set of attributes. It is expected that the instructions are
* ordered so that the instructor can define which pattern matches first.
* Attributes in the instructions override any attributes from the actual.<br/>
*
* A pattern is a modified regexp so it looks like globbing. The * becomes a .*
* just like the ? becomes a .?. '.' are replaced with \\. Additionally, if
* the pattern starts with an exclamation mark, it will remove that matches
* for that pattern (- the !) from the working set. So the following
* patterns should work:
* <ul>
* <li>com.foo.bar</li>
* <li>com.foo.*</li>
* <li>com.foo.???</li>
* <li>com.*.[^b][^a][^r]</li>
* <li>!com.foo.* (throws away any match for com.foo.*)</li>
* </ul>
* Enough rope to hang the average developer I would say.
*
*
* @param instructions
* the instructions with patterns. A
* @param actual
* the actual found packages
*/
Map merge(String type, Map instructions, Map actual, Set superfluous) {
actual = new HashMap(actual); // we do not want to ruin our original
Map result = new HashMap();
for (Iterator i = instructions.keySet().iterator(); i.hasNext();) {
String instruction = (String) i.next();
String originalInstruction = instruction;
Map instructedAttributes = (Map) instructions.get(instruction);
if (instruction.startsWith("=")) {
result.put(instruction.substring(1), instructedAttributes);
superfluous.remove(originalInstruction);
continue;
}
Instruction instr = Instruction.getPattern(instruction);
for (Iterator p = actual.keySet().iterator(); p.hasNext();) {
String packageName = (String) p.next();
if (instr.matches(packageName)) {
superfluous.remove(originalInstruction);
if (!instr.isNegated()) {
Map newAttributes = new HashMap();
newAttributes.putAll((Map) actual.get(packageName));
newAttributes.putAll(instructedAttributes);
result.put(packageName, newAttributes);
} else {
ignored.put(packageName, new HashMap());
}
p.remove(); // Can never match again for another pattern
}
}
}
return result;
}
/**
* Print a standard Map based OSGi header.
*
* @param exports
* map { name => Map { attribute|directive => value } }
* @return the clauses
*/
public String printClauses(Map exports, String allowedDirectives) {
StringBuffer sb = new StringBuffer();
String del = "";
for (Iterator i = exports.keySet().iterator(); i.hasNext();) {
String name = (String) i.next();
Map map = (Map) exports.get(name);
sb.append(del);
sb.append(name);
for (Iterator j = map.keySet().iterator(); j.hasNext();) {
String key = (String) j.next();
// Skip directives we do not recognize
if (!key.startsWith("x-") && key.endsWith(":")
&& allowedDirectives.indexOf(key) < 0)
continue;
String value = ((String) map.get(key)).trim();
sb.append(";");
sb.append(key);
sb.append("=");
boolean clean = (value.length() >= 2 && value.charAt(0) == '"' && value
.charAt(value.length() - 1) == '"')
|| Verifier.TOKEN.matcher(value).matches();
if (!clean)
sb.append("\"");
sb.append(value);
if (!clean)
sb.append("\"");
}
del = ",";
}
return sb.toString();
}
/**
* Transitively remove all elemens from unreachable through the uses link.
*
* @param name
* @param unreachable
*/
void removeTransitive(String name, Set unreachable) {
if (!unreachable.contains(name))
return;
unreachable.remove(name);
Set ref = (Set) uses.get(name);
if (ref != null) {
for (Iterator r = ref.iterator(); r.hasNext();) {
String element = (String) r.next();
removeTransitive(element, unreachable);
}
}
}
/**
* Helper method to set the package info
*
* @param dir
* @param key
* @param value
*/
void setPackageInfo(String dir, String key, String value) {
if (value != null) {
String pack = dir.replace('/', '.');
Map map = (Map) cpExports.get(pack);
if (map == null) {
map = new HashMap();
cpExports.put(pack, map);
}
map.put(key, value);
}
}
public void close() {
dot.close();
if (classpath != null)
for (Iterator j = classpath.iterator(); j.hasNext();) {
Jar jar = (Jar) j.next();
jar.close();
}
}
/**
* Findpath looks through the contents of the JAR and finds paths that end
* with the given regular expression
*
* ${findpath (; reg-expr (; replacement)? )? }
*
* @param args
* @return
*/
public String _findpath(String args[]) {
return findPath("findpath", args, true);
}
public String _findname(String args[]) {
return findPath("findname", args, false);
}
String findPath(String name, String[] args, boolean fullPathName) {
if (args.length > 3) {
warning("Invalid nr of arguments to " + name + " "
+ Arrays.asList(args) + ", syntax: ${" + name
+ " (; reg-expr (; replacement)? )? }");
return null;
}
String regexp = ".*";
String replace = null;
switch (args.length) {
case 3:
replace = args[2];
case 2:
regexp = args[1];
}
StringBuffer sb = new StringBuffer();
String del = "";
Pattern expr = Pattern.compile(regexp);
for (Iterator e = dot.getResources().keySet().iterator(); e.hasNext();) {
String path = (String) e.next();
if (!fullPathName) {
int n = path.lastIndexOf('/');
if (n >= 0) {
path = path.substring(n + 1);
}
}
Matcher m = expr.matcher(path);
if (m.matches()) {
if (replace != null)
path = m.replaceAll(replace);
sb.append(del);
sb.append(path);
del = ", ";
}
}
return sb.toString();
}
public void updateModified(long time) {
if (time > lastModified)
lastModified = time;
}
public long lastModified() {
updateModified(getBndLastModified());
return lastModified;
}
public void putAll(Map additional, boolean force) {
for (Iterator i = additional.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
if (force || getProperties().get(entry.getKey()) == null)
setProperty((String) entry.getKey(), (String) entry.getValue());
}
}
public List getClasspath() {
return classpath;
}
}