Modified: felix/trunk/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java Thu Sep 6 18:28:06 2012 @@ -3,29 +3,204 @@ package aQute.lib.deployer; import java.io.*; import java.security.*; import java.util.*; -import java.util.jar.*; import java.util.regex.*; -import aQute.bnd.header.*; import aQute.bnd.osgi.*; +import aQute.bnd.osgi.Verifier; import aQute.bnd.service.*; import aQute.bnd.version.*; +import aQute.lib.collections.*; +import aQute.lib.hex.*; import aQute.lib.io.*; +import aQute.libg.command.*; +import aQute.libg.cryptography.*; +import aQute.libg.reporter.*; import aQute.service.reporter.*; -public class FileRepo implements Plugin, RepositoryPlugin, Refreshable, RegistryPlugin { - public final static String LOCATION = "location"; - public final static String READONLY = "readonly"; - public final static String NAME = "name"; +/** + * A FileRepo is the primary and example implementation of a repository based on + * a file system. It maintains its files in a bsn/bsn-version.jar style from a + * given location. It implements all the functions of the + * {@link RepositoryPlugin}, {@link Refreshable}, {@link Actionable}, and + * {@link Closeable}. The FileRepo can be extended or used as is. When used as + * is, it is possible to add shell commands to the life cycle of the FileRepo. + * This life cycle is as follows: + * <ul> + * <li>{@link #CMD_INIT} - Is only executed when the location did not exist</li> + * <li>{@link #CMD_OPEN} - Called (after init if necessary) to open it once</li> + * <li>{@link #CMD_REFRESH} - Called when refreshed.</li> + * <li>{@link #CMD_BEFORE_PUT} - Before the file system is changed</li> + * <li>{@link #CMD_AFTER_PUT} - After the file system has changed, and the put + * <li>{@link #CMD_BEFORE_GET} - Before the file is gotten</li> + * <li>{@link #CMD_AFTER_ACTION} - Before the file is gotten</li> + * <li>{@link #CMD_CLOSE} - When the repo is closed and no more actions will + * take place</li> was a success</li> + * <li>{@link #CMD_ABORT_PUT} - When the put is aborted.</li> + * <li>{@link #CMD_CLOSE} - To close the repository.</li> + * </ul> + * Additionally, it is possible to set the {@link #CMD_SHELL} and the + * {@link #CMD_PATH}. Notice that you can use the ${global} macro to read global + * (that is, machine local) settings from the ~/.bnd/settings.json file (can be + * managed with bnd). + */ +public class FileRepo implements Plugin, RepositoryPlugin, Refreshable, RegistryPlugin, Actionable, Closeable { - File[] EMPTY_FILES = new File[0]; + /** + * If set, will trace to stdout. Works only if no reporter is set. + */ + public final static String TRACE = "trace"; + + /** + * Property name for the location of the repo, must be a valid path name + * using forward slashes (see {@link IO#getFile(String)}. + */ + public final static String LOCATION = "location"; + + /** + * Property name for the readonly state of the repository. If no, will + * read/write, otherwise it must be a boolean value read by + * {@link Boolean#parseBoolean(String)}. Read only repositories will not + * accept writes. + */ + public final static String READONLY = "readonly"; + + /** + * Set the name of this repository (optional) + */ + public final static String NAME = "name"; + + /** + * Path property for commands. A comma separated path for directories to be + * searched for command. May contain $ @} which will be replaced by the + * system path. If this property is not set, the system path is assumed. + */ + public static final String CMD_PATH = "cmd.path"; + + /** + * The name ( and path) of the shell to execute the commands. By default + * this is sh and searched in the path. + */ + public static final String CMD_SHELL = "cmd.shell"; + + /** + * Property for commands. The command only runs when the location does not + * exist. </p> + * + * @param rootFile + * the root of the repo (directory exists) + */ + public static final String CMD_INIT = "cmd.init"; + + /** + * Property for commands. Command is run before the repo is first used. </p> + * + * @param $0 + * rootFile the root of the repo (directory exists) + */ + public static final String CMD_OPEN = "cmd.open"; + + /** + * Property for commands. The command runs after a put operation. </p> + * + * @param $0 + * the root of the repo (directory exists) + * @param $1 + * the file that was put + * @param $2 + * the hex checksum of the file + */ + public static final String CMD_AFTER_PUT = "cmd.after.put"; + + /** + * Property for commands. The command runs when the repository is refreshed. + * </p> + * + * @param $ + * {0} the root of the repo (directory exists) + */ + public static final String CMD_REFRESH = "cmd.refresh"; + + /** + * Property for commands. The command runs after the file is put. </p> + * + * @param $0 + * the root of the repo (directory exists) + * @param $1 + * the path to a temporary file + */ + public static final String CMD_BEFORE_PUT = "cmd.before.put"; + + /** + * Property for commands. The command runs when a put is aborted after file + * changes were made. </p> + * + * @param $0 + * the root of the repo (directory exists) + * @param $1 + * the temporary file that was used (optional) + */ + public static final String CMD_ABORT_PUT = "cmd.abort.put"; + + /** + * Property for commands. The command runs after the file is put. </p> + * + * @param $0 + * the root of the repo (directory exists) + */ + public static final String CMD_CLOSE = "cmd.close"; + + /** + * Property for commands. Will be run after an action has been executed. + * </p> + * + * @param $0 + * the root of the repo (directory exists) + * @param $1 + * the path to the file that the action was executed on + * @param $2 + * the action executed + */ + public static final String CMD_AFTER_ACTION = "cmd.after.action"; + + /** + * Called before a before get. + * + * @param $0 + * the root of the repo (directory exists) + * @param $1 + * the bsn + * @param $2 + * the version + */ + public static final String CMD_BEFORE_GET = "cmd.before.get"; + + /** + * Options used when the options are null + */ + static final PutOptions DEFAULTOPTIONS = new PutOptions(); + + String shell; + String path; + String init; + String open; + String refresh; + String beforePut; + String afterPut; + String abortPut; + String beforeGet; + String close; + String action; + + File[] EMPTY_FILES = new File[0]; protected File root; Registry registry; - boolean canWrite = true; - Pattern REPO_FILE = Pattern.compile("([-a-zA-z0-9_\\.]+)-([0-9\\.]+|latest)\\.(jar|lib)"); + boolean canWrite = true; + Pattern REPO_FILE = Pattern.compile("([-a-zA-z0-9_\\.]+)-([0-9\\.]+)\\.(jar|lib)"); Reporter reporter; boolean dirty; String name; + boolean inited; + boolean trace; public FileRepo() {} @@ -35,280 +210,200 @@ public class FileRepo implements Plugin, this.canWrite = canWrite; } - protected void init() throws Exception { - // for extensions + /** + * Initialize the repository Subclasses should first call this method and + * then if it returns true, do their own initialization + * + * @return true if initialized, false if already had been initialized. + * @throws Exception + */ + protected boolean init() throws Exception { + if (inited) + return false; + + inited = true; + + if (reporter == null) { + ReporterAdapter reporter = trace ? new ReporterAdapter(System.out) : new ReporterAdapter(); + reporter.setTrace(trace); + reporter.setExceptions(trace); + this.reporter = reporter; + } + + if (!root.isDirectory()) { + root.mkdirs(); + if (!root.isDirectory()) + throw new IllegalArgumentException("Location cannot be turned into a directory " + root); + + exec(init, root.getAbsolutePath()); + } + open(); + return true; } + /** + * @see aQute.bnd.service.Plugin#setProperties(java.util.Map) + */ public void setProperties(Map<String,String> map) { String location = map.get(LOCATION); if (location == null) throw new IllegalArgumentException("Location must be set on a FileRepo plugin"); - root = new File(location); - + root = IO.getFile(IO.home, location); String readonly = map.get(READONLY); if (readonly != null && Boolean.valueOf(readonly).booleanValue()) canWrite = false; name = map.get(NAME); + path = map.get(CMD_PATH); + shell = map.get(CMD_SHELL); + init = map.get(CMD_INIT); + open = map.get(CMD_OPEN); + refresh = map.get(CMD_REFRESH); + beforePut = map.get(CMD_BEFORE_PUT); + abortPut = map.get(CMD_ABORT_PUT); + afterPut = map.get(CMD_AFTER_PUT); + beforeGet = map.get(CMD_BEFORE_GET); + close = map.get(CMD_CLOSE); + action = map.get(CMD_AFTER_ACTION); + + trace = map.get(TRACE) != null && Boolean.parseBoolean(map.get(TRACE)); } /** - * Get a list of URLs to bundles that are constrained by the bsn and - * versionRange. + * Answer if this repository can write. */ - private File[] get(String bsn, String versionRange) throws Exception { - init(); - - // If the version is set to project, we assume it is not - // for us. A project repo will then get it. - if (versionRange != null && versionRange.equals("project")) - return null; - - // - // Check if the entry exists - // - File f = new File(root, bsn); - if (!f.isDirectory()) - return null; - - // - // The version range we are looking for can - // be null (for all) or a version range. - // - VersionRange range; - if (versionRange == null || versionRange.equals("latest")) { - range = new VersionRange("0"); - } else - range = new VersionRange(versionRange); - - // - // Iterator over all the versions for this BSN. - // Create a sorted map over the version as key - // and the file as URL as value. Only versions - // that match the desired range are included in - // this list. - // - File instances[] = f.listFiles(); - SortedMap<Version,File> versions = new TreeMap<Version,File>(); - for (int i = 0; i < instances.length; i++) { - Matcher m = REPO_FILE.matcher(instances[i].getName()); - if (m.matches() && m.group(1).equals(bsn)) { - String versionString = m.group(2); - Version version; - if (versionString.equals("latest")) - version = new Version(Integer.MAX_VALUE); - else - version = new Version(versionString); - - if (range.includes(version) || versionString.equals(versionRange)) - versions.put(version, instances[i]); - } - } - - File[] files = versions.values().toArray(EMPTY_FILES); - if ("latest".equals(versionRange) && files.length > 0) { - return new File[] { - files[files.length - 1] - }; - } - return files; - } - public boolean canWrite() { return canWrite; } - protected PutResult putArtifact(File tmpFile, PutOptions options) throws Exception { + /** + * Local helper method that tries to insert a file in the repository. This + * method can be overridden but MUST not change the content of the tmpFile. + * This method should also create a latest version of the artifact for + * reference by tools like ant etc. </p> It is allowed to rename the file, + * the tmp file must be beneath the root directory to prevent rename + * problems. + * + * @param tmpFile + * source file + * @param digest + * @return a File that contains the content of the tmpFile + * @throws Exception + */ + protected File putArtifact(File tmpFile, byte[] digest) throws Exception { assert (tmpFile != null); - assert (options != null); - Jar jar = null; + Jar jar = new Jar(tmpFile); try { - init(); dirty = true; - jar = new Jar(tmpFile); - - Manifest manifest = jar.getManifest(); - if (manifest == null) - throw new IllegalArgumentException("No manifest in JAR: " + jar); - - String bsn = manifest.getMainAttributes().getValue(Analyzer.BUNDLE_SYMBOLICNAME); + String bsn = jar.getBsn(); if (bsn == null) - throw new IllegalArgumentException("No Bundle SymbolicName set"); - - Parameters b = Processor.parseHeader(bsn, null); - if (b.size() != 1) - throw new IllegalArgumentException("Multiple bsn's specified " + b); - - for (String key : b.keySet()) { - bsn = key; - if (!Verifier.SYMBOLICNAME.matcher(bsn).matches()) - throw new IllegalArgumentException("Bundle SymbolicName has wrong format: " + bsn); - } + throw new IllegalArgumentException("No bsn set in jar: " + tmpFile); - String versionString = manifest.getMainAttributes().getValue(Analyzer.BUNDLE_VERSION); - Version version; + String versionString = jar.getVersion(); if (versionString == null) - version = new Version(); - else - version = new Version(versionString); + versionString = "0"; + else if (!Verifier.isVersion(versionString)) + throw new IllegalArgumentException("Incorrect version in : " + tmpFile + " " + versionString); - if (reporter != null) - reporter.trace("bsn=%s version=%s", bsn, version); + Version version = new Version(versionString); + + reporter.trace("bsn=%s version=%s", bsn, version); File dir = new File(root, bsn); - if (!dir.exists() && !dir.mkdirs()) { + dir.mkdirs(); + if (!dir.isDirectory()) throw new IOException("Could not create directory " + dir); - } + String fName = bsn + "-" + version.getWithoutQualifier() + ".jar"; File file = new File(dir, fName); - boolean renamed = false; - PutResult result = new PutResult(); + reporter.trace("updating %s ", file.getAbsolutePath()); - if (reporter != null) - reporter.trace("updating %s ", file.getAbsolutePath()); - if (!file.exists() || file.lastModified() < jar.lastModified()) { - if (file.exists()) { - IO.delete(file); - } - IO.rename(tmpFile, file); - renamed = true; - result.artifact = file.toURI(); + IO.rename(tmpFile, file); - if (reporter != null) - reporter.progress(-1, "updated " + file.getAbsolutePath()); - - fireBundleAdded(jar, file); - } else { - if (reporter != null) { - reporter.progress(-1, "Did not update " + jar + " because repo has a newer version"); - reporter.trace("NOT Updating " + fName + " (repo is newer)"); - } - } + fireBundleAdded(jar, file); + afterPut(file, bsn, version, Hex.toHexString(digest)); + // TODO like to beforeGet rid of the latest option. This is only + // used to have a constant name for the outside users (like ant) + // we should be able to handle this differently? File latest = new File(dir, bsn + "-latest.jar"); - boolean latestExists = latest.exists() && latest.isFile(); - boolean latestIsOlder = latestExists && (latest.lastModified() < jar.lastModified()); - if ((options.createLatest && !latestExists) || latestIsOlder) { - if (latestExists) { - IO.delete(latest); - } - if (!renamed) { - IO.rename(tmpFile, latest); - } else { - IO.copy(file, latest); - } - result.latest = latest.toURI(); - } + IO.copy(file, latest); - return result; + reporter.trace("updated %s", file.getAbsolutePath()); + + return file; } finally { - if (jar != null) { - jar.close(); - } + jar.close(); } } - /* a straight copy of this method lives in LocalIndexedRepo */ + /* + * (non-Javadoc) + * @see aQute.bnd.service.RepositoryPlugin#put(java.io.InputStream, + * aQute.bnd.service.RepositoryPlugin.PutOptions) + */ public PutResult put(InputStream stream, PutOptions options) throws Exception { - /* both parameters are required */ - if ((stream == null) || (options == null)) { - throw new IllegalArgumentException("No stream and/or options specified"); - } - /* determine if the put is allowed */ if (!canWrite) { throw new IOException("Repository is read-only"); } - /* the root directory of the repository has to be a directory */ - if (!root.isDirectory()) { - throw new IOException("Repository directory " + root + " is not a directory"); - } + assert stream != null; - /* determine if the artifact needs to be verified */ - boolean verifyFetch = (options.digest != null); - boolean verifyPut = !options.allowArtifactChange; + if (options == null) + options = DEFAULTOPTIONS; - /* determine which digests are needed */ - boolean needFetchDigest = verifyFetch || verifyPut; - boolean needPutDigest = verifyPut || options.generateDigest; + init(); /* - * setup a new stream that encapsulates the stream and calculates (when - * needed) the digest + * copy the artifact from the (new/digest) stream into a temporary file + * in the root directory of the repository */ - DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1")); - dis.on(needFetchDigest); - - File tmpFile = null; + File tmpFile = IO.createTempFile(root, "put", ".jar"); try { - /* - * copy the artifact from the (new/digest) stream into a temporary - * file in the root directory of the repository - */ - tmpFile = IO.createTempFile(root, "put", ".bnd"); - IO.copy(dis, tmpFile); - - /* get the digest if available */ - byte[] disDigest = needFetchDigest ? dis.getMessageDigest().digest() : null; - - /* verify the digest when requested */ - if (verifyFetch && !MessageDigest.isEqual(options.digest, disDigest)) { - throw new IOException("Retrieved artifact digest doesn't match specified digest"); - } + DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1")); + try { + IO.copy(dis, tmpFile); - /* put the artifact into the repository (from the temporary file) */ - PutResult r = putArtifact(tmpFile, options); + byte[] digest = dis.getMessageDigest().digest(); - /* calculate the digest when requested */ - if (needPutDigest && (r.artifact != null)) { - MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - IO.copy(new File(r.artifact), sha1); - r.digest = sha1.digest(); - } + if (options.digest != null && !Arrays.equals(digest, options.digest)) + throw new IOException("Retrieved artifact digest doesn't match specified digest"); - /* verify the artifact when requested */ - if (verifyPut && (r.digest != null) && !MessageDigest.isEqual(disDigest, r.digest)) { - File f = new File(r.artifact); - if (f.exists()) { - IO.delete(f); - } - throw new IOException("Stored artifact digest doesn't match specified digest"); - } + /* + * put the artifact into the repository (from the temporary + * file) + */ + beforePut(tmpFile); + File file = putArtifact(tmpFile, digest); + file.setReadOnly(); - return r; - } - finally { - if (tmpFile != null && tmpFile.exists()) { - IO.delete(tmpFile); - } - } - } + PutResult result = new PutResult(); + result.digest = digest; + result.artifact = file.toURI(); - protected void fireBundleAdded(Jar jar, File file) { - if (registry == null) - return; - List<RepositoryListenerPlugin> listeners = registry.getPlugins(RepositoryListenerPlugin.class); - for (RepositoryListenerPlugin listener : listeners) { - try { - listener.bundleAdded(this, jar, file); + return result; } - catch (Exception e) { - if (reporter != null) - reporter.warning("Repository listener threw an unexpected exception: %s", e); + finally { + dis.close(); } } + catch (Exception e) { + abortPut(tmpFile); + throw e; + } + finally { + IO.delete(tmpFile); + } } public void setLocation(String string) { - root = new File(string); - if (!root.isDirectory()) - throw new IllegalArgumentException("Invalid repository directory"); + root = IO.getFile(string); } public void setReporter(Reporter reporter) { @@ -344,7 +439,7 @@ public class FileRepo implements Plugin, return result; } - public List<Version> versions(String bsn) throws Exception { + public SortedSet<Version> versions(String bsn) throws Exception { init(); File dir = new File(root, bsn); if (dir.isDirectory()) { @@ -359,9 +454,9 @@ public class FileRepo implements Plugin, list.add(new Version(version)); } } - return list; + return new SortedList<Version>(list); } - return null; + return SortedList.empty(); } @Override @@ -373,7 +468,9 @@ public class FileRepo implements Plugin, return root; } - public boolean refresh() { + public boolean refresh() throws Exception { + init(); + exec(refresh, root); if (dirty) { dirty = false; return true; @@ -388,60 +485,290 @@ public class FileRepo implements Plugin, return name; } - public Jar get(String bsn, Version v) throws Exception { + /* + * (non-Javadoc) + * @see aQute.bnd.service.RepositoryPlugin#get(java.lang.String, + * aQute.bnd.version.Version, java.util.Map) + */ + public File get(String bsn, Version version, Map<String,String> properties, DownloadListener... listeners) + throws Exception { init(); - File bsns = new File(root, bsn); - File version = new File(bsns, bsn + "-" + v.getMajor() + "." + v.getMinor() + "." + v.getMicro() + ".jar"); - if (version.exists()) - return new Jar(version); + beforeGet(bsn, version); + File file = getLocal(bsn, version, properties); + if (file.exists()) { + for (DownloadListener l : listeners) { + try { + l.success(file); + } + catch (Exception e) { + reporter.exception(e, "Download listener for %s", file); + } + } + return file; + } return null; } - public File get(String bsn, String version, Strategy strategy, Map<String,String> properties) throws Exception { - if (version == null) - version = "0.0.0"; + public void setRegistry(Registry registry) { + this.registry = registry; + } - if (strategy == Strategy.EXACT) { - VersionRange vr = new VersionRange(version); - if (vr.isRange()) - return null; + public String getLocation() { + return root.toString(); + } - if (vr.getHigh().getMajor() == Integer.MAX_VALUE) - version = "latest"; + public Map<String,Runnable> actions(Object... target) throws Exception { + if (target == null || target.length == 0) + return null; // no default actions - File file = IO.getFile(root, bsn + "/" + bsn + "-" + version + ".jar"); - if (file.isFile()) - return file; - file = IO.getFile(root, bsn + "/" + bsn + "-" + version + ".lib"); - if (file.isFile()) - return file; - return null; + try { + String bsn = (String) target[0]; + Version version = (Version) target[1]; + final File f = get(bsn, version, null); + if (f == null) + return null; + + Map<String,Runnable> actions = new HashMap<String,Runnable>(); + actions.put("Delete " + bsn + "-" + status(bsn, version), new Runnable() { + public void run() { + IO.delete(f); + if (f.getParentFile().list().length == 0) + IO.delete(f.getParentFile()); + afterAction(f, "delete"); + }; + }); + return actions; } - File[] files = get(bsn, version); - if (files == null || files.length == 0) + catch (Exception e) { return null; + } + } + + protected void afterAction(File f, String key) { + exec(action, root, f, key); + } + + /* + * (non-Javadoc) + * @see aQute.bnd.service.Actionable#tooltip(java.lang.Object[]) + */ + @SuppressWarnings("unchecked") + public String tooltip(Object... target) throws Exception { + if (target == null || target.length == 0) + return String.format("%s\n%s", getName(), root); - if (files.length >= 0) { - switch (strategy) { - case LOWEST : - return files[0]; - case HIGHEST : - return files[files.length - 1]; - case EXACT : - // TODO - break; + try { + String bsn = (String) target[0]; + Version version = (Version) target[1]; + Map<String,String> map = null; + if (target.length > 2) + map = (Map<String,String>) target[2]; + + File f = getLocal(bsn, version, map); + String s = String.format("Path: %s\nSize: %s\nSHA1: %s", f.getAbsolutePath(), readable(f.length(), 0), SHA1 + .digest(f).asHex()); + if (f.getName().endsWith(".lib") && f.isFile()) { + s += "\n" + IO.collect(f); } + return s; + + } + catch (Exception e) { + return null; + } + } + + /* + * (non-Javadoc) + * @see aQute.bnd.service.Actionable#title(java.lang.Object[]) + */ + public String title(Object... target) throws Exception { + if (target == null || target.length == 0) + return getName(); + + if (target.length == 1 && target[0] instanceof String) + return (String) target[0]; + + if (target.length == 2 && target[0] instanceof String && target[1] instanceof Version) { + return status((String) target[0], (Version) target[1]); } + return null; } - public void setRegistry(Registry registry) { - this.registry = registry; + protected File getLocal(String bsn, Version version, Map<String,String> properties) { + File dir = new File(root, bsn); + + File fjar = new File(dir, bsn + "-" + version.getWithoutQualifier() + ".jar"); + if (fjar.isFile()) + return fjar.getAbsoluteFile(); + + File flib = new File(dir, bsn + "-" + version.getWithoutQualifier() + ".lib"); + if (flib.isFile()) + return flib.getAbsoluteFile(); + + return fjar.getAbsoluteFile(); } - public String getLocation() { - return root.toString(); + protected String status(String bsn, Version version) { + File file = getLocal(bsn, version, null); + StringBuilder sb = new StringBuilder(version.toString()); + String del = " ["; + + if (file.getName().endsWith(".lib")) { + sb.append(del).append("L"); + del = ""; + } + if (!file.getName().endsWith(".jar")) { + sb.append(del).append("?"); + del = ""; + } + if (!file.isFile()) { + sb.append(del).append("X"); + del = ""; + } + if (file.length() == 0) { + sb.append(del).append("0"); + del = ""; + } + if (del.equals("")) + sb.append("]"); + return sb.toString(); + } + + private static String[] names = { + "bytes", "Kb", "Mb", "Gb" + }; + + private Object readable(long length, int n) { + if (length < 0) + return "<invalid>"; + + if (length < 1024 || n >= names.length) + return length + names[n]; + + return readable(length / 1024, n + 1); + } + + public void close() throws IOException { + if (inited) + exec(close, root.getAbsolutePath()); + } + + protected void open() { + exec(open, root.getAbsolutePath()); + } + + protected void beforePut(File tmp) { + exec(beforePut, root.getAbsolutePath(), tmp.getAbsolutePath()); + } + + protected void afterPut(File file, String bsn, Version version, String sha) { + exec(afterPut, root.getAbsolutePath(), file.getAbsolutePath(), sha); + } + + protected void abortPut(File tmpFile) { + exec(abortPut, root.getAbsolutePath(), tmpFile.getAbsolutePath()); + } + + protected void beforeGet(String bsn, Version version) { + exec(beforeGet, root.getAbsolutePath(), bsn, version); + } + + protected void fireBundleAdded(Jar jar, File file) { + if (registry == null) + return; + List<RepositoryListenerPlugin> listeners = registry.getPlugins(RepositoryListenerPlugin.class); + for (RepositoryListenerPlugin listener : listeners) { + try { + listener.bundleAdded(this, jar, file); + } + catch (Exception e) { + if (reporter != null) + reporter.warning("Repository listener threw an unexpected exception: %s", e); + } + } + } + + /** + * Execute a command. Used in different stages so that the repository can be + * synced with external tools. + * + * @param line + * @param target + */ + void exec(String line, Object... args) { + if (line == null) + return; + + try { + if (args != null) + for (int i = 0; i < args.length; i++) { + if (i == 0) + line = line.replaceAll("\\$\\{@\\}", args[0].toString()); + line = line.replaceAll("\\$" + i, args[i].toString()); + } + + if (shell == null) { + shell = System.getProperty("os.name").toLowerCase().indexOf("win") > 0 ? "cmd.exe" : "sh"; + } + Command cmd = new Command(shell); + + if (path != null) { + cmd.inherit(); + String oldpath = cmd.var("PATH"); + path = path.replaceAll("\\s*,\\s*", File.pathSeparator); + path = path.replaceAll("\\$\\{@\\}", oldpath); + cmd.var("PATH", path); + } + + cmd.setCwd(getRoot()); + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + int result = cmd.execute(line, stdout, stderr); + if (result != 0) { + reporter.error("Command %s failed with %s %s %s", line, result, stdout, stderr); + } + } + catch (Exception e) { + e.printStackTrace(); + reporter.exception(e, e.getMessage()); + } + } + + /* + * 8 Set the root directory directly + */ + public void setDir(File repoDir) { + this.root = repoDir; + } + + /** + * Delete an entry from the repository and cleanup the directory + * + * @param bsn + * @param version + * @throws Exception + */ + public void delete(String bsn, Version version) throws Exception { + assert bsn != null; + + SortedSet<Version> versions; + if (version == null) + versions = versions(bsn); + else + versions = new SortedList<Version>(version); + + for (Version v : versions) { + File f = getLocal(bsn, version, null); + if (!f.isFile()) + reporter.error("No artifact found for %s:%s", bsn, version); + else + IO.delete(f); + } + if ( versions(bsn).isEmpty()) + IO.delete( new File(root,bsn)); } }
Modified: felix/trunk/bundleplugin/src/main/java/aQute/lib/io/IO.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/io/IO.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/io/IO.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/io/IO.java Thu Sep 6 18:28:06 2012 @@ -7,6 +7,8 @@ import java.security.*; import java.util.*; public class IO { + static public File work = new File(System.getProperty("user.dir")); + static public File home = new File(System.getProperty("user.home")); public static void copy(Reader r, Writer w) throws IOException { try { @@ -226,7 +228,7 @@ public class IO { /** * Create a temporary file. - * + * * @param directory * the directory in which to create the file. Can be null, in * which case the system TMP directory is used @@ -257,16 +259,32 @@ public class IO { } public static File getFile(String filename) { - return new File(filename.replace("/", File.separator)); + return getFile(work, filename); } - + public static File getFile(File base, String file) { + + if (file.startsWith("~/")) { + file = file.substring(2); + if (!file.startsWith("~/")) { + return getFile(home, file); + } + } + if (file.startsWith("~")) { + file = file.substring(1); + return getFile(home.getParentFile(), file); + } + File f = new File(file); if (f.isAbsolute()) return f; int n; + if (base == null) + base = work; + f = base.getAbsoluteFile(); + while ((n = file.indexOf('/')) > 0) { String first = file.substring(0, n); file = file.substring(n + 1); @@ -280,28 +298,35 @@ public class IO { return new File(f, file).getAbsoluteFile(); } - /** Deletes the specified file. - * Folders are recursively deleted.<br> + /** + * Deletes the specified file. Folders are recursively deleted.<br> * If file(s) cannot be deleted, no feedback is provided (fail silently). - * @param f file to be deleted + * + * @param f + * file to be deleted */ public static void delete(File f) { try { deleteWithException(f); - } catch (IOException e) { + } + catch (IOException e) { // Ignore a failed delete } } - - /** Deletes the specified file. - * Folders are recursively deleted.<br> + + /** + * Deletes the specified file. Folders are recursively deleted.<br> * Throws exception if any of the files could not be deleted. - * @param f file to be deleted - * @throws IOException if the file (or contents of a folder) could not be deleted + * + * @param f + * file to be deleted + * @throws IOException + * if the file (or contents of a folder) could not be deleted */ public static void deleteWithException(File f) throws IOException { f = f.getAbsoluteFile(); - if (!f.exists()) return; + if (!f.exists()) + return; if (f.getParentFile() == null) throw new IllegalArgumentException("Cannot recursively delete root for safety reasons"); @@ -311,7 +336,8 @@ public class IO { for (File sub : subs) { try { deleteWithException(sub); - } catch (IOException e) { + } + catch (IOException e) { wasDeleted = false; } } @@ -323,19 +349,25 @@ public class IO { } } - /** Deletes <code>to</code> file if it exists, and renames <code>from</code> file to <code>to</code>.<br> - * Throws exception the rename operation fails. - * @param from source file - * @param to destination file - * @throws IOException if the rename operation fails - */ - public static void rename(File from, File to) throws IOException { - IO.deleteWithException(to); - - boolean renamed = from.renameTo(to); - if (!renamed) throw new IOException("Could not rename " + from.getAbsoluteFile() + " to " + to.getAbsoluteFile()); - } + /** + * Deletes <code>to</code> file if it exists, and renames <code>from</code> + * file to <code>to</code>.<br> + * Throws exception the rename operation fails. + * + * @param from + * source file + * @param to + * destination file + * @throws IOException + * if the rename operation fails + */ + public static void rename(File from, File to) throws IOException { + IO.deleteWithException(to); + boolean renamed = from.renameTo(to); + if (!renamed) + throw new IOException("Could not rename " + from.getAbsoluteFile() + " to " + to.getAbsoluteFile()); + } public static long drain(InputStream in) throws IOException { long result = 0; Modified: felix/trunk/bundleplugin/src/main/java/aQute/lib/json/ByteArrayHandler.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/json/ByteArrayHandler.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/json/ByteArrayHandler.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/json/ByteArrayHandler.java Thu Sep 6 18:28:06 2012 @@ -4,17 +4,17 @@ import java.io.*; import java.lang.reflect.*; import java.util.*; -import aQute.lib.base64.*; import aQute.lib.hex.*; +/** + * + * Will now use hex for encoding byte arrays + * + */ public class ByteArrayHandler extends Handler { - @Override void encode(Encoder app, Object object, Map<Object,Type> visited) throws IOException, Exception { - if ( app.codec.isHex()) - StringHandler.string(app, Hex.toHexString((byte[]) object)); - else - StringHandler.string(app, Base64.encodeBase64((byte[]) object)); + StringHandler.string(app, Hex.toHexString((byte[]) object)); } @Override @@ -31,8 +31,6 @@ public class ByteArrayHandler extends Ha @Override Object decode(Decoder dec, String s) throws Exception { - if ( dec.codec.isHex()) - return Hex.toByteArray(s); - return Base64.decodeBase64(s); + return Hex.toByteArray(s); } } Modified: felix/trunk/bundleplugin/src/main/java/aQute/lib/json/JSONCodec.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/json/JSONCodec.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/json/JSONCodec.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/json/JSONCodec.java Thu Sep 6 18:28:06 2012 @@ -34,6 +34,8 @@ import java.util.regex.*; * <p/> * This Codec class can be used in a concurrent environment. The Decoders and * Encoders, however, must only be used in a single thread. + * <p/> + * Will now use hex for encoding byte arrays */ public class JSONCodec { final static String START_CHARACTERS = "[{\"-0123456789tfn"; @@ -51,7 +53,6 @@ public class JSONCodec { private static ByteArrayHandler byteh = new ByteArrayHandler(); boolean ignorenull; - boolean useHex; /** * Create a new Encoder with the state and appropriate API. @@ -488,19 +489,4 @@ public class JSONCodec { return ignorenull; } - /** - * Use hex instead of default base 64 encoding - * - * @param useHex - * @return - */ - public JSONCodec setHex(boolean useHex) { - this.useHex = useHex; - return this; - } - - public boolean isHex() { - return useHex; - } - } \ No newline at end of file Modified: felix/trunk/bundleplugin/src/main/java/aQute/lib/json/packageinfo URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/json/packageinfo?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/json/packageinfo (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/json/packageinfo Thu Sep 6 18:28:06 2012 @@ -1 +1 @@ -version 2.4.0 +version 3.0.0 Added: felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java?rev=1381708&view=auto ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java (added) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java Thu Sep 6 18:28:06 2012 @@ -0,0 +1,251 @@ +package aQute.lib.settings; + +import java.io.*; +import java.security.*; +import java.security.spec.*; +import java.util.*; + +import aQute.lib.io.*; +import aQute.lib.json.*; + +/** + * Maintains persistent settings for bnd (or other apps). The default is + * ~/.bnd/settings.json). The settings are normal string properties but it + * specially maintains a public/private key pair and it provides a method to + * sign a byte array with this pair. + * <p/> + * Why not keystore and preferences? Well, keystore is hard to use (you can only + * store a private key when you have a certificate, but you cannot create a + * certificate without using com.sun classes) and preferences are not editable. + */ +public class Settings implements Map<String,String> { + static JSONCodec codec = new JSONCodec(); + + private File where; + private PublicKey publicKey; + private PrivateKey privateKey; + private boolean loaded; + private boolean dirty; + + public static class Data { + public int version = 1; + public byte[] secret; + public byte[] id; + public Map<String,String> map = new HashMap<String,String>(); + } + + Data data = new Data(); + + public Settings() { + this("~/.bnd/settings.json"); + } + + public Settings(String where) { + assert where != null; + this.where = IO.getFile(IO.work, where); + } + + public boolean load() { + if (this.where.isFile() && this.where.length() > 1) { + try { + data = codec.dec().from(this.where).get(Data.class); + loaded = true; + return true; + } + catch (Exception e) { + throw new RuntimeException("Cannot read settings file " + this.where, e); + } + } + + if (!data.map.containsKey("name")) + data.map.put("name", System.getProperty("user.name")); + return false; + } + + private void check() { + if (loaded) + return; + load(); + loaded = true; + } + + public void save() { + if (!this.where.getParentFile().isDirectory() && !this.where.getParentFile().mkdirs()) + throw new RuntimeException("Cannot create directory in " + this.where.getParent()); + + try { + codec.enc().to(this.where).put(data).flush(); + assert this.where.isFile(); + } + catch (Exception e) { + throw new RuntimeException("Cannot write settings file " + this.where, e); + } + } + + public void generate() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + keyGen.initialize(1024, random); + KeyPair pair = keyGen.generateKeyPair(); + privateKey = pair.getPrivate(); + publicKey = pair.getPublic(); + data.secret = privateKey.getEncoded(); + data.id = publicKey.getEncoded(); + save(); + } + + public String getEmail() { + return get("email"); + } + + public void setEmail(String email) { + put("email", email); + } + + public void setName(String v) { + put("name", v); + } + + public String getName() { + String name = get("name"); + if (name != null) + return name; + return System.getProperty("user.name"); + } + + /** + * Return an encoded public RSA key. this key can be decoded with an + * X509EncodedKeySpec + * + * @return an encoded public key. + * @throws Exception + */ + public byte[] getPublicKey() throws Exception { + initKeys(); + return data.id; + } + + /** + * Return an encoded private RSA key. this key can be decoded with an + * PKCS8EncodedKeySpec + * + * @return an encoded private key. + * @throws Exception + */ + public byte[] getPrivateKey() throws Exception { + initKeys(); + return data.secret; + } + + /* + * Initialize the keys. + */ + private void initKeys() throws Exception { + check(); + if (publicKey != null) + return; + + if (data.id == null || data.secret == null) { + generate(); + } else { + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(data.secret); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(data.id); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + privateKey = keyFactory.generatePrivate(privateKeySpec); + publicKey = keyFactory.generatePublic(publicKeySpec); + } + } + + /** + * Sign a byte array + */ + public byte[] sign(byte[] con) throws Exception { + initKeys(); + + Signature hmac = Signature.getInstance("SHA1withRSA"); + hmac.initSign(privateKey); + hmac.update(con); + return hmac.sign(); + } + + /** + * Verify a signed byte array + */ + public boolean verify(byte[] con) throws Exception { + initKeys(); + + Signature hmac = Signature.getInstance("SHA1withRSA"); + hmac.initVerify(publicKey); + hmac.update(con); + return hmac.verify(con); + } + + public void clear() { + data = new Data(); + IO.delete(where); + } + + public boolean containsKey(Object key) { + check(); + return data.map.containsKey(key); + } + + public boolean containsValue(Object value) { + check(); + return data.map.containsValue(value); + } + + public Set<java.util.Map.Entry<String,String>> entrySet() { + check(); + return data.map.entrySet(); + } + + public String get(Object key) { + check(); + + return data.map.get(key); + } + + public boolean isEmpty() { + check(); + return data.map.isEmpty(); + } + + public Set<String> keySet() { + check(); + return data.map.keySet(); + } + + public String put(String key, String value) { + check(); + dirty = true; + return data.map.put(key, value); + } + + public void putAll(Map< ? extends String, ? extends String> v) { + check(); + dirty = true; + data.map.putAll(v); + } + + public String remove(Object key) { + check(); + dirty = true; + return data.map.remove(key); + } + + public int size() { + check(); + return data.map.size(); + } + + public Collection<String> values() { + check(); + return data.map.values(); + } + + public boolean isDirty() { + return dirty; + } + +} Propchange: felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/Settings.java ------------------------------------------------------------------------------ svn:keywords = Author Date Id Revision Added: felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/packageinfo URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/packageinfo?rev=1381708&view=auto ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/packageinfo (added) +++ felix/trunk/bundleplugin/src/main/java/aQute/lib/settings/packageinfo Thu Sep 6 18:28:06 2012 @@ -0,0 +1 @@ +version 1.1 \ No newline at end of file Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/command/Command.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/command/Command.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/command/Command.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/command/Command.java Thu Sep 6 18:28:06 2012 @@ -68,7 +68,7 @@ public class Command { if (timeout != 0) { timer = new TimerTask() { - @Override + //@Override TODO why did this not work? TimerTask implements Runnable public void run() { timedout = true; process.destroy(); Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/Digester.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/Digester.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/Digester.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/Digester.java Thu Sep 6 18:28:06 2012 @@ -48,4 +48,13 @@ public abstract class Digester<T extends public abstract T digest(byte[] bytes) throws Exception; public abstract String getAlgorithm(); + + public T from(File f) throws Exception { + IO.copy(f, this); + return digest(); + } + public T from(byte[] f) throws Exception { + IO.copy(f, this); + return digest(); + } } Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/MD5.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/MD5.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/MD5.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/MD5.java Thu Sep 6 18:28:06 2012 @@ -36,6 +36,13 @@ public class MD5 extends Digest { } public static MD5 digest(byte [] data) throws Exception { - return getDigester().digest(data); + return getDigester().from(data); + } + + public static MD5 digest(File f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); + } + public static MD5 digest(InputStream f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); } } \ No newline at end of file Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA1.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA1.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA1.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA1.java Thu Sep 6 18:28:06 2012 @@ -36,6 +36,13 @@ public class SHA1 extends Digest { } public static SHA1 digest(byte [] data) throws Exception { - return getDigester().digest(data); + return getDigester().from(data); + } + + public static SHA1 digest(File f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); + } + public static SHA1 digest(InputStream f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); } } \ No newline at end of file Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA256.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA256.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA256.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/cryptography/SHA256.java Thu Sep 6 18:28:06 2012 @@ -35,7 +35,15 @@ public class SHA256 extends Digest { return ALGORITHM; } + public static SHA256 digest(byte [] data) throws Exception { - return getDigester().digest(data); + return getDigester().from(data); + } + + public static SHA256 digest(File f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); + } + public static SHA256 digest(InputStream f) throws NoSuchAlgorithmException, Exception { + return getDigester().from(f); } } \ No newline at end of file Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/qtokens/QuotedTokenizer.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/qtokens/QuotedTokenizer.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/qtokens/QuotedTokenizer.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/qtokens/QuotedTokenizer.java Thu Sep 6 18:28:06 2012 @@ -82,7 +82,7 @@ public class QuotedTokenizer { c = string.charAt(index++); if (c == quote) break; - if (c == '\\' && index < string.length() && string.charAt(index + 1) == quote) + if (c == '\\' && index < string.length() && string.charAt(index) == quote) c = string.charAt(index++); sb.append(c); } Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/reporter/ReporterAdapter.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/reporter/ReporterAdapter.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/reporter/ReporterAdapter.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/reporter/ReporterAdapter.java Thu Sep 6 18:28:06 2012 @@ -246,20 +246,9 @@ public class ReporterAdapter implements return getInfo(other,null); } public boolean getInfo(Report other, String prefix) { - boolean ok = true; - if ( prefix == null) - prefix = ""; - else - prefix = prefix + ": "; - for ( String error : other.getErrors()) { - errors.add( prefix + error); - ok = false; - } - - for ( String warning : other.getWarnings()) { - warnings.add( prefix + warning); - } - return ok; + addErrors(prefix, other.getErrors()); + addWarnings(prefix, other.getWarnings()); + return other.isOk(); } public Location getLocation(String msg) { @@ -285,4 +274,31 @@ public class ReporterAdapter implements public <T> T getMessages(Class<T> c) { return ReporterMessages.base(this, c); } + + /** + * Add a number of errors + */ + + public void addErrors( String prefix, Collection<String> errors) { + if ( prefix == null) + prefix = ""; + else + prefix = prefix + ": "; + for ( String s: errors) { + this.errors.add( prefix + s); + } + } + /** + * Add a number of warnings + */ + + public void addWarnings( String prefix, Collection<String> warnings) { + if ( prefix == null) + prefix = ""; + else + prefix = prefix + ": "; + for ( String s: warnings) { + this.warnings.add( prefix + s); + } + } } Modified: felix/trunk/bundleplugin/src/main/java/aQute/libg/sed/ReplacerAdapter.java URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/libg/sed/ReplacerAdapter.java?rev=1381708&r1=1381707&r2=1381708&view=diff ============================================================================== --- felix/trunk/bundleplugin/src/main/java/aQute/libg/sed/ReplacerAdapter.java (original) +++ felix/trunk/bundleplugin/src/main/java/aQute/libg/sed/ReplacerAdapter.java Thu Sep 6 18:28:06 2012 @@ -268,7 +268,7 @@ public class ReplacerAdapter extends Rep } catch (InvocationTargetException e) { if (e.getCause() instanceof IllegalArgumentException) { - reporter.error("%s, for cmd: %s, arguments; %s", e.getMessage(), method, Arrays.toString(args)); + reporter.error("%s, for cmd: %s, arguments; %s", e.getCause().getMessage(), method, Arrays.toString(args)); } else { reporter.warning("Exception in replace: " + e.getCause()); e.getCause().printStackTrace();
