package aQute.library.bnd;
import java.awt.*;
import java.awt.datatransfer.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.List;
import java.util.concurrent.*;
import java.util.jar.*;
import java.util.regex.*;
import org.osgi.resource.Capability;
import org.osgi.service.indexer.*;
import org.osgi.service.indexer.impl.*;
import aQute.bnd.header.*;
import aQute.bnd.osgi.*;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.service.*;
import aQute.bnd.service.repository.*;
import aQute.bnd.version.*;
import aQute.lib.collections.*;
import aQute.lib.converter.*;
import aQute.lib.hex.*;
import aQute.lib.io.*;
import aQute.lib.justif.*;
import aQute.lib.settings.*;
import aQute.libg.glob.*;
import aQute.libg.reporter.*;
import aQute.library.capreq.*;
import aQute.library.remote.*;
import aQute.service.library.*;
import aQute.service.library.Library.Deposit;
import aQute.service.library.Library.Program;
import aQute.service.library.Library.Requirement;
import aQute.service.library.Library.Revision;
import aQute.service.library.Library.RevisionRef;
import aQute.service.library.SharedTypes.Contributor;
import aQute.service.library.SharedTypes.Developer;
import aQute.service.library.SharedTypes.License;
import aQute.service.library.Library.Requirement;
import aQute.service.reporter.*;
import aQute.struct.struct.Error;
/**
* A bnd repository based on the jpm4j server.
*/
public class JpmRepository implements Plugin, RepositoryPlugin, Closeable, Refreshable, Actionable, RegistryPlugin,
org.osgi.service.repository.Repository, SearchableRepository {
private static final String SUPERIOR_1 = " \u00B9";
private final String DOWN_ARROW = " \u21E9";
protected final DownloadListener[] EMPTY_LISTENER = new DownloadListener[0];
private Pattern SHA = Pattern.compile(
"([A-F0-9][a-fA-F0-9]){20,20}",
Pattern.CASE_INSENSITIVE);
private Pattern COORDINATE_P = Pattern
.compile("([-\\w_.\\d]+):([-\\w_.\\d]+)[:@]([-\\w_.\\d]+)");
private XRemoteLibrary library;
private final Justif j = new Justif(80, new int[] {
20, 28, 36, 44
});
private final Settings settings = new Settings();
private boolean canwrite;
private final MultiMap<File,DownloadListener> queues = new MultiMap<File,RepositoryPlugin.DownloadListener>();
private final Pattern JPM_REVISION_URL_PATTERN = Pattern
.compile("https?://.+#!?/p/([^/]+)/([^/]+)/([^/]+)?/([^/]+)+");
private Options options;
private Reporter reporter = new ReporterAdapter(System.out);
/**
* Maintains the index of what we've downloaded so far.
*/
private File indexFile;
private File obrFile;
private Index index;
private boolean offline;
private Registry registry;
private StoredRevisionCache cache;
private Set<File> notfound = new HashSet<File>();
private Set<String> notfoundref = new HashSet<String>();
private final Semaphore limitDownloads = new Semaphore(12);
private Engine engine;
interface Options {
/**
* The URL to the remote repository. Default is jpm4j.org/rest
*
* @return
*/
URI url();
/**
* The domain of a depository,optional.
*
* @return
*/
String domain();
/**
* The id of the depository
*
* @return
*/
String depository();
/**
* The email address of the user
*
* @return
*/
String email();
/**
* Where the index file is stored. The default should reside in the
* workspace and be part of the scm
*
* @return
*/
String index();
/**
* The cache location, default is ~/.bnd/cache. This file is relative
* from the users home directory if not absolute.
*
* @return
*/
String location();
/**
* The
*/
/**
* Set the settings
*/
String settings();
}
/**
* Get a revision.
*/
@Override
public File get(String bsn, Version version, Map<String,String> attrs, final DownloadListener... listeners)
throws Exception {
init();
// Check if we're supposed to have this
RevisionRef resource = index.getRevisionRef(bsn, version);
if (resource == null) {
if (index.isLearning())
// Not in the index. See if we can retrieve it. We add it
// when the bsn was not marked remote so we can
// let the repo 'discover' the bsns so that an existing
// workspace can learn the configuration.
return getRemote(bsn, version, attrs, true, listeners);
else
return null;
} else
return getLocal(resource, attrs, listeners);
}
/**
* The index indicates we're allowed to have this one. So check if we have
* it cached or if we need to download it.
*/
private File getLocal(RevisionRef resource, Map<String,String> attrs, DownloadListener... downloadListeners)
throws Exception {
File file = cache.getPath(resource.bsn, Index.toVersion(resource), resource.revision);
scheduleDownload(file, resource.url, resource.revision, resource.size, downloadListeners);
return file;
}
/**
* The index does not know the artifact but since bndtools immediately
* fetches something when clicked upon we need to fetch it in the cache.
* However, since it will not be added automatically to the index it will
* not be listed and cannot be used as a dep. Needs to be added for that.
*/
private File getRemote(String bsn, Version version, Map<String,String> attrs, boolean addIt,
DownloadListener... downloadListeners) throws Exception {
if (!isConnected()) {
reporter.warning("system is not connected, can't retrieve %s", bsn + "-" + version);
return null;
}
// Search on jpm
RevisionRef ref = getRevisionRef(bsn, version);
if (ref == null)
return null;
if (addIt) {
index.addRevision(ref);
index.save();
}
File file = cache.getPath(bsn, version, ref.revision);
scheduleDownload(file, ref.url, ref.revision, ref.size, downloadListeners);
return file;
}
/**
* Schedule a download, handling the listeners
*/
private void scheduleDownload(final File file, final URI url, final byte[] sha, final long size,
DownloadListener... listeners) throws Exception {
synchronized (notfound) {
if (notfound.contains(file)) {
failure(listeners, file, "Not found");
return;
}
}
if (file.isFile()) {
if (file.length() == size) {
// Already exists, done
success(listeners, file);
reporter.trace("was in cache");
return;
}
reporter.error("found file but of different length %s, will refetch", file);
} else {
reporter.trace("not in cache %s", file + " " + queues);
}
// Check if we need synchronous
if (listeners.length == 0) {
reporter.trace("in cache, no listeners");
cache.download(file, url, sha);
return;
}
//
// With download listeners we need to be careful to queue them
// appropriately. Don't want to download n times because
// requests arrive during downloads.
//
synchronized (queues) {
List<DownloadListener> list = queues.get(file);
boolean first = list == null || list.isEmpty();
for (DownloadListener l : listeners) {
queues.add(file, l);
}
if (!first) {
// return, file is being downloaded by another and that
// other will signal the download listener.
reporter.trace("someone else is downloading our file " + queues.get(file));
return;
}
}
try {
reporter.trace("starting thread for " + file);
// Limit the total downloads going on at the same time
limitDownloads.acquire();
Thread t = new Thread("Downloading " + file) {
public void run() {
try {
reporter.trace("downloading in background " + file);
cache.download(file, url, sha);
success(queues.get(file).toArray(EMPTY_LISTENER), file);
}
catch (FileNotFoundException e) {
synchronized (notfound) {
reporter.error("Not found %s", e, file);
notfound.add(file);
}
synchronized (queues) {
failure(queues.get(file).toArray(EMPTY_LISTENER), file, e.toString());
}
}
catch (Throwable e) {
reporter.error("failed to download %s: %s", e, file);
synchronized (queues) {
failure(queues.get(file).toArray(EMPTY_LISTENER), file, e.toString());
}
}
finally {
synchronized (queues) {
queues.remove(file);
}
reporter.trace("downloaded " + file);
// Allow other downloads to start
limitDownloads.release();
}
}
};
t.start();
}
catch (Exception e) {
// Is very unlikely to happen but we must ensure the
// listeners are called and we're at the head of the queue
reporter.error("Starting a download for %s failed %s", file, e);
synchronized (queues) {
failure(queues.get(file).toArray(EMPTY_LISTENER), file, e.toString());
queues.remove(file);
}
}
}
/**
* API method
*/
@Override
public boolean canWrite() {
return canwrite;
}
/**
* Put an artifact in the repo
*/
@Override
public PutResult put(InputStream in, PutOptions options) throws Exception {
try {
Deposit depo = new Library.Deposit();
depo.depository = this.options.depository();
depo.organization = this.options.domain();
depo.email = settings.getEmail();
depo.type = options.type;
List<Error> s = depo.validate();
if (!s.isEmpty())
throw new IllegalArgumentException("Invalid request: " + s.toString());
File f = File.createTempFile("depo-", ".jar");
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
DigestInputStream din = new DigestInputStream(in, md);
IO.copy(din, f);
byte[] sha = md.digest();
if (options.digest != null && !Arrays.equals(sha, depo.sha))
throw new IllegalArgumentException("Indicated SHA does not match input stream");
depo.sha = sha;
FileInputStream fin = new FileInputStream(f);
try {
Revision r = null; // = library.deposit(depo, fin);
if (r == null)
throw new IllegalArgumentException("No result returned");
if (!Arrays.equals(r._id, sha))
throw new IllegalArgumentException("Mismatch in shas " + Hex.toHexString(r._id) + "!="
+ Hex.toHexString(options.digest));
PutResult putr = new PutResult();
putr.artifact = r.url;
putr.digest = r._id;
return putr;
}
finally {
fin.close();
}
}
finally {
f.delete();
}
}
finally {
in.close();
}
}
/**
* If we have no search or an empty search we list our index. Otherwise we
* query remotely.
*/
@Override
public List<String> list(String query) throws Exception {
init();
if (query == null || query.trim().isEmpty())
query = "*";
Glob glob = null;
try {
glob = new Glob(query);
}
catch (Exception e) {
glob = new Glob("*");
}
Set<String> bsns = new HashSet<String>();
for (String bsn : index.getBsns()) {
if (glob.matcher(bsn).matches())
bsns.add(bsn);
}
List<String> result = new ArrayList<String>(bsns);
Collections.sort(result);
return result;
}
/**
* List the versions belonging to a bsn
*/
@Override
public SortedSet<Version> versions(String bsn) throws Exception {
init();
SortedSet<Version> versions = index.getVersions(bsn);
if (!versions.isEmpty() || !index.isLearning()) {
return versions;
}
// We try to learn what is needed so a new project
// does not have to add them all by hand.
Program program = library.getProgram(Library.OSGI_GROUP, bsn);
if (program == null)
return versions;
RevisionRef ref = program.revisions.get(0);
index.addRevision(ref);
index.save();
return index.getVersions(bsn);
}
/*
* Convert a baseline/qualifier to a version
*/
private Version toVersion(String baseline, String qualifier) {
if (qualifier == null || qualifier.isEmpty())
return new Version(baseline);
else
return new Version(baseline + "." + qualifier);
}
/*
* Return if bsn is a SHA
*/
private boolean isSha(String bsn) {
return SHA.matcher(bsn).matches();
}
@Override
public String getName() {
return "jpm4j";
}
@Override
public void setProperties(Map<String,String> map) {
try {
options = Converter.cnv(Options.class, map);
setOptions(options);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public void setOptions(Options options) {
try {
canwrite = options.domain() != null && options.depository() != null;
setLibrary(options.url(), options.location());
setLocation(options.location());
String email = options.email();
if (email == null)
email = settings.getEmail();
if (options.domain() != null) {
if (email != null)
library.credentials(email, InetAddress.getLocalHost().getHostName(), settings.getPublicKey(),
settings.getPrivateKey());
}
if (options.index() == null)
throw new IllegalArgumentException("Index file not set");
setIndex(options.index());
}
catch (Exception e) {
if (reporter != null)
reporter.exception(e, "Creating options");
throw new RuntimeException(e);
}
}
public void setIndex(String indexFile) {
this.indexFile = new File(indexFile);
this.obrFile = new File(this.indexFile.getAbsolutePath() + ".cache");
if (!this.indexFile.isAbsolute())
throw new IllegalArgumentException("Index file must be absolute");
}
public void setLocation(String location) {
if (location != null) {
File cacheDir = IO.getFile(IO.home, location);
cacheDir.mkdirs();
if (!cacheDir.isDirectory())
throw new IllegalArgumentException("Not able to create cache directory " + cacheDir);
cache = new StoredRevisionCache(cacheDir, settings);
File pc = new File(cacheDir, "jpm-library");
pc.mkdirs();
}
}
public void setLibrary(URI url, String location) {
if (url != null)
library = new XRemoteLibrary(url.toString());
else
library = new XRemoteLibrary(null);
if (location == null)
return;
File cache = IO.getFile(IO.home, location);
cache.mkdirs();
if (!cache.exists()) {
reporter.error("Got cache dir but cannot create it: %s", null, cache);
return;
}
library.setCache(cache);
}
@Override
public void setReporter(Reporter processor) {
reporter = processor;
if (index != null)
index.setReporter(reporter);
}
@Override
public boolean refresh() throws Exception {
index = new Index(indexFile);
library.refresh();
cache.refresh();
notfound.clear();
notfoundref.clear();
engine = null;
return true;
}
/**
* Return the actions for this repository
*/
@Override
public Map<String,Runnable> actions(Object... target) throws Exception {
init();
if (target == null)
return null;
if (target.length == 0)
return getRepositoryActions();
final String bsn = (String) target[0];
Program careful = null;
try {
careful = library.getProgram(Library.OSGI_GROUP, bsn);
}
catch (Exception e) {
reporter.error("Offline? %s", e);
}
final Program p = careful;
if (target.length == 1)
return getProgramActions(bsn, p);
if (target.length >= 2) {
final Version version = (Version) target[1];
return getRevisionActions(p, bsn, version);
}
return null;
}
/**
* @param p
* @param bsn
* @param version
* @return
* @throws Exception
*/
private Map<String,Runnable> getRevisionActions(final Program p, final String bsn, final Version version)
throws Exception {
final Library.RevisionRef resource = index.getRevisionRef(bsn, version);
Map<String,Runnable> map = new LinkedHashMap<String,Runnable>();
map.put("Inspect Revision", new Runnable() {
public void run() {
open("https://www.jpm4j.org#!/p/sha/" + Hex.toHexString(resource.revision) + "//0.0.0");
}
});
RevisionRef update = findUpdate(p, version);
if (update != null) {
map.put("Update to " + update.version, new Runnable() {
public void run() {
try {
update(p, bsn, version);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
map.put("Delete", new Runnable() {
public void run() {
try {
delete(bsn, version, true);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
map.put("Copy Metadata", new Runnable() {
public void run() {
try {
wrap(bsn, version);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
return map;
}
/**
* @param bsn
* @param p
* @return
* @throws Exception
*/
private Map<String,Runnable> getProgramActions(final String bsn, final Program p) throws Exception {
Map<String,Runnable> map = new LinkedHashMap<String,Runnable>();
if (p != null) {
Set<Version> update = new TreeSet<Version>();
for (Version v : index.getVersions(bsn)) {
RevisionRef ref = findUpdate(p, v);
if (ref != null) {
update.add(toVersion(ref.baseline, ref.qualifier));
}
}
map.put("Inspect Program", new Runnable() {
public void run() {
open("https://www.jpm4j.org#/p/osgi/" + bsn);
}
});
RevisionRef ref = p.revisions.get(0);
Version latest = toVersion(ref.baseline, ref.qualifier);
for (Version v : index.getVersions(bsn)) {
if (v.equals(latest)) {
latest = null;
break;
}
}
final Version l = latest;
String title = "Get Latest";
if (latest == null)
title = "-" + title;
else
title += " " + l;
map.put(title, new Runnable() {
public void run() {
try {
add(bsn, l);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
});
String label = "-Update";
if (!update.isEmpty())
label = "Update to " + Processor.join(update);
map.put(label, new Runnable() {
@Override
public void run() {
try {
update(bsn);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
});
map.put("Delete", new Runnable() {
public void run() {
try {
delete(bsn);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
});
} else {
map.put("-Update (offline)", null);
}
return map;
}
/**
* @return
*/
private Map<String,Runnable> getRepositoryActions() {
Map<String,Runnable> map = new LinkedHashMap<String,Runnable>();
map.put("Delete Cache", new Runnable() {
@Override
public void run() {
try {
cache.deleteAll();
}
catch (Exception e) {
reporter.error("Deleting cache %s", e);
}
}
});
map.put("Refresh", new Runnable() {
@Override
public void run() {
try {
refresh();
}
catch (Exception e) {
reporter.error("Refreshing %s", e);
}
}
});
map.put("Update All", new Runnable() {
@Override
public void run() {
try {
updateAll();
}
catch (Exception e) {
reporter.error("Update all %s", e);
}
}
});
String title = "Learning{Unknown resources are an error, select to learn}";
if (index.isLearning()) {
title = "!Learning{Will attempt to fetch unknown resources, select to make this an error}";
}
map.put(title, new Runnable() {
@Override
public void run() {
try {
index.setLearning(!index.isLearning());
index.save();
}
catch (Exception e) {
reporter.error("Learning %s", e);
}
}
});
title = "Recurse{Do not fetch dependencies automatically}";
if (index.isRecurse()) {
title = "!Recurse{Fetch dependencies automatically}";
}
map.put(title, new Runnable() {
@Override
public void run() {
try {
index.setRecurse(!index.isRecurse());
index.save();
}
catch (Exception e) {
reporter.error("Learning %s", e);
}
}
});
return map;
}
@Override
public String tooltip(Object... target) throws Exception {
init();
if (target == null || target.length == 0)
return repositoryTooltip();
if (target.length == 1)
return programTooltip((String) target[0]);
if (target.length == 2)
return revisionTooltip((String) target[0], (Version) target[1]);
return "Hmm, have no idea on what object you want a tooltip ...";
}
private String repositoryTooltip() throws Exception {
Formatter f = new Formatter();
try {
f.format("%s, a JPM4J repository!\n", getName());
f.format("Repository %s\n", options.url() == null ? "default" : options.url());
if (options.depository() != null)
f.format("Depository %s\n", options.depository());
String email = options.email();
if (email == null)
email = settings.getEmail();
if (email != null)
f.format("Email %s\n", email);
f.format("Cache location %s\n", options.location());
f.format("Index file %s\n", options.index());
f.format("Number of bsns %s\n", index.getBsns().size());
f.format("Number of revs %s\n", index.getRevisionRefs().size());
return f.toString();
}
finally {
f.close();
}
}
private String programTooltip(String bsn) throws Exception {
Program p = library.getProgram(Library.OSGI_GROUP, bsn);
if (p != null) {
Formatter sb = new Formatter();
try {
if (p.wiki != null && p.wiki.text != null)
sb.format("%s\n", p.wiki.text.replaceAll("#\\s?", ""));
else if (p.last.description != null)
sb.format("%s\n", p.last.description);
else
sb.format("No description\n");
j.wrap((StringBuilder) sb.out());
return sb.toString();
}
finally {
sb.close();
}
}
return null;
}
private String revisionTooltip(String bsn, Version version) throws Exception {
RevisionRef r = getRevisionRef(bsn, version);
if (r == null)
return null;
Formatter sb = new Formatter();
try {
sb.format("[%s:%s", r.groupId, r.artifactId);
if (r.classifier != null) {
sb.format(":%s", r.classifier);
}
sb.format("@%s]\n\n", r.version);
if (r.releaseSummary != null)
sb.format("%s\n\n", r.releaseSummary);
if (r.description != null)
sb.format("%s\n\n", r.description.replaceAll("#\\s*", ""));
sb.format("Size %s\nSHA-1 %s\nAge %s\nURL %s\n", size(r.size, 0), Hex.toHexString(r.revision),
age(r.created), r.url);
File f = cache.getPath(bsn, version, r.revision);
if (f.isFile() && f.length() == r.size)
sb.format("Cached %s\n", f);
else
sb.format("Not downloaded\n");
Program p = library.getProgram(Library.OSGI_GROUP, bsn);
if (p != null) {
RevisionRef update = findUpdate(p, version);
if (update != null) {
sb.format(DOWN_ARROW + " This version should be updated to " + update.version);
}
}
j.wrap((StringBuilder) sb.out());
return sb.toString();
}
finally {
sb.close();
}
}
private List<RevisionRef> getRevisionRefs(String bsn) throws Exception {
String classifier = null;
String parts[] = bsn.split("__");
if (parts.length == 3) {
bsn = parts[0] + "__" + parts[1];
classifier = parts[2];
}
Program program = library.getProgram(Library.OSGI_GROUP, bsn);
if (program != null) {
List<RevisionRef> refs = new ArrayList<Library.RevisionRef>();
for (RevisionRef r : program.revisions) {
if (eq(classifier, r.classifier))
refs.add(r);
}
return refs;
}
return Collections.emptyList();
}
/**
* Find a revisionref for a bsn/version
*
* @param bsn
* @param version
* @return
* @throws Exception
*/
private RevisionRef getRevisionRef(String bsn, Version version) throws Exception {
// Handle when we have a sha reference
String id = bsn + "-" + version;
if (notfoundref.contains(id))
return null;
if (isSha(bsn) && version.equals(Version.LOWEST)) {
Revision r = library.getRevision(Library.SHA_GROUP + ":" + bsn + "@0.0.0");
if (r == null)
return null;
return new RevisionRef(r);
}
reporter.trace("Looking for %s-%s", bsn, version);
for (RevisionRef r : getRevisionRefs(bsn)) {
Version v = toVersion(r.baseline, r.qualifier);
if (v.equals(version))
return r;
}
notfoundref.add(id);
return null;
}
private boolean eq(String a, String b) {
if (a == null)
a = "";
if (b == null)
b = "";
return a.equals(b);
}
private String age(long created) {
if (created == 0)
return "unknown";
long diff = (System.currentTimeMillis() - created) / (1000 * 60 * 60);
if (diff < 48)
return diff + " hours";
diff /= 24;
if (diff < 14)
return diff + " days";
diff /= 7;
if (diff < 8)
return diff + " weeks";
diff /= 4;
if (diff < 24)
return diff + " months";
diff /= 12;
return diff + " years";
}
String[] sizes = {
"bytes", "Kb", "Mb", "Gb", "Tb", "Pb", "Showing off?"
};
private String size(long size, int power) {
if (power >= sizes.length)
return size + " Pb";
if (size < 1000)
return size + sizes[power];
return size(size / 1000, power + 1);
}
public void setLibrary(XRemoteLibrary library) {
this.library = library;
}
/**
* Create a bnd recipe for this element from the library data that wraps the
* JAR with some proper metadata. This is used to easily wrap jars from JPM
* into real bundles.
*
* @param bsn
* @param version
* @throws Exception
*/
private void wrap(String bsn, Version version) throws Exception {
Revision r = library.getRevision(Library.OSGI_GROUP + ":" + bsn + "@" + version.toString());
if (r == null)
return;
Jar jar = new Jar(get(bsn, version, null));
Formatter f = new Formatter();
Analyzer an = new Analyzer();
try {
an.setJar(jar);
an.analyze();
f.format("# Generated bnd wrapper for %s:%s@%s\n", r.groupId, r.artifactId, r.version);
f.format("# Recipe by ${global;name}\n\n");
if (an.getBundleSymbolicName() != null) {
f.format("#\n# ALREADY AN OSGI BUNDLE!!!\n#\n");
}
Manifest m = jar.getManifest();
if (m != null) {
String del = "# From original manifest\n";
boolean yes = false;
for (Entry<Object,Object> e : m.getMainAttributes().entrySet()) {
yes = true;
String key = e.getKey().toString();
if ("Manifest-Version".equalsIgnoreCase(key) || "Bundle-ManifestVersion".equalsIgnoreCase(key)
|| key.startsWith("Bnd") || key.startsWith("Created"))
continue;
f.format("%s%-40s: %s", del, key, e.getValue());
del = "\n";
}
if (yes)
f.format("\n\n# End of original manifest");
f.format("\n\n");
}
if (r.description != null)
f.format("Bundle-Description: %s %s\\\n ${disclaimer}\n", r.title == null ? ""
: r.title, r.description);
else
f.format("Bundle-Description: ##########################################################");
String v = r.baseline;
if (r.qualifier != null)
v += "." + r.qualifier;
f.format("Bundle-Version: %s\n", v);
if (r.icon != null)
f.format("Bundle-Icon: %s\n", r.icon);
f.format("\n#\n# Coordinates\n#\n");
f.format("JPM-From: sha:%s@0.0.0;coordinates=%s;bsn=%s\n",
Hex.toHexString(r._id), getCoordinates(r), r.bsn);
f.format("JPM-URL: https://jpm4j.org/#/p/%s/%s/%s/%s\n", r.groupId,
r.artifactId, r.classifier == null ? "" : r.classifier, r.version);
f.format("Include-Resource: @${repo;%s;0.0.0}\n", Hex.toHexString(r._id));
if (r.docUrl != null)
f.format("Bundle-DocURL: %s\n", r.docUrl);
if (r.organization != null && r.organization.name != null) {
f.format("Bundle-Vendor: %s\n", r.organization.name);
}
if (r.organization != null && r.organization.url != null) {
f.format("Bundle-ContactAddress: %s\n", r.organization.url);
}
String del = "Bundle-License: ";
for (License license : r.licenses) {
f.format("%s %s", del, license.name);
if (license.url != null)
f.format(";url='%s'", license.url);
if (license.comments != null)
f.format(";description='%s'", license.comments);
del = ", \\\n ";
}
f.format("\n\n");
del = "Bundle-Developers: \\\n ";
for (Developer dev : r.developers) {
f.format("%s '%s'", del, dev.name);
if (dev.email != null)
f.format(";email='%s'", dev.email);
if (dev.roles != null) {
ExtList<String> el = new ExtList<String>(dev.roles);
if (!el.isEmpty())
f.format(";roles='%s'", el.join());
}
del = ", \\\n ";
}
f.format("\n\n");
del = "Bundle-Contributors: \\\n ";
for (Contributor dev : r.contributors) {
f.format("%s %s", del, dev.name);
if (dev.email != null)
f.format(";email='%s'", dev.email);
if (dev.roles != null) {
ExtList<String> el = new ExtList<String>(dev.roles);
if (!el.isEmpty())
f.format(";roles='%s'", el.join());
}
del = ", \\\n ";
}
f.format("\n\n");
f.format("Export-Package: ");
del = "";
for (Entry<PackageRef,Attrs> pr : an.getContained().entrySet()) {
f.format("%s\\\n %s", del, pr.getKey().getFQN());
if (pr.getValue().containsKey("version")) {
f.format(";version=%s", pr.getValue().get("version"));
} else {
f.format(";version=100.0.0;provide:=true");
}
del = ",";
}
f.format("\n\n");
f.format("#\n# Remove after inspection:\n\nImport-Package: ");
del = "";
for (Entry<PackageRef,Attrs> pr : an.getReferred().entrySet()) {
if (pr.getKey().isJava() || pr.getKey().isMetaData())
continue;
f.format("%s\\\n %s", del, pr.getKey().getFQN());
if (pr.getValue().containsKey("version")) {
f.format(";version='%s'", pr.getValue().get("version"));
}
del = ",";
}
f.format("\n\n");
f.format("-buildpath:");
del = " \\\n ";
for (Requirement req : r.requirements) {
String name = req.name == null ? (String) req.ps.get("name:") : req.name;
if ("x-maven".equals(req.ns) && name != null) {
Matcher matcher = COORDINATE_P.matcher(name);
if (matcher.matches()) {
String g = matcher.group(1);
String a = matcher.group(2);
String vv = matcher.group(3);
Revision dep = library.getRevision(g + ":" + a + "@" + vv);
if (dep == null) {
f.format("%s%s.%s;version=%s", del, g, a, vv);
} else {
f.format("%s%s;version=%s", del, r.bsn, vv);
}
del = ", \\\n";
} else {
f.format("\n#### %s\n", req.name);
}
}
}
f.format("\n\n");
}
catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
f.format("Sorry, should not have happend :-(\n\n%s\n", sw);
}
finally {
jar.close();
f.close();
an.close();
}
clipboard(f.toString());
}
private Object getCoordinates(Revision r) {
return r.groupId + ":" + r.artifactId + (r.classifier == null ? "" : ":" + r.classifier) + "@" + r.version;
}
private void clipboard(final String string) {
Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(new Transferable() {
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return false;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] {
DataFlavor.stringFlavor
};
}
@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
return string;
}
}, new ClipboardOwner() {
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) {
// what me worry?
}
});
}
/**
* Update all bsns
*
* @throws Exception
*/
void updateAll() throws Exception {
for (String bsn : index.getBsns()) {
update(bsn);
}
}
/**
* Update all baselines for a bsn
*
* @param bsn
* @throws Exception
*/
void update(String bsn) throws Exception {
Program p = library.getProgram(Library.OSGI_GROUP, bsn);
for (Version v : index.getVersions(bsn))
update(p, bsn, v);
}
/**
* Update a revision of there is a higher with the same baseline but higher
* qualifier.
*
* @param p
* @param v
* @throws Exception
*/
void update(Program p, String bsn, Version v) throws Exception {
RevisionRef ref = findUpdate(p, v);
if (ref == null)
return;
index.addRevision(ref);
index.delete(bsn, v);
index.save(indexFile);
}
/**
* Find a RevisionRef from the Program. We are looking for a version with
* the same baseline but a higher qualifier.
*
* @param p
* @param currentVersion
* @return
* @throws Exception
*/
private RevisionRef findUpdate(Program p, Version currentVersion) throws Exception {
String baseline = currentVersion.getWithoutQualifier().toString();
Version candidate = currentVersion;
RevisionRef candidateRef = null;
for (RevisionRef r : p.revisions) {
if (r.baseline.equals(baseline)) {
if (r.phase == Library.Phase.MASTER) {
if (currentVersion.equals(toVersion(r.baseline, r.qualifier)))
return null;
return r;
}
Version refVersion = toVersion(r.baseline, r.qualifier);
if (refVersion.compareTo(candidate) > 0) {
candidate = refVersion;
candidateRef = r;
}
}
}
return candidateRef;
}
public void setIndex(File index) {
indexFile = index;
obrFile = IO.getFile(index.getParentFile(), "cache/" + index.getName() + ".xml");
}
private void success(DownloadListener[] downloadListeners, File f) {
for (DownloadListener l : downloadListeners) {
try {
l.success(f);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
private void failure(DownloadListener[] listeners, File f, String reason) {
for (DownloadListener l : listeners) {
try {
l.failure(f, reason);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
@SuppressWarnings({
"rawtypes", "unchecked"
})
public Map<org.osgi.resource.Requirement,Collection<org.osgi.resource.Capability>> findProviders(
Collection< ? extends org.osgi.resource.Requirement> requirements) {
MultiMap map = new MultiMap<Requirement,List<Capability>>();
Set<org.osgi.resource.Requirement> notFound = new HashSet<org.osgi.resource.Requirement>();
try {
Engine engine = getEngine();
for (org.osgi.resource.Requirement req : requirements) {
List<Capability> providers = engine.findProviders(req);
if (providers.isEmpty()) {
if (Engine.isMandatory(req))
notFound.add(req);
} else
map.put(req, providers);
}
for (org.osgi.resource.Requirement req : notFound) {
System.out.println("Not found " + req);
}
return map;
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@Override
public String title(Object... target) throws Exception {
init();
if (target == null || target.length == 0)
return getName();
if (target.length == 1 && target[0] instanceof String) {
String bsn = (String) target[0];
String title = bsn;
Program p = library.getProgram(Library.OSGI_GROUP, bsn);
if (p != null) {
for (Version version : versions(bsn)) {
RevisionRef findUpdate = findUpdate(p, version);
if (findUpdate != null)
title += SUPERIOR_1; // superscript 1
}
}
return title;
}
if (target.length == 2 && target[0] instanceof String && target[1] instanceof Version) {
String bsn = (String) target[0];
Version version = (Version) target[1];
Library.RevisionRef resource = index.getRevisionRef(bsn, version);
if (resource == null)
return "[deleted " + version + "]";
String title = getPhase(resource.phase.toString()) + " " + version.toString();
File path = cache.getPath(bsn, version, resource.revision);
if (path.isFile() && path.length() == resource.size) {
title += DOWN_ARROW;
}
Program p = library.getProgram(Library.OSGI_GROUP, bsn);
if (p != null) {
RevisionRef findUpdate = findUpdate(p, version);
if (findUpdate != null)
title += SUPERIOR_1; // superscript 1
}
return title;
}
return null;
}
// Temp until we fixed bnd in bndtools
enum Phase {
STAGING(false, false, false, "[s]"), LOCKED(true, false, false, "[l]"), MASTER(true, true, true, "[m]"), RETIRED(
true, false, true, "[r]"), WITHDRAWN(true, false, true, "[x]"), UNKNOWN(true, false, false, "[?]");
boolean locked;
boolean listable;
boolean permanent;
final String symbol;
private Phase(boolean locked, boolean listable, boolean permanent, String symbol) {
this.locked = locked;
this.listable = listable;
this.permanent = permanent;
this.symbol = symbol;
}
public boolean isLocked() {
return locked;
}
public boolean isListable() {
return listable;
}
public boolean isPermanent() {
return permanent;
}
public String getSymbol() {
return symbol;
}
}
private String getPhase(String phase) {
try {
return Phase.valueOf(phase).getSymbol();
}
catch (Exception e) {
return "?";
}
}
@Override
public File getRoot() {
return cache.getRoot();
}
@Override
public void close() throws IOException {}
@Override
public String getLocation() {
return options.location();
}
protected void fireBundleAdded(File file) throws IOException {
if (registry == null)
return;
List<RepositoryListenerPlugin> listeners = registry.getPlugins(RepositoryListenerPlugin.class);
if (listeners.isEmpty())
return;
Jar jar = new Jar(file);
try {
for (RepositoryListenerPlugin listener : listeners) {
try {
listener.bundleAdded(this, jar, file);
}
catch (Exception e) {
reporter.error("Repository listener threw an unexpected exception: %s", e, e);
}
finally {}
}
}
finally {
jar.close();
}
}
@Override
public void setRegistry(Registry registry) {
this.registry = registry;
}
private void init() {
if (index == null) {
reporter.trace("init " + indexFile);
index = new Index(indexFile);
index.setReporter(reporter);
}
}
public void add(String bsn, Version version) throws Exception {
reporter.trace("Add %s %s", bsn, version);
RevisionRef ref = getRevisionRef(bsn, version);
add(ref);
}
void add(RevisionRef ref) throws Exception {
index.addRevision(ref);
if (index.isRecurse()) {
Iterable<RevisionRef> refs = library.getClosure(ref.revision, false);
for (RevisionRef r : refs) {
index.addRevision(r);
}
}
index.save(indexFile);
}
public void delete(String bsn, Version version, boolean immediate) throws Exception {
reporter.trace("Delete %s %s", bsn, version);
Library.RevisionRef resource = index.getRevisionRef(bsn, version);
if (resource != null) {
boolean removed = index.delete(bsn, version);
reporter.trace("Was present " + removed);
index.save();
} else
reporter.trace("No such resource");
}
public void delete(String bsn) throws Exception {
reporter.trace("Delete %s", bsn);
Set<Version> set = new HashSet<Version>(index.getVersions(bsn));
reporter.trace("Versions %s", set);
for (Version version : set) {
delete(bsn, version, true);
}
}
public boolean dropTarget(URI uri) throws Exception {
Matcher m = JPM_REVISION_URL_PATTERN.matcher(uri.toString());
if (!m.matches())
return false;
Revision revision = library.getRevision(m.group(1), m.group(2), m.group(3), m.group(4));
if (revision == null)
return false;
Library.RevisionRef resource = index.getRevisionRef(revision._id);
if (resource != null)
return true;
RevisionRef ref = new RevisionRef(revision);
add(ref);
return true;
}
/*
* A utility to open a URL on different OS's browsers
* @param url the url to open
* @throws IOException
*/
void open(String url) {
try {
try {
Desktop desktop = Desktop.getDesktop();
desktop.browse(new URI(url));
return;
}
catch (Throwable e) {
}
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.indexOf("mac") >= 0 || os.indexOf("darwin") >= 0) {
rt.exec("open " + url);
} else if (os.indexOf("win") >= 0) {
// this doesn't support showing urls in the form of
// "page.html#nameLink"
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0) {
// Do a best guess on unix until we get a platform independent
// way
// Build a list of browsers to try, in this order.
String[] browsers = {
"epiphany", "firefox", "mozilla", "konqueror", "netscape", "opera", "links", "lynx"
};
// Build a command string which looks like
// "browser1 "url" || browser2 "url" ||..."
StringBuffer cmd = new StringBuffer();
for (int i = 0; i < browsers.length; i++)
cmd.append((i == 0 ? "" : " || ") + browsers[i] + " \"" + url + "\" ");
rt.exec(new String[] {
"sh", "-c", cmd.toString()
});
} else
System.out.println("Open " + url);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Get the resources of the current repository.
*
* @return
* @throws Exception
*/
public Engine getEngine() throws Exception {
if (!indexFile.isFile() || obrFile.lastModified() < indexFile.lastModified()) {
RepoIndex indexer = new RepoIndex();
indexer.addAnalyzer(new KnownBundleAnalyzer(), null);
if (registry != null) {
List<ResourceAnalyzer> analyzers = registry.getPlugins(ResourceAnalyzer.class);
for (ResourceAnalyzer analyzer : analyzers) {
// TODO: where to get the filter property??
indexer.addAnalyzer(analyzer, null);
}
}
Map<String,String> config = new HashMap<String,String>();
config.put(ResourceIndexer.REPOSITORY_NAME, indexFile.getName());
config.put(ResourceIndexer.ROOT_URL, "/");
config.put(ResourceIndexer.PRETTY, Boolean.FALSE.toString());
final Set<File> set = new HashSet<File>(index.getRevisionRefs().size());
final Semaphore downloads = new Semaphore(0);
final List<String> errors = new ArrayList<String>();
int count = 0;
for (RevisionRef ref : index.getRevisionRefs()) {
count++;
get(ref.bsn, Index.toVersion(ref), null, new DownloadListener() {
@Override
public void success(File file) throws Exception {
set.add(file);
downloads.release();
}
@Override
public void failure(File file, String reason) throws Exception {
errors.add(file + ": " + reason);
downloads.release();
}
@Override
public boolean progress(File file, int percentage) throws Exception {
return true;
}
});
}
downloads.acquire(count);
if (errors.size() > 0)
// TODO
System.err.println("DOWNLOAD ERRORS!! " + errors);
FileOutputStream fout = new FileOutputStream(obrFile);
try {
indexer.index(set, fout, config);
}
finally {
fout.close();
}
}
// obrFile is up to data
if (engine == null) {
engine = new Engine();
engine.parse(obrFile);
}
return engine;
}
/**
* Answer the resource descriptors from a URL
*/
@Override
public Set<ResourceDescriptor> getResources(URI url, boolean includeDependencies) throws Exception {
Matcher m = JPM_REVISION_URL_PATTERN.matcher(url.toString());
if (!m.matches())
return null;
Set<ResourceDescriptor> resources = new HashSet<ResourceDescriptor>();
Revision revision = library.getRevision(m.group(1), m.group(2), m.group(3), m.group(4));
if (revision != null) {
ResourceDescriptor rd = createResourceDescriptor(new RevisionRef(revision));
resources.add(rd);
if (includeDependencies) {
for (RevisionRef dependency : library.getClosure(revision._id, false)) {
resources.add(createResourceDescriptor(dependency));
}
}
}
return resources;
}
private ResourceDescriptor createResourceDescriptor(RevisionRef ref) throws Exception {
ResourceDescriptorImpl rd = new ResourceDescriptorImpl(ref);
rd.bsn = ref.bsn;
rd.version = toVersion(ref.baseline, ref.qualifier);
rd.description = ref.description;
rd.id = ref.revision;
rd.included = getIndex().getRevisionRef(rd.id) != null;
rd.phase = toPhase(ref.phase);
return rd;
}
private Index getIndex() {
init();
return index;
}
private aQute.bnd.service.repository.Phase toPhase(aQute.service.library.Library.Phase phase) {
switch (phase) {
case STAGING :
return aQute.bnd.service.repository.Phase.STAGING;
case LOCKED :
return aQute.bnd.service.repository.Phase.LOCKED;
case MASTER :
return aQute.bnd.service.repository.Phase.MASTER;
case RETIRED :
return aQute.bnd.service.repository.Phase.RETIRED;
case WITHDRAWN :
return aQute.bnd.service.repository.Phase.WITHDRAWN;
default :
return null;
}
}
@Override
public Set<ResourceDescriptor> query(String query) throws Exception {
Set<ResourceDescriptor> resources = new HashSet<ResourceDescriptor>();
RevisionRef master = null;
RevisionRef staging = null;
for (Program p : library.findProgram().query(query)) {
for (RevisionRef ref : p.revisions) {
if (master == null && ref.phase == Library.Phase.MASTER) {
master = ref;
} else if (staging != null && ref.phase == Library.Phase.STAGING) {
staging = ref;
}
}
if (master != null)
resources.add(createResourceDescriptor(master));
if (staging != null)
resources.add(createResourceDescriptor(staging));
}
return resources;
}
@Override
public boolean addResource(ResourceDescriptor resource) throws Exception {
if (resource instanceof ResourceDescriptorImpl) {
RevisionRef ref = ((ResourceDescriptorImpl) resource).revision;
if (index.addRevision(ref)) {
index.save();
return true;
}
}
return false;
}
@Override
public Set<ResourceDescriptor> findResources(org.osgi.resource.Requirement requirement, boolean includeDependencies)
throws Exception {
throw new UnsupportedOperationException("Not yet");
}
/**
* Check if there is at least one network interface up and running so we
* have internet access.
*/
private boolean isConnected() throws SocketException {
if (offline)
return false;
try {
for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements();) {
NetworkInterface interf = e.nextElement();
if (!interf.isLoopback() && interf.isUp())
return true;
}
}
catch (SocketException e) {
// ignore, we assume we're offline
}
return false;
}
}