aQute.lib.deployer.FileRepo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bnd Show documentation
Show all versions of bnd Show documentation
A command line utility and Ant plugin to wrap, build, or examine bundles.
package aQute.lib.deployer;
import java.io.*;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import aQute.bnd.osgi.*;
import aQute.bnd.osgi.Verifier;
import aQute.bnd.service.*;
import aQute.bnd.service.repository.SearchableRepository.ResourceDescriptor;
import aQute.bnd.version.*;
import aQute.lib.collections.*;
import aQute.lib.hex.*;
import aQute.lib.io.*;
import aQute.lib.json.*;
import aQute.lib.persistentmap.*;
import aQute.libg.command.*;
import aQute.libg.cryptography.*;
import aQute.libg.reporter.*;
import aQute.service.reporter.*;
/**
* 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:
*
* - {@link #CMD_INIT} - Is only executed when the location did not exist
* - {@link #CMD_OPEN} - Called (after init if necessary) to open it once
* - {@link #CMD_REFRESH} - Called when refreshed.
* - {@link #CMD_BEFORE_PUT} - Before the file system is changed
* - {@link #CMD_AFTER_PUT} - After the file system has changed, and the put
*
- {@link #CMD_BEFORE_GET} - Before the file is gotten
* - {@link #CMD_AFTER_ACTION} - Before the file is gotten
* - {@link #CMD_CLOSE} - When the repo is closed and no more actions will
* take place
was a success
* - {@link #CMD_ABORT_PUT} - When the put is aborted.
* - {@link #CMD_CLOSE} - To close the repository.
*
* 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 {
/**
* 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";
/**
* Should this file repo have an index? Either true or false (absent)
*/
public final static String INDEX = "index";
/**
* 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.
*
* @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.
*
* @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.
*
* @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.
*
*
* @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.
*
* @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.
*
* @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.
*
* @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.
*
*
* @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();
public static final int MAX_MAJOR = 999999999;
private static final SortedSet LATEST_SET = new TreeSet(
Collections.singleton(new Version(MAX_MAJOR, 0,
0)));
final static JSONCodec codec = new JSONCodec();
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)");
Reporter reporter;
boolean dirty;
String name;
boolean inited;
boolean trace;
PersistentMap index;
private boolean hasIndex;
public FileRepo() {}
public FileRepo(String name, File location, boolean canWrite) {
this.name = name;
this.root = location;
this.canWrite = canWrite;
}
/**
* 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());
}
if (hasIndex)
index = new PersistentMap(new File(root, ".index"), ResourceDescriptor.class);
open();
return true;
}
/**
* @see aQute.bnd.service.Plugin#setProperties(java.util.Map)
*/
public void setProperties(Map map) {
String location = map.get(LOCATION);
if (location == null)
throw new IllegalArgumentException("Location must be set on a FileRepo plugin");
root = IO.getFile(IO.home, location);
String readonly = map.get(READONLY);
if (readonly != null && Boolean.valueOf(readonly).booleanValue())
canWrite = false;
hasIndex = Processor.isTrue(map.get(INDEX));
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));
}
/**
* Answer if this repository can write.
*/
public boolean canWrite() {
return canWrite;
}
/**
* 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. 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);
Jar tmpJar = new Jar(tmpFile);
try {
dirty = true;
String bsn = tmpJar.getBsn();
if (bsn == null)
throw new IllegalArgumentException("No bsn set in jar: " + tmpFile);
String versionString = tmpJar.getVersion();
if (versionString == null)
versionString = "0";
else if (!Verifier.isVersion(versionString))
throw new IllegalArgumentException("Incorrect version in : " + tmpFile + " " + versionString);
Version version = new Version(versionString);
reporter.trace("bsn=%s version=%s", bsn, version);
File dir = new File(root, bsn);
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);
reporter.trace("updating %s ", file.getAbsolutePath());
if (hasIndex)
index.put(bsn + "-" + version, buildDescriptor(tmpFile, tmpJar, digest, bsn, version));
// An open jar on file will fail rename on windows
tmpJar.close();
IO.rename(tmpFile, file);
fireBundleAdded(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");
IO.copy(file, latest);
reporter.trace("updated %s", file.getAbsolutePath());
return file;
}
finally {
tmpJar.close();
}
}
/*
* (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 {
/* determine if the put is allowed */
if (!canWrite) {
throw new IOException("Repository is read-only");
}
assert stream != null;
if (options == null)
options = DEFAULTOPTIONS;
init();
/*
* copy the artifact from the (new/digest) stream into a temporary file
* in the root directory of the repository
*/
File tmpFile = IO.createTempFile(root, "put", ".jar");
try {
DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1"));
try {
IO.copy(dis, tmpFile);
byte[] digest = dis.getMessageDigest().digest();
if (options.digest != null && !Arrays.equals(digest, options.digest))
throw new IOException("Retrieved 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();
PutResult result = new PutResult();
result.digest = digest;
result.artifact = file.toURI();
return result;
}
finally {
dis.close();
}
}
catch (Exception e) {
abortPut(tmpFile);
throw e;
}
finally {
IO.delete(tmpFile);
}
}
public void setLocation(String string) {
root = IO.getFile(string);
}
public void setReporter(Reporter reporter) {
this.reporter = reporter;
}
public List list(String regex) throws Exception {
init();
Instruction pattern = null;
if (regex != null)
pattern = new Instruction(regex);
List result = new ArrayList();
if (root == null) {
if (reporter != null)
reporter.error("FileRepo root directory is not set.");
} else {
File[] list = root.listFiles();
if (list != null) {
for (File f : list) {
if (!f.isDirectory())
continue; // ignore non-directories
String fileName = f.getName();
if (fileName.charAt(0) == '.')
continue; // ignore hidden files
if (pattern == null || pattern.matches(fileName))
result.add(fileName);
}
} else if (reporter != null)
reporter.error("FileRepo root directory (%s) does not exist", root);
}
return result;
}
public SortedSet versions(String bsn) throws Exception {
init();
File dir = new File(root, bsn);
boolean latest = false;
if (dir.isDirectory()) {
String versions[] = dir.list();
List list = new ArrayList();
for (String v : versions) {
Matcher m = REPO_FILE.matcher(v);
if (m.matches()) {
String version = m.group(2);
if (!version.equals("latest"))
list.add(new Version(version));
else
latest = true;
}
}
if (list.isEmpty() && latest)
return LATEST_SET;
else
return new SortedList(list);
}
return SortedList.empty();
}
@Override
public String toString() {
return String.format("%-40s r/w=%s", root.getAbsolutePath(), canWrite());
}
public File getRoot() {
return root;
}
public boolean refresh() throws Exception {
init();
exec(refresh, root);
if (dirty) {
dirty = false;
return true;
}
rebuildIndex();
return false;
}
public String getName() {
if (name == null) {
return toString();
}
return name;
}
/*
* (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 properties, DownloadListener... listeners)
throws Exception {
init();
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 void setRegistry(Registry registry) {
this.registry = registry;
}
public String getLocation() {
return root.toString();
}
public Map actions(Object... target) throws Exception {
if (target == null || target.length == 0) {
Map actions = new LinkedHashMap();
actions.put("Rebuild Resource Index", new Runnable() {
public void run() {
try {
rebuildIndex();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
});
return actions; // no default actions
}
try {
String bsn = (String) target[0];
Version version = (Version) target[1];
final File f = get(bsn, version, null);
if (f == null)
return null;
Map actions = new HashMap();
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;
}
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);
try {
String bsn = (String) target[0];
Version version = (Version) target[1];
Map map = null;
if (target.length > 2)
map = (Map) target[2];
File f = getLocal(bsn, version, map);
String s = "";
ResourceDescriptor descriptor = getDescriptor(bsn, version);
if (descriptor != null && descriptor.description != null) {
s = descriptor.description + "\n";
}
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;
}
protected File getLocal(String bsn, Version version, Map properties) {
File dir = new File(root, bsn);
if (version.getMajor() == MAX_MAJOR && version.getMinor() == 0 && version.getMicro() == 0
&& version.getQualifier() == null) {
File fjar = new File(dir, bsn + "-latest.jar");
if (fjar.isFile())
return fjar.getAbsoluteFile();
}
File fjar = new File(dir, bsn + "-" + version.getWithoutQualifier() + ".jar");
if (fjar.isFile())
return fjar.getAbsoluteFile();
File sfjar = new File(dir, version.getWithoutQualifier() + ".jar");
if (sfjar.isFile())
return sfjar.getAbsoluteFile();
File flib = new File(dir, bsn + "-" + version.getWithoutQualifier() + ".lib");
if (flib.isFile())
return flib.getAbsoluteFile();
File sflib = new File(dir, version.getWithoutQualifier() + ".lib");
if (sflib.isFile())
return sflib.getAbsoluteFile();
return fjar.getAbsoluteFile();
}
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 = "";
} else 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 "";
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(File file) {
if (registry == null)
return;
List listeners = registry.getPlugins(RepositoryListenerPlugin.class);
Jar jar = null;
for (RepositoryListenerPlugin listener : listeners) {
try {
if (jar == null)
jar = new Jar(file);
listener.bundleAdded(this, jar, file);
}
catch (Exception e) {
if (reporter != null)
reporter.warning("Repository listener threw an unexpected exception: %s", e);
}
finally {
if (jar != null)
jar.close();
}
}
}
/**
* 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) {
// replaceAll backslash magic ensures windows paths
// remain intact
line = line.replaceAll("\\$\\{@\\}", args[0].toString().replaceAll("\\\\", "\\\\\\\\"));
}
// replaceAll backslash magic ensures windows paths remain
// intact
line = line.replaceAll("\\$" + i, args[i].toString().replaceAll("\\\\", "\\\\\\\\"));
}
}
// purge remaining placeholders
line = line.replaceAll("\\s*\\$[0-9]\\s*", "");
int result = 0;
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) {
// FIXME ignoring possible shell setting stdin approach used
// below does not work in windows
Command cmd = new Command("cmd.exe /C " + line);
cmd.setCwd(getRoot());
result = cmd.execute(stdout, stderr);
} else {
if (shell == null) {
shell = "sh";
}
Command cmd = new Command(shell);
cmd.setCwd(getRoot());
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);
}
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 {
init();
assert bsn != null;
SortedSet versions;
if (version == null)
versions = versions(bsn);
else
versions = new SortedList(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));
index.remove(bsn + "-" + version);
}
public ResourceDescriptor getDescriptor(String bsn, Version version) throws Exception {
init();
if (hasIndex) {
return index.get(bsn + "-" + version);
}
return null;
}
public SortedSet getResources() throws Exception {
init();
if (hasIndex) {
TreeSet resources = new TreeSet(
new Comparator() {
public int compare(ResourceDescriptor a, ResourceDescriptor b) {
if (a == b)
return 0;
int r = a.bsn.compareTo(b.bsn);
if (r != 0)
return r;
if (a.version != b.version) {
if (a.version == null)
return 1;
if (b.version == null)
return -1;
r = a.version.compareTo(b.version);
if (r != 0)
return r;
}
if (a.id.length > b.id.length)
return 1;
if (a.id.length < b.id.length)
return -1;
for (int i = 0; i < a.id.length; i++) {
if (a.id[i] > b.id[i])
return 1;
if (a.id[i] < b.id[i])
return 1;
}
return 0;
}
});
for (ResourceDescriptor rd : index.values()) {
resources.add(rd);
}
return resources;
}
return null;
}
public ResourceDescriptor getResource(byte[] sha) throws Exception {
init();
if (hasIndex) {
for (ResourceDescriptor rd : index.values()) {
if (Arrays.equals(rd.id, sha))
return rd;
}
}
return null;
}
void rebuildIndex() throws Exception {
init();
if (!hasIndex)
return;
index.clear();
for (String bsn : list(null)) {
for (Version version : versions(bsn)) {
File f = get(bsn, version, null);
index.put(bsn + "-" + version, buildDescriptor(f, null, null, bsn, version));
}
}
}
private ResourceDescriptor buildDescriptor(File f, Jar jar, byte[] digest, String bsn, Version version)
throws NoSuchAlgorithmException, Exception {
init();
Jar tmpjar = jar;
if (jar == null)
tmpjar = new Jar(f);
try {
Manifest m = tmpjar.getManifest();
ResourceDescriptor rd = new ResourceDescriptor();
rd.bsn = bsn;
rd.version = version;
rd.description = m.getMainAttributes().getValue(Constants.BUNDLE_DESCRIPTION);
rd.id = digest;
if (rd.id == null)
rd.id = SHA1.digest(f).digest();
rd.sha256 = SHA256.digest(f).digest();
rd.url = f.toURI();
return rd;
}
finally {
if (jar == null)
tmpjar.close();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy