blob: 459b4986133ce755c6ae38549c83d7dd9dc6aaad [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.net.*;
import java.util.*;
import java.util.jar.*;
import aQute.bnd.make.*;
import aQute.bnd.service.*;
import aQute.libg.header.*;
import aQute.libg.reporter.*;
public class Processor implements Reporter, Constants, Closeable {
// TODO handle include files out of date
public static String DEFAULT_PLUGINS = ""; // "aQute.lib.spring.SpringComponent";
// TODO make splitter skip eagerly whitespace so trim is not necessary
public static String LIST_SPLITTER = "\\\\?\\s*,\\s*";
private List<String> errors = new ArrayList<String>();
private List<String> warnings = new ArrayList<String>();
boolean pedantic;
boolean trace;
boolean exceptions;
boolean fileMustExist = true;
List<Object> plugins;
private File base = new File("").getAbsoluteFile();
private List<Closeable> toBeClosed = newList();
final Properties properties;
private Macro replacer;
private long lastModified;
private File propertiesFile;
private boolean fixup = true;
long modified;
Processor parent;
Set<File> included;
CL pluginLoader;
Collection<String> filter;
public Processor() {
properties = new Properties();
}
public Processor(Properties parent) {
properties = new Properties(parent);
}
public Processor(Processor parent) {
this(parent.properties);
this.parent = parent;
}
public void setParent(Processor processor) {
this.parent = processor;
}
public Processor getParent() {
return parent;
}
public Processor getTop() {
if (parent == null)
return this;
else
return parent.getTop();
}
public void getInfo(Processor processor, String prefix) {
if (isFailOk())
addAll(warnings, processor.getErrors(), prefix);
else
addAll(errors, processor.getErrors(), prefix);
addAll(warnings, processor.getWarnings(), prefix);
processor.errors.clear();
processor.warnings.clear();
}
public void getInfo(Processor processor) {
getInfo(processor, "");
}
private <T> void addAll(List<String> to, List<? extends T> from,
String prefix) {
for (T x : from) {
to.add(prefix + x);
}
}
public void warning(String string, Object... args) {
String s = String.format(string, args);
if (!warnings.contains(s))
warnings.add(s);
}
public void error(String string, Object... args) {
if (isFailOk())
warning(string, args);
else {
String s = String.format(string, args);
if (!errors.contains(s))
errors.add(s);
}
}
public void error(String string, Throwable t, Object... args) {
if (isFailOk())
warning(string + ": " + t, args);
else {
String s = String.format(string, args);
if (!errors.contains(s))
errors.add(s);
}
if (exceptions)
t.printStackTrace();
}
public List<String> getWarnings() {
return warnings;
}
public List<String> getErrors() {
return errors;
}
public Map<String, Map<String, String>> parseHeader(String value) {
return parseHeader(value, this);
}
/**
* Standard OSGi header parser.
*
* @param value
* @return
*/
@SuppressWarnings("unchecked")
static public Map<String, Map<String, String>> parseHeader(String value,
Processor logger) {
return OSGiHeader.parseHeader(value, logger);
}
Map<String, Map<String, String>> getClauses(String header) {
return parseHeader(getProperty(header));
}
public void addClose(Closeable jar) {
toBeClosed.add(jar);
}
/**
* Remove all entries from a map that start with a specific prefix
*
* @param <T>
* @param source
* @param prefix
* @return
*/
static <T> Map<String, T> removeKeys(Map<String, T> source, String prefix) {
Map<String, T> temp = new TreeMap<String, T>(source);
for (Iterator<String> p = temp.keySet().iterator(); p.hasNext();) {
String pack = (String) p.next();
if (pack.startsWith(prefix))
p.remove();
}
return temp;
}
public void progress(String s, Object... args) {
// System.out.println(s);
}
public boolean isPedantic() {
return pedantic;
}
public void setPedantic(boolean pedantic) { // System.out.println("Set
// pedantic: " + pedantic + " "
// + this );
this.pedantic = pedantic;
}
public static File getFile(File base, String file) {
File f = new File(file);
if (f.isAbsolute())
return f;
int n;
f = base.getAbsoluteFile();
while ((n = file.indexOf('/')) > 0) {
String first = file.substring(0, n);
file = file.substring(n + 1);
if (first.equals(".."))
f = f.getParentFile();
else
f = new File(f, first);
}
return new File(f, file).getAbsoluteFile();
}
public File getFile(String file) {
return getFile(base, file);
}
/**
* Return a list of plugins that implement the given class.
*
* @param clazz
* Each returned plugin implements this class/interface
* @return A list of plugins
*/
public <T> List<T> getPlugins(Class<T> clazz) {
List<T> l = new ArrayList<T>();
List<Object> all = getPlugins();
for (Object plugin : all) {
if (clazz.isInstance(plugin))
l.add(clazz.cast(plugin));
}
return l;
}
/**
* Return a list of plugins. Plugins are defined with the -plugin command.
* They are class names, optionally associated with attributes. Plugins can
* implement the Plugin interface to see these attributes.
*
* Any object can be a plugin.
*
* @return
*/
protected List<Object> getPlugins() {
// if (parent != null) {
// List<Object> val = parent.getPlugins();
// getInfo(parent);
// return val;
// }
if (this.plugins != null)
return this.plugins;
String spe = getProperty(Analyzer.PLUGIN, DEFAULT_PLUGINS);
Map<String, Map<String, String>> plugins = parseHeader(spe);
List<Object> list = new ArrayList<Object>();
// Add the default plugins. Only if non is specified
// will they be removed.
list.add(new MakeBnd());
list.add(new MakeCopy());
list.add(new ServiceComponent());
for (Map.Entry<String, Map<String, String>> entry : plugins.entrySet()) {
String key = (String) entry.getKey();
if (key.equals(NONE))
return this.plugins = newList();
try {
CL loader = getLoader();
String path = entry.getValue().get(PATH_DIRECTIVE);
if (path != null) {
File f = getFile(path).getAbsoluteFile();
loader.add(f.toURL());
}
trace("Using plugin %s", key);
// Plugins could use the same class with different
// parameters so we could have duplicate names Remove
// the ! added by the parser to make each name unique.
key = removeDuplicateMarker(key);
Class<?> c = (Class<?>) loader.loadClass(key);
Object plugin = c.newInstance();
if (plugin instanceof Plugin) {
((Plugin) plugin).setProperties(entry.getValue());
((Plugin) plugin).setReporter(this);
}
list.add(plugin);
} catch (Exception e) {
error("Problem loading the plugin: " + key + " exception: " + e);
}
}
return this.plugins = list;
}
public boolean isFailOk() {
String v = getProperty(Analyzer.FAIL_OK, null);
return v != null && v.equalsIgnoreCase("true");
}
public File getBase() {
return base;
}
public void setBase(File base) {
this.base = base;
}
public void clear() {
errors.clear();
warnings.clear();
}
public void trace(String msg, Object... parms) {
if (trace) {
System.out.printf("# " + msg + "\n", parms);
}
}
public <T> List<T> newList() {
return new ArrayList<T>();
}
public <T> Set<T> newSet() {
return new TreeSet<T>();
}
public static <K, V> Map<K, V> newMap() {
return new LinkedHashMap<K, V>();
}
public static <K, V> Map<K, V> newHashMap() {
return new HashMap<K, V>();
}
public <T> List<T> newList(Collection<T> t) {
return new ArrayList<T>(t);
}
public <T> Set<T> newSet(Collection<T> t) {
return new TreeSet<T>(t);
}
public <K, V> Map<K, V> newMap(Map<K, V> t) {
return new LinkedHashMap<K, V>(t);
}
public void close() {
for (Closeable c : toBeClosed) {
try {
c.close();
} catch (IOException e) {
// Who cares?
}
}
toBeClosed = null;
}
public String _basedir(String args[]) {
if (base == null)
throw new IllegalArgumentException("No base dir set");
return base.getAbsolutePath();
}
/**
* Property handling ...
*
* @return
*/
public Properties getProperties() {
if (fixup) {
fixup = false;
begin();
}
return properties;
}
public String getProperty(String key) {
return getProperty(key, null);
}
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) {
for (Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
String key = (String) e.nextElement();
String value = properties.getProperty(key);
if (override || !getProperties().containsKey(key))
setProperty(key, value);
}
}
public void setProperties(Properties properties) {
doIncludes(getBase(), properties, new HashSet<String>());
this.properties.putAll(properties);
}
public void addProperties(File file) throws Exception {
addIncluded(file);
Properties p = loadProperties(file);
setProperties(p);
}
public synchronized void addIncluded(File file) {
if (included == null)
included = new HashSet<File>();
included.add(file);
}
/**
* Inspect the properties and if you find -includes parse the line included
* manifest files or properties 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
*/
private void doIncludes(File ubase, Properties p, Set<String> done) {
String includes = p.getProperty(INCLUDE);
if (includes != null) {
includes = getReplacer().process(includes);
p.remove(INCLUDE);
Collection<String> clauses = parseHeader(includes).keySet();
for (String value : clauses) {
boolean fileMustExist = true;
boolean overwrite = true;
while (true) {
if (value.startsWith("-")) {
fileMustExist = false;
value = value.substring(1).trim();
} else if (value.startsWith("~")) {
// Overwrite properties!
overwrite = false;
value = value.substring(1).trim();
} else
break;
}
try {
File file = getFile(ubase, value).getAbsoluteFile();
if (file.isFile()) {
if (included != null && included.contains(file)) {
error("Cyclic include of " + file);
} else {
addIncluded(file);
updateModified(file.lastModified(), "Include "
+ value);
InputStream in = new FileInputStream(file);
Properties sub;
if (file.getName().toLowerCase().endsWith(".mf")) {
sub = getManifestAsProperties(in);
} else
sub = loadProperties(in, file.getAbsolutePath());
in.close();
doIncludes(file.getParentFile(), sub, done);
// make sure we do not override properties
if (!overwrite)
sub.keySet().removeAll(p.keySet());
p.putAll(sub);
}
} else {
if (fileMustExist)
error("Included file "
+ file
+ (file.exists() ? " does not exist"
: " is directory"));
}
} catch (IOException e) {
if (fileMustExist)
error("Error in processing included file: " + value, e);
}
}
}
}
public void unsetProperty(String string) {
getProperties().remove(string);
}
public boolean refresh() {
if (propertiesFile == null)
return false;
boolean changed = false;
if (included != null) {
for (File file : included) {
if (file.lastModified() > modified) {
changed = true;
break;
}
}
}
// System.out.println("Modified " + modified + " file: "
// + propertiesFile.lastModified() + " diff "
// + (modified - propertiesFile.lastModified()));
// Date last = new Date(propertiesFile.lastModified());
// Date current = new Date(modified);
changed |= modified < propertiesFile.lastModified();
if (changed) {
included = null;
properties.clear();
plugins = null;
setProperties(propertiesFile, base);
propertiesChanged();
return true;
}
return false;
}
public void propertiesChanged() {
}
/**
* 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 IOException {
propertiesFile = propertiesFile.getAbsoluteFile();
setProperties(propertiesFile, propertiesFile.getParentFile());
}
public void setProperties(File propertiesFile, File base) {
this.propertiesFile = propertiesFile.getAbsoluteFile();
setBase(base);
try {
if (propertiesFile.isFile()) {
// System.out.println("Loading properties " + propertiesFile);
long modified = propertiesFile.lastModified();
if (modified > System.currentTimeMillis() + 100) {
System.out.println("Huh? This is in the future "
+ propertiesFile);
this.modified = System.currentTimeMillis();
} else
this.modified = modified;
included = null;
Properties p = loadProperties(propertiesFile);
setProperties(p);
} else {
if (fileMustExist) {
error("No such properties file: " + propertiesFile);
}
}
} catch (IOException e) {
error("Could not load properties " + propertiesFile);
}
}
protected void begin() {
if (isTrue(getProperty(PEDANTIC)))
setPedantic(true);
}
public static boolean isTrue(String value) {
return "true".equalsIgnoreCase(value);
}
/**
* Get a property with a proper default
*
* @param headerName
* @param deflt
* @return
*/
public String getProperty(String key, String deflt) {
String value;
if (filter != null && filter.contains(key)) {
value = (String) getProperties().get(key);
} else
value = getProperties().getProperty(key);
if (value != null)
return getReplacer().process(value);
else if (deflt != null)
return getReplacer().process(deflt);
else
return null;
}
/**
* Helper to load a properties file from disk.
*
* @param file
* @return
* @throws IOException
*/
public Properties loadProperties(File file) throws IOException {
updateModified(file.lastModified(), "Properties file: " + file);
InputStream in = new FileInputStream(file);
Properties p = loadProperties(in, file.getAbsolutePath());
in.close();
return p;
}
Properties loadProperties(InputStream in, String name) throws IOException {
int n = name.lastIndexOf('/');
if (n > 0)
name = name.substring(0, n);
if (name.length() == 0)
name = ".";
try {
Properties p = new Properties();
p.load(in);
return replaceAll(p, "\\$\\{\\.\\}", name);
} catch (Exception e) {
error("Error during loading properties file: " + name + ", error:"
+ e);
return new Properties();
}
}
/**
* Replace a string in all the values of the map. This can be used to
* preassign variables that change. I.e. the base directory ${.} for a
* loaded properties
*/
public static Properties replaceAll(Properties p, String pattern,
String replacement) {
Properties result = new Properties();
for (Iterator<Map.Entry<Object, Object>> i = p.entrySet().iterator(); i
.hasNext();) {
Map.Entry<Object, Object> entry = i.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
value = value.replaceAll(pattern, replacement);
result.put(key, value);
}
return result;
}
/**
* 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
*/
public static Map<String, Map<String, String>> merge(String type,
Map<String, Map<String, String>> instructions,
Map<String, Map<String, String>> actual, Set<String> superfluous,
Map<String, Map<String, String>> ignored) {
Map<String, Map<String, String>> toVisit = new HashMap<String, Map<String, String>>(
actual); // we do not want to ruin our
// original
Map<String, Map<String, String>> result = newMap();
for (Iterator<String> i = instructions.keySet().iterator(); i.hasNext();) {
String instruction = i.next();
String originalInstruction = instruction;
Map<String, String> instructedAttributes = instructions
.get(instruction);
// Check if we have a fixed (starts with '=') or a
// duplicate name. A fixed name is added to the output without
// checking against the contents. Duplicates are marked
// at the end. In that case we do not pick up any contained
// information but just add them to the output including the
// marker.
if (instruction.startsWith("=")) {
result.put(instruction.substring(1), instructedAttributes);
superfluous.remove(originalInstruction);
continue;
}
if (isDuplicate(instruction)) {
result.put(instruction, instructedAttributes);
superfluous.remove(originalInstruction);
continue;
}
Instruction instr = Instruction.getPattern(instruction);
for (Iterator<String> p = toVisit.keySet().iterator(); p.hasNext();) {
String packageName = p.next();
if (instr.matches(packageName)) {
superfluous.remove(originalInstruction);
if (!instr.isNegated()) {
Map<String, String> newAttributes = new HashMap<String, String>();
newAttributes.putAll(actual.get(packageName));
newAttributes.putAll(instructedAttributes);
result.put(packageName, newAttributes);
} else if (ignored != null) {
ignored.put(packageName, new HashMap<String, String>());
}
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 static String printClauses(Map<String, Map<String, String>> exports,
String allowedDirectives) {
return printClauses(exports, allowedDirectives, false);
}
public static String printClauses(Map<String, Map<String, String>> exports,
String allowedDirectives, boolean checkMultipleVersions) {
StringBuffer sb = new StringBuffer();
String del = "";
for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
String name = i.next();
Map<String, String> clause = exports.get(name);
// We allow names to be duplicated in the input
// by ending them with '~'. This is necessary to use
// the package names as keys. However, we remove these
// suffixes in the output so that you can set multiple
// exports with different attributes.
String outname = removeDuplicateMarker(name);
sb.append(del);
sb.append(outname);
printClause(clause, allowedDirectives, sb);
del = ",";
}
return sb.toString();
}
public static void printClause(Map<String, String> map,
String allowedDirectives, StringBuffer sb) {
for (Iterator<String> j = map.keySet().iterator(); j.hasNext();) {
String key = j.next();
// Skip directives we do not recognize
if (!key.startsWith("x-")
&& key.endsWith(":")
&& (allowedDirectives == null || 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("\"");
}
}
public Macro getReplacer() {
if (replacer == null)
return replacer = new Macro(getProperties(), this,
getMacroDomains());
else
return replacer;
}
/**
* This should be overridden by subclasses to add extra macro command
* domains on the search list.
*
* @return
*/
protected Object[] getMacroDomains() {
return new Object[] {};
}
/**
* Return the properties but expand all macros. This always returns a new
* Properties object that can be used in any way.
*
* @return
*/
public Properties getFlattenedProperties() {
return getReplacer().getFlattenedProperties();
}
public void updateModified(long time, String reason) {
if (time > lastModified) {
lastModified = time;
}
}
public long lastModified() {
return lastModified;
}
/**
* 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);
}
/**
* Read a manifest but return a properties object.
*
* @param in
* @return
* @throws IOException
*/
public static Properties getManifestAsProperties(InputStream in)
throws IOException {
Properties p = new Properties();
Manifest manifest = new Manifest(in);
for (Iterator<Object> 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;
}
public File getPropertiesFile() {
return propertiesFile;
}
public void setFileMustExist(boolean mustexist) {
fileMustExist = mustexist;
}
static public String read(InputStream in) throws Exception {
InputStreamReader ir = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
try {
char chars[] = new char[1000];
int size = ir.read(chars);
while (size > 0) {
sb.append(chars, 0, size);
size = ir.read(chars);
}
} finally {
ir.close();
}
return sb.toString();
}
/**
* Join a list.
*
* @param args
* @return
*/
public static String join(Collection<?> list, String delimeter) {
if (list == null)
return "";
StringBuilder sb = new StringBuilder();
String del = "";
for (Object item : list) {
sb.append(del);
sb.append(item);
del = delimeter;
}
return sb.toString();
}
public static String join(Object[] list, String delimeter) {
if (list == null)
return "";
StringBuilder sb = new StringBuilder();
String del = "";
for (Object item : list) {
sb.append(del);
sb.append(item);
del = delimeter;
}
return sb.toString();
}
public static String join(Collection<?> list) {
return join(list, ",");
}
public static void split(String s, Collection<String> set) {
String elements[] = s.trim().split(LIST_SPLITTER);
for (String element : elements) {
if (element.length() > 0)
set.add(element);
}
}
public static Collection<String> split(String s) {
return split(s, LIST_SPLITTER);
}
public static Collection<String> split(String s, String splitter) {
if (s != null)
s = s.trim();
if (s == null || s.trim().length() == 0)
return Collections.emptyList();
return Arrays.asList(s.split(splitter));
}
public boolean isExceptions() {
return exceptions;
}
public void setExceptions(boolean exceptions) {
this.exceptions = exceptions;
}
/**
* Make the file short if it is inside our base directory, otherwise long.
*
* @param f
* @return
*/
public String normalize(String f) {
if (f.startsWith(base.getAbsolutePath() + "/"))
return f.substring(base.getAbsolutePath().length() + 1);
else
return f;
}
public String normalize(File f) {
return normalize(f.getAbsolutePath());
}
public static String removeDuplicateMarker(String key) {
int i = key.length() - 1;
while (i >= 0 && key.charAt(i) == DUPLICATE_MARKER)
--i;
return key.substring(0, i + 1);
}
public static boolean isDuplicate(String name) {
return name.length() > 0
&& name.charAt(name.length() - 1) == DUPLICATE_MARKER;
}
public void setTrace(boolean x) {
trace = x;
}
static class CL extends URLClassLoader {
CL() {
super(new URL[0], Processor.class.getClassLoader());
}
void add(URL url) {
URL urls[] = getURLs();
for (URL u : urls) {
if (u.equals(url))
return;
}
super.addURL(url);
}
}
private CL getLoader() {
if (pluginLoader == null)
pluginLoader = new CL();
return pluginLoader;
}
public boolean exists() {
return base != null && base.exists();
}
public boolean isOk() {
return isFailOk() || (getErrors().size() == 0);
}
public boolean isPerfect() {
return getErrors().size() == 0 && getWarnings().size() == 0;
}
public void setForceLocal(Collection<String> local) {
filter = local;
}
}