package aQute.bnd.jpm; import static aQute.lib.io.IO.copy; import static aQute.libg.slf4j.GradleLogging.LIFECYCLE; import java.awt.Desktop; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.Formatter; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.Semaphore; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import aQute.bnd.build.Container; import aQute.bnd.build.Project; import aQute.bnd.build.Workspace; import aQute.bnd.header.Attrs; import aQute.bnd.http.HttpClient; import aQute.bnd.jpm.StoredRevisionCache.Download; import aQute.bnd.jpm.util.JSONRPCProxy; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Domain; import aQute.bnd.osgi.Jar; import aQute.bnd.osgi.Resource; import aQute.bnd.osgi.Verifier; import aQute.bnd.osgi.resource.FilterParser; import aQute.bnd.service.Actionable; import aQute.bnd.service.Plugin; import aQute.bnd.service.Refreshable; import aQute.bnd.service.Registry; import aQute.bnd.service.RegistryPlugin; import aQute.bnd.service.RepositoryListenerPlugin; import aQute.bnd.service.RepositoryPlugin; import aQute.bnd.service.Strategy; import aQute.bnd.service.repository.InfoRepository; import aQute.bnd.service.repository.SearchableRepository; import aQute.bnd.version.Version; import aQute.jpm.facade.repo.JpmRepo; import aQute.lib.collections.MultiMap; import aQute.lib.collections.SortedList; import aQute.lib.converter.Converter; import aQute.lib.exceptions.Exceptions; import aQute.lib.hex.Hex; import aQute.lib.io.IO; import aQute.lib.justif.Justif; import aQute.lib.settings.Settings; import aQute.libg.cryptography.MD5; import aQute.libg.cryptography.SHA1; import aQute.libg.cryptography.SHA256; import aQute.libg.glob.Glob; import aQute.libg.reporter.ReporterAdapter; import aQute.service.library.Coordinate; import aQute.service.library.Library; import aQute.service.library.Library.Program; import aQute.service.library.Library.Revision; import aQute.service.library.Library.RevisionRef; import aQute.service.library.Revisions; import aQute.service.reporter.Reporter; /** * A bnd repository based on the jpm4j server. */ public class Repository implements Plugin, RepositoryPlugin, Closeable, Refreshable, Actionable, RegistryPlugin, SearchableRepository, InfoRepository { private final static Logger logger = LoggerFactory.getLogger(Repository.class); private static final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); private static final XPathFactory xpf = XPathFactory.newInstance(); public static final String REPO_DEFAULT_URI = "http://repo.jpm4j.org"; private static final PutOptions DEFAULT_OPTIONS = new PutOptions(); private static final String SEARCH_PREFIX = "/#!/search?q="; private static final String UTF_8 = "UTF-8"; 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 final Justif j = new Justif(80, new int[] { 20, 28, 36, 44 }); private Settings settings = new Settings(); private boolean canwrite; final MultiMap<File,DownloadListener> queues = new MultiMap<File,RepositoryPlugin.DownloadListener>(); private final Pattern JPM_REVISION_URL_PATTERN = Pattern .compile("https?://.+#!?/p/([^/]+)/([^/]+)/([^/]*)/([^/]+)"); private Options options; Reporter reporter = new ReporterAdapter(System.out); /** * Maintains the index of what we've downloaded so far. */ private File indexFile; private boolean indexRecurse; Index index; boolean offline; private Registry registry; StoredRevisionCache cachex; Set<File> notfound = new HashSet<File>(); private Set<String> notfoundref = new HashSet<String>(); final Semaphore limitDownloads = new Semaphore(12); private JpmRepo libraryx; private String depositoryGroup; private String depositoryName; private String location; private URI depository; private String email; private String name; private URI url; private HttpClient httpClient = new HttpClient(); /** * Reports downloads but does never block on them. This is a best effort, if * it fails, we can still get them later. */ class LocalDownloadListener implements DownloadListener { @Override public void success(File file) throws Exception { logger.debug("downloaded {}", file); } @Override public void failure(File file, String reason) throws Exception { logger.debug("failed to downloaded {}", file); } @Override public boolean progress(File file, int percentage) throws Exception { logger.debug("Downloading {} {}%", file, percentage); return true; } } interface Options { /** * The URL to the remote repository. Default is http://repo.jpm4j.org * */ URI url(); /** * The group of a depository,optional. * */ String depository_group(); /** * The name of the depository * */ String depository_name(); /** * The email address of the user * */ String email(); /** * Where the index file is stored. The default should reside in the * workspace and be part of the scm * */ String index(); /** * The cache location, default is ~/.bnd/cache. This file is relative * from the users home directory if not absolute. * */ String location(); /** * Set the settings */ String settings(); /** * The name of the repo * */ String name(); /** * Fetch dependencies automatically */ boolean recurse(); boolean trace(); boolean crawl(); } /** * 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) 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 sources = getCache().getPath(resource.bsn, Index.toVersion(resource).toString(), resource.revision, true); if (sources.isFile()) { for (DownloadListener dl : downloadListeners) { dl.success(sources); } return sources; } File file = getCache().getPath(resource.bsn, Index.toVersion(resource).toString(), resource.revision); scheduleDownload(file, resource.revision, resource.size, resource.urls, downloadListeners); return file; } /** * Schedule a download, handling the listeners * * @param url */ private void scheduleDownload(final File file, final byte[] sha, final long size, final Set<URI> urls, 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); logger.debug("was in cache"); return; } reporter.error("found file but of different length %s, will refetch", file); } else { logger.debug("not in cache {} {}", file, queues); } if (!isConnected()) { failure(listeners, file, "Not online"); } // Check if we need synchronous if (listeners.length == 0) { logger.debug("in cache, no listeners"); getCache().download(file, urls, 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. logger.debug("someone else is downloading our file {}", queues.get(file)); return; } } try { logger.debug("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 { logger.debug("downloading in background {}", file); getCache().download(file, urls, 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, Exceptions.toString(e)); } } catch (Throwable e) { e.printStackTrace(); reporter.error("failed to download %s: %s", e, file); synchronized (queues) { failure(queues.get(file).toArray(EMPTY_LISTENER), file, Exceptions.toString(e)); } } finally { synchronized (queues) { queues.remove(file); } logger.debug("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, Exceptions.toString(e)); 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 { if (!canwrite) throw new UnsupportedOperationException("This is not a writeable repo, s" + "et depository.group, depository.name and properties and ensure the email property is in your global settings"); assert in != null; assert depositoryGroup != null; assert depositoryName != null; init(); if (options == null) options = DEFAULT_OPTIONS; logger.debug("syncing"); sync(); File file = File.createTempFile("put", ".jar"); file.deleteOnExit(); try { logger.debug("creating tmp copy"); copy(in, file); if (depository == null) { logger.debug("send to url {}", url); depository = getLibrary().depository(depositoryGroup, depositoryName); logger.debug("credentials {}", depository); } byte[] digest = options.digest == null ? SHA1.digest(file).digest() : options.digest; String path = Hex.toHexString(digest); logger.debug("putting {}", path); URI uri = getDepository(path); Library.RevisionRef d = httpClient.build() .verb("PUT") .upload(file) .get(Library.RevisionRef.class) .go(uri.toURL()); if (d == null) { reporter.error("Cant deposit %s", file); return null; } if (!Arrays.equals(digest, d.revision)) throw new Exception("Invalid digest"); // Copy it to our cache getCache().add(d, file); index.addRevision(d); index.save(); // Coordinator PutResult putr = new PutResult(); putr.artifact = uri; putr.digest = digest; return putr; } catch (Exception e) { e.printStackTrace(); throw e; } finally { IO.delete(file); } } private URI getDepository(String path) throws Exception { if (depository == null) { depository = getLibrary().depository(depositoryGroup, depositoryName); } return new URI(depository + "/" + path); } /** * If we have no search or an empty search we list our index. Otherwise we * query remotely. */ Pattern COMMAND_P = Pattern.compile("^([^/]*)/(!?[lmsprw])([^/]*)$"); private File cacheDir; @Override public List<String> list(String query) throws Exception { init(); Set<String> bsns = new HashSet<String>(); if (query == null || query.trim().isEmpty()) query = "*"; else query = query.trim(); Library.Phase phase = null; boolean negated = false; Matcher m = COMMAND_P.matcher(query); if (m.matches()) { query = m.group(1) + m.group(3); String cmd = m.group(2); if (cmd.startsWith("!")) { negated = true; cmd = cmd.substring(1); } char c = Character.toLowerCase(cmd.charAt(0)); switch (c) { case 'l' : phase = Library.Phase.LOCKED; break; case 'p' : phase = Library.Phase.PENDING; break; case 's' : phase = Library.Phase.STAGING; break; case 'm' : phase = Library.Phase.MASTER; break; case 'r' : phase = Library.Phase.RETIRED; break; case 'w' : phase = Library.Phase.WITHDRAWN; break; } logger.debug("Phase is {} {}", c, phase); } Glob glob = null; try { glob = new Glob(query); } catch (Exception e) { glob = new Glob("*"); } bsn: for (String bsn : index.getBsns()) { if (glob.matcher(bsn).matches()) { if (phase != null) { boolean hasPhase = false; revision: for (Version version : index.getVersions(bsn)) { RevisionRef ref = index.getRevisionRef(bsn, version); if (ref.phase == phase) { hasPhase = true; break revision; } } if (hasPhase == negated) continue bsn; } 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; } return versions; } /* * Convert a baseline/qualifier to a version */ static 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 name == null ? "jpm4j" : name; } @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 { location = options.location(); if (location == null) location = "~/.bnd/shacache"; this.name = options.name(); if (options.settings() != null) { settings = new Settings(options.settings()); } email = options.email(); if (email == null) email = settings.getEmail(); url = options.url(); if (url == null) url = new URI(REPO_DEFAULT_URI); cacheDir = IO.getFile(IO.home, location); IO.mkdirs(cacheDir); if (!cacheDir.isDirectory()) throw new IllegalArgumentException("Not able to create cache directory " + cacheDir); String indexPath = options.index(); if (indexPath == null) throw new IllegalArgumentException("Index file not set (index) "); indexFile = IO.getFile(indexPath); if (indexFile.isDirectory()) throw new IllegalArgumentException( "Index file is a directory instead of a file " + indexFile.getAbsolutePath()); indexRecurse = options.recurse(); if (options.index() == null) throw new IllegalArgumentException("Index file not set"); canwrite = false; if (options.depository_group() != null) { depositoryGroup = options.depository_group(); depositoryName = options.depository_name(); if (depositoryName == null) depositoryName = "home"; canwrite = email != null; } crawl = options.crawl(); } catch (Exception e) { if (reporter != null) reporter.exception(e, "Creating options"); throw Exceptions.duck(e); } } @Override public void setReporter(Reporter processor) { reporter = processor; if (index != null) index.setReporter(reporter); reporter.warning( "%s is deprecated as of Bnd 3.4 and support will be removed in Bnd 4.0. Please change to another repository type.", getClass().getName()); } @Override public boolean refresh() throws Exception { index = new Index(indexFile); index.setRecurse(indexRecurse); getCache().refresh(); notfound.clear(); notfoundref.clear(); if (crawler != null) crawler.refresh(); return true; } /** * Return the actions for this repository */ @Override public Map<String,Runnable> actions(Object... target) throws Exception { init(); boolean connected = isConnected(); if (target == null) return null; if (target.length == 0) return getRepositoryActions(); final String bsn = (String) target[0]; Program careful = null; if (connected) try { careful = getProgram(bsn, true); } 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; } static Pattern JAR_FILE_P = Pattern.compile("(https?:.+)(\\.jar)"); private Map<String,Runnable> getRevisionActions(final Program program, 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(url + "#!/p/sha/" + Hex.toHexString(resource.revision) + "//0.0.0"); } }); map.put("Copy reference", new Runnable() { @Override public void run() { toClipboard(bsn, version); } }); Runnable doUpdate = getUpdateAction(program, resource); if (doUpdate != null) { map.put("Update to " + doUpdate, doUpdate); } else { map.put("-Update", null); } map.put("Delete", new Runnable() { public void run() { try { delete(bsn, version, true); } catch (Exception e) { e.printStackTrace(); } } }); if (isConnected()) { final File sourceFile = getCache().getPath(bsn, version.toString(), resource.revision, true); Runnable run = null; if (!sourceFile.isFile()) { URL sourceURI = null; for (URI uri : resource.urls) { try { Matcher m = JAR_FILE_P.matcher(uri.toString()); if (m.matches()) { String stem = m.group(1); URL src = new URL(stem + "-sources.jar"); HttpURLConnection conn = (HttpURLConnection) src.openConnection(); conn.setRequestMethod("HEAD"); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { sourceURI = src; continue; } } } catch (Exception e) { // ignore } } if (sourceURI != null) { run = createAddSourceAction(bsn, version, resource, sourceFile, sourceURI); } } else logger.debug("sources in {}", sourceFile); if (run != null) map.put("Add Sources", run); else map.put("-Add Sources", null); } if (getCache().hasSources(bsn, version.toString(), resource.revision)) { map.put("Remove Sources", new Runnable() { @Override public void run() { try { getCache().removeSources(bsn, version.toString(), resource.revision); } catch (Exception e) { throw new RuntimeException(e); } } }); } return map; } /** * @param bsn * @param version * @param resource * @param withSources * @param src */ protected Runnable createAddSourceAction(final String bsn, final Version version, final Library.RevisionRef resource, final File withSources, final URL src) { Runnable run; run = new Runnable() { public void run() { try { // Sync downloads so that we do not assume the // binary is already there ... so call without // listeners. get(bsn, version, null); File file = getCache().getPath(bsn, version.toString(), resource.revision); try (Jar binary = new Jar(file); Jar sources = new Jar(src.getFile(), src.openStream())) { binary.setDoNotTouchManifest(); binary.addAll(sources, null, "OSGI-OPT/src"); binary.write(withSources); } } catch (Exception e) { throw new RuntimeException(e); } } }; return run; } /** * @param bsn * @param version * @param resource * @param withSources * @param src */ protected Runnable createRemoveSourceAction(final String bsn, final Version version, final Library.RevisionRef resource, final File withSources, final URL src) { Runnable run; run = new Runnable() { public void run() { try { // Sync downloads so that we do not assume the // binary is already there ... so call without // listeners. get(bsn, version, null); File file = getCache().getPath(bsn, version.toString(), resource.revision); try (Jar binary = new Jar(file); Jar sources = new Jar(src.getFile(), src.openStream())) { binary.addAll(sources, null, "OSGI-OPT/src"); binary.write(withSources); } } catch (Exception e) { throw Exceptions.duck(e); } } }; return run; } /** * @param bsn * @param p * @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) { map.put("Inspect Program", new Runnable() { public void run() { open(url + "#!/p/osgi/" + bsn); } }); final SortedSet<Version> versions = index.getVersions(bsn); if (versions.isEmpty()) map.put("-Copy reference", null); else map.put("Copy reference", new Runnable() { @Override public void run() { toClipboard(bsn, versions.first()); } }); 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 + ref.phase; map.put(title, new Runnable() { public void run() { try { add(bsn, l); } catch (Exception e) { throw new RuntimeException(e); } } }); Runnable updateAction = getUpdateAction(p, bsn); if (updateAction != null) map.put("Update " + updateAction, updateAction); else map.put("-Update", null); } else { map.put("-Update (offline)", null); } map.put("Delete", new Runnable() { public void run() { try { delete(bsn); } catch (Exception e) { throw new RuntimeException(e); } } }); return map; } /** * @throws Exception */ private Map<String,Runnable> getRepositoryActions() throws Exception { Map<String,Runnable> map = new LinkedHashMap<String,Runnable>(); if (offline) { if (isConnected()) { map.put("Try Online", new Runnable() { public void run() { offline = false; } }); } } else { map.put("Go Offline", new Runnable() { public void run() { offline = true; } }); } map.put("Inspect", new Runnable() { public void run() { try { byte[] revisions = sync(); open(url + "#!/revisions/" + Hex.toHexString(revisions)); } catch (Exception e) { throw new RuntimeException(e); } } }); map.put("Delete Cache", new Runnable() { @Override public void run() { try { getCache().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); } } }); map.put("Download All", new Runnable() { @Override public void run() { try { DownloadListener dl = new DownloadListener() { @Override public void success(File file) throws Exception { logger.debug("downloaded {}", file); } @Override public void failure(File file, String reason) throws Exception { logger.debug("failed to download {} becasue {}", file, reason); } @Override public boolean progress(File file, int percentage) throws Exception { logger.info(LIFECYCLE, "[{}] downloading {}", percentage / 100, file); return true; } }; for (String bsn : list(null)) { for (Version v : versions(bsn)) { get(bsn, v, null, dl); } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); map.put("Remove unused/Add missing", new Runnable() { @Override public void run() { try { cleanUp(); } catch (Exception e) { e.printStackTrace(); } } }); 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 { try (Formatter f = new Formatter()) { f.format("%s\n", this); if (depositoryGroup != null && depositoryName != null) { f.format("\n[Depository]\n"); f.format("Group: %s\n", depositoryGroup); f.format("Depository: %s\n", depositoryName); f.format("Email: %s\n", email); f.format("Writable: %s %s\n", canwrite, (email == null ? "(no email set, see 'bnd settings email=...')" : "")); f.format("Public key: %s…\n", Hex.toHexString(settings.getPublicKey()).substring(0, 16)); } f.format("\n[Files]\nCache 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()); f.format("Dirty %s\n", index.isDirty()); return f.toString().trim(); } } private String programTooltip(String bsn) throws Exception { Program p = getProgram(bsn, false); if (p != null) { try (Formatter sb = new Formatter()) { 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"); if (bsn.indexOf("__") >= 0) { sb.format("\nThis artifact has no OSGi metadata. Its coordinates are %s:%s\n", p.groupId, p.artifactId); } j.wrap((StringBuilder) sb.out()); return sb.toString().trim(); } } return null; } private String revisionTooltip(String bsn, Version version) throws Exception { RevisionRef r = getRevisionRef(bsn, version); if (r == null) return null; try (Formatter sb = new Formatter()) { sb.format("[%s:%s", r.groupId, r.artifactId); if (r.classifier != null) { sb.format(":%s", r.classifier); } sb.format("@%s] %s\n\n", r.version, r.phase); 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\n", size(r.size, 0)); sb.format("SHA-1: %s\n", Hex.toHexString(r.revision)); sb.format("Age: %s\n", age(r.created)); sb.format("URL: %s\n", r.urls); File f = getCache().getPath(bsn, version.toString(), r.revision); if (f.isFile() && f.length() == r.size) sb.format("Cached %s\n", f); else sb.format("Not downloaded\n"); if (bsn.indexOf("__") >= 0) { sb.format("\nThis artifact has no OSGi metadata. Its coordinates are %s:%s@%s\n", r.groupId, r.artifactId, r.version); } Program p = getProgram(bsn, false); if (p != null) { Runnable update = getUpdateAction(p, r); if (update != null) { sb.format("%c This version can be updated to %s\n", DOWN_ARROW, update); } } File sources = getCache().getPath(bsn, version.toString(), r.revision, true); if (sources.isFile()) sb.format("Has sources: %s\n", sources.getAbsolutePath()); else sb.format("No sources\n"); j.wrap((StringBuilder) sb.out()); return sb.toString().trim(); } } 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 = getProgram(bsn, false); 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 * @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 = getRevision(new Coordinate(bsn)); if (r == null) return null; return new RevisionRef(r); } logger.debug("Looking for {}-{}", 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 Crawler crawler; private boolean crawl; 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); } /** * Update all bsns * * @throws Exception */ void updateAll() throws Exception { for (String bsn : new ArrayList<String>(index.getBsns())) { update(bsn); } } /** * Update all baselines for a bsn * * @param bsn * @throws Exception */ void update(String bsn) throws Exception { Program program = getProgram(bsn, false); Runnable updateAction = getUpdateAction(program, bsn); if (updateAction == null) return; logger.debug("update bsn {}", updateAction); updateAction.run(); } /** * Update a bsn * * @throws Exception */ Runnable getUpdateAction(Program program, String bsn) throws Exception { final List<Runnable> update = new ArrayList<Runnable>(); for (Version v : index.getVersions(bsn)) { RevisionRef resource = index.getRevisionRef(bsn, v); Runnable updateAction = getUpdateAction(program, resource); if (updateAction != null) update.add(updateAction); } if (update.isEmpty()) return null; return new Runnable() { @Override public void run() { for (Runnable r : update) { r.run(); } } @Override public String toString() { return update.toString(); } }; } /** * Find a RevisionRef from the Program. We are looking for a version with * the same baseline but a higher qualifier or different phase. * * @param p * @param currentVersion * @throws Exception */ private Runnable getUpdateAction(Program program, final RevisionRef current) throws Exception { RevisionRef candidateRef = null; Version candidate = toVersion(current.baseline, current.qualifier); for (RevisionRef r : program.revisions) { Version refVersion = toVersion(r.baseline, r.qualifier); if (eq(r.classifier, current.classifier)) { if (refVersion.compareTo(candidate) >= 0) { candidate = refVersion; candidateRef = r; } } } if (candidateRef == null) // // We're not present anymore, should never happen ... // return new Runnable() { @Override public void run() { try { index.delete(current.bsn, toVersion(current.baseline, current.qualifier)); } catch (Exception e) { throw new RuntimeException(e); } } public String toString() { return "[delete]"; } }; // // Check if we are not same revision // if (!candidateRef.version.equals(current.version)) { final RevisionRef toAdd = candidateRef; return new Runnable() { // // Replace the current version // public void run() { try { index.delete(current.bsn, toVersion(current.baseline, current.qualifier)); index.addRevision(toAdd); index.save(); } catch (Exception e) { throw new RuntimeException(e); } } public String toString() { return toAdd.version; } }; } // // So now we are the same, check if the phase has changed // if (candidateRef.phase != current.phase) { final RevisionRef toChange = candidateRef; return new Runnable() { @Override public void run() { try { index.delete(current.bsn, toVersion(current.baseline, current.qualifier)); index.addRevision(toChange); index.save(); } catch (Exception e) { throw new RuntimeException(e); } } public String toString() { return "-> " + toChange.phase; } }; } return null; } public void setIndex(File index) { indexFile = index; } void success(DownloadListener[] downloadListeners, File f) { for (DownloadListener l : downloadListeners) { try { l.success(f); } catch (Exception e) { e.printStackTrace(); } } } void failure(DownloadListener[] listeners, File f, String reason) { for (DownloadListener l : listeners) { try { l.failure(f, reason); } catch (Exception e) { e.printStackTrace(); } } } @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; if (bsn.indexOf("__") > 0) title += " [!]"; 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 = getCache().getPath(bsn, version.toString(), resource.revision); if (path.isFile() && path.length() == resource.size) { title += DOWN_ARROW; } if (getCache().getPath(bsn, version.toString(), resource.revision, true).isFile()) title += "+"; 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() throws Exception { return getCache().getRoot(); } @Override public void close() throws IOException { if (crawler != null) crawler.close(); } @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; try (Jar jar = new Jar(file)) { for (RepositoryListenerPlugin listener : listeners) { try { listener.bundleAdded(this, jar, file); } catch (Exception e) { reporter.error("Repository listener threw an unexpected exception: %s", e, e); } } } } @Override public void setRegistry(Registry registry) { this.registry = registry; this.httpClient = registry.getPlugin(HttpClient.class); } private void init() throws Exception { if (index == null) { logger.debug("init {}", indexFile); index = new Index(indexFile); index.setRecurse(indexRecurse); index.setReporter(reporter); if (crawl == true) { crawler = new Crawler(this); crawler.start(); } } } public void add(String bsn, Version version) throws Exception { logger.debug("Add {} {}", bsn, version); RevisionRef ref = getRevisionRef(bsn, version); add(ref); } void add(RevisionRef ref) throws Exception { // Cleanup existing versions // We remove everything between [mask(v), v) Version newVersion = toVersion(ref.baseline, ref.qualifier); logger.debug("New version {} {}", ref.bsn, newVersion); Version newMask = mask(newVersion); List<Version> toBeDeleted = new ArrayList<Version>(); for (Version existingVersion : index.getVersions(ref.bsn)) { Version existingMask = mask(existingVersion); if (newMask.equals(existingMask)) { logger.debug("delete {}-{}", ref.bsn, existingVersion); toBeDeleted.add(existingVersion); } } for (Version v : toBeDeleted) index.delete(ref.bsn, v); logger.debug("add {}-{}", ref.bsn, newVersion); index.addRevision(ref); getLocal(ref, null, new LocalDownloadListener()); if (index.isRecurse()) { Iterable<RevisionRef> refs = getClosure(ref); for (RevisionRef r : refs) { index.addRevision(r); getLocal(ref, null, new LocalDownloadListener()); } } index.save(); } /** * @param ref * @throws Exception */ private Iterable<RevisionRef> getClosure(RevisionRef ref) throws Exception { return getLibrary().getClosure(ref.revision, false); } public void delete(String bsn, Version version, boolean immediate) throws Exception { logger.debug("Delete {} {}", bsn, version); Library.RevisionRef resource = index.getRevisionRef(bsn, version); if (resource != null) { boolean removed = index.delete(bsn, version); logger.debug("Was present {}", removed); index.save(); } else logger.debug("No such resource"); } public void delete(String bsn) throws Exception { logger.debug("Delete {}", bsn); Set<Version> set = new HashSet<Version>(index.getVersions(bsn)); logger.debug("Versions {}", set); for (Version version : set) { delete(bsn, version, true); } } public boolean dropTarget(URI uri) throws Exception { try { init(); // // On Linux we seem to get some spurious text. One case it added the // text of the version after the URI. So we remove anything after // the new line // String t = uri.toString().trim(); int n = t.indexOf('\n'); if (n > 0) { uri = new URI(t.substring(0, n)); logger.debug("dropTarget cleaned up from {} to {}", t, uri); } RevisionRef ref; logger.debug("dropTarget {}", uri); String uriString = uri.toString(); Matcher m = JPM_REVISION_URL_PATTERN.matcher(uriString); if (!m.matches()) { // // If we're connected to a depository we should go through // bndtools' import facility // if (depositoryGroup != null || depositoryName != null) return false; if (!Boolean.getBoolean("jpm4j.in.test") && uri.getScheme().equalsIgnoreCase("file")) return false; // // See if it is a bundle // Download d = getCache().doDownload(uri); if (d == null) { return false; } ref = analyze(d.tmp, uri); if (ref == null) { logger.debug("not a proper url to drop {}", uri); IO.delete(d.tmp); return false; } getCache().makePermanent(ref, d); } else { Revision revision = getRevision(new Coordinate(m.group(1), m.group(2), m.group(3), m.group(4))); if (revision == null) { reporter.error("no revision found for %s", uri); return false; } ref = new RevisionRef(revision); } Library.RevisionRef resource = index.getRevisionRef(ref.revision); if (resource != null) { resource.urls.add(uri); // we know that we modified a resource so the index is dirty index.save(true); logger.debug("resource already loaded {}", uri); return true; } logger.debug("adding revision {}", ref); add(ref); return true; } catch (Exception e) { e.printStackTrace(); throw e; } } /** * We have a URI that potentially could be a JAR. We download it and analyze * it. If it looks like a bndle or JAR, we try to guess the different parts * from it and return a ReveisionRef. * * @param uri the potential URI to a bundle/jar * @return null or a RevisionRef describing the bundle/jar * @throws IOException * @throws IllegalArgumentException */ private RevisionRef analyze(File file, URI uri) throws IllegalArgumentException, IOException { try { try (Jar jar = new Jar(file)) { Manifest manifest = jar.getManifest(); if (manifest == null) { logger.debug("Jar {} has no manifest", uri); return null; } Domain domain = Domain.domain(manifest); RevisionRef ref = new RevisionRef(); ref.created = System.currentTimeMillis(); ref.md5 = MD5.digest(file).digest(); ref.revision = SHA1.digest(file).digest(); ref.phase = Library.Phase.MASTER; ref.size = file.length(); ref.urls.add(uri); Entry<String,Attrs> bsn = domain.getBundleSymbolicName(); if (bsn != null) { ref.bsn = bsn.getKey(); ref.name = domain.get(Constants.BUNDLE_SYMBOLICNAME); ref.version = domain.getBundleVersion(); ref.description = domain.get(Constants.BUNDLE_DESCRIPTION); ref.groupId = "osgi"; ref.artifactId = ref.bsn; } // Try maven try { Map<String,Resource> map = jar.getDirectories().get("META-INF/maven"); if (map.size() != 1) { return ref; } ref.groupId = map.keySet().iterator().next(); map = jar.getDirectories().get("META-INF/maven/" + ref.groupId); if (map.size() != 1) { return ref; } ref.artifactId = map.keySet().iterator().next(); if (ref.bsn == null) { ref.bsn = ref.groupId + "__" + ref.artifactId; } Resource r = jar.getResource("META-INF/maven/" + ref.groupId + "/" + ref.artifactId + "/pom.xml"); if (r != null) { DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(r.openInputStream()); XPath xp = xpf.newXPath(); if (ref.description == null) { ref.description = xp.evaluate("//description", doc); } if (ref.version == null) { ref.version = xp.evaluate("//version", doc); } if (ref.name == null) { ref.name = xp.evaluate("//name", doc); } ref.packaging = xp.evaluate("//packaging", doc); ref.classifier = xp.evaluate("//classifier", doc); } } catch (Exception e) { logger.debug("parsing maven failed for {}: {}", uri, e); } if (ref.version == null) ref.version = "0"; if (Verifier.isVersion(ref.version)) { Version version = new Version(ref.version); ref.baseline = version.getWithoutQualifier().toString(); ref.qualifier = version.getQualifier(); } if (ref.bsn == null) { Pattern JAR_URI_P = Pattern.compile(".*/([^/]+)(?:\\.jar)?", Pattern.CASE_INSENSITIVE); Matcher m = JAR_URI_P.matcher(uri.toString()); if (m.matches()) { ref.bsn = m.group(1); } else ref.bsn = "unknown"; } return ref; } } catch (Exception e) { logger.debug("Could not parse JAR {}: {}", uri, e); } return null; } /* * 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 logger.debug("Open {}", url); } catch (Exception e) { throw new RuntimeException(e); } } /** * 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 = getRevision(new Coordinate(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 : getLibrary().getClosure(revision._id, false)) { ResourceDescriptor dep = createResourceDescriptor(dependency); dep.dependency = true; resources.add(dep); } } } 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); rd.url = ref.urls.isEmpty() ? null : ref.urls.iterator().next(); File f = get(rd.bsn, rd.version, null); if (f != null) rd.sha256 = SHA256.digest(f).digest(); else rd.sha256 = SHA256.digest(new byte[0]).digest(); return rd; } private Index getIndex() throws Exception { 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 : getLibrary().getQueryPrograms(query, 0, 100)) { 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 { FilterParser fp = new FilterParser(); aQute.bnd.osgi.resource.FilterParser.Expression expression = fp .parse(requirement.getDirectives().get("filter")); String query = expression.query(); if (query == null) { return Collections.emptySet(); } return query(query); } @Override public URI browse(String searchString) throws Exception { if (searchString == null) return url; return url.resolve(SEARCH_PREFIX + URLEncoder.encode(searchString, UTF_8)); } /** * 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; } /** * @param bsn * @throws Exception */ private Program getProgram(final String bsn, boolean force) throws Exception { Program p = getCache().getProgram(bsn); if (p == null || force) { p = getLibrary().getProgram(Library.OSGI_GROUP, bsn); if (p != null) getCache().putProgram(bsn, p); } return p; } /** * @param sha * @throws Exception */ private Revision getRevision(Coordinate c) throws Exception { return getLibrary().getRevisionByCoordinate(c); } public byte[] getDigest() throws Exception { init(); return index.getRevisions()._id; } /** * Ensure that the revisions is updated * * @throws Exception */ byte[] sync() throws Exception { Revisions revisions = index.getRevisions(); if (!index.isSynced()) { logger.debug("Syncing repo indexes"); getLibrary().createRevisions(revisions); index.setSynced(revisions._id); } return revisions._id; } /** * Compare a list of versions against the available versions and return the * desired list. This will remove all staged version that are 'below' a * master. */ public SortedSet<Version> update(SortedSet<Version> input, Program p) throws Exception { Map<Version,Version> mapped = new HashMap<Version,Version>(); for (RevisionRef ref : p.revisions) { Version a = toVersion(ref.baseline, ref.qualifier); Version mask = mask(a); Version highest = mapped.get(mask); if (highest == null || a.compareTo(highest) > 0 || ref.phase == Library.Phase.MASTER) mapped.put(mask, a); } HashSet<Version> output = new HashSet<Version>(); for (Version i : input) { Version mask = mask(i); Version found = mapped.get(mask); if (found != null) output.add(found); else reporter.error("[update] Missing version %s for bsn %s", mask, p.last.bsn); } return new SortedList<Version>(output); } private static Version mask(Version in) { return new Version(in.getMajor(), in.getMinor()); } /** * Remove any unused entries in this repository * * @throws Exception */ void cleanUp() throws Exception { Workspace workspace = registry.getPlugin(Workspace.class); Set<Container> set = new HashSet<Container>(); for (Project project : workspace.getAllProjects()) { set.addAll(project.getBuildpath()); set.addAll(project.getRunbundles()); set.addAll(project.getRunpath()); set.addAll(project.getTestpath()); set.addAll(project.getBootclasspath()); set.addAll(project.getClasspath()); // // This should be replaced with project.getRunfw() // String s = project.getProperty(Constants.RUNFW); List<Container> bundles = project.getBundles(Strategy.HIGHEST, s, Constants.RUNFW); set.addAll(bundles); File base = project.getBase(); for (File sub : base.listFiles()) { if (sub.getName().endsWith(".bndrun")) { try (Project bndrun = new Project(workspace, base, sub)) { set.addAll(bndrun.getRunbundles()); set.addAll(bndrun.getRunpath()); set.addAll(bndrun.getTestpath()); set.addAll(bndrun.getBootclasspath()); set.addAll(bndrun.getClasspath()); } } } } Set<RevisionRef> refs = new HashSet<RevisionRef>(index.getRevisionRefs()); Set<RevisionRef> keep = new HashSet<RevisionRef>(); for (Container libOrRev : set) { for (Container c : libOrRev.getMembers()) { logger.debug("Dependency {}", c); if (!Verifier.isVersion(c.getVersion())) continue; RevisionRef ref = index.getRevisionRef(c.getBundleSymbolicName(), new Version(c.getVersion())); if (ref != null) refs.remove(ref); else { // missing! logger.debug("Missing {}", c.getBundleSymbolicName()); Coordinate coord = new Coordinate(c.getBundleSymbolicName()); Revision rev = getLibrary().getRevisionByCoordinate(coord); if (rev != null) { index.addRevision(new RevisionRef(rev)); } else System.out.printf("not found %s\n", c); } keep.add(ref); } } for (RevisionRef ref : refs) { index.delete(ref.bsn, Index.toVersion(ref)); } index.save(); } /** * Get a Resource Descriptor for a given bsn/version * * @param bsn * @param version * @throws Exception */ public ResourceDescriptor getDescriptor(String bsn, Version version) throws Exception { init(); RevisionRef revisionRef = index.getRevisionRef(bsn, version); if (revisionRef == null) return null; return createResourceDescriptor(revisionRef); } /** * Copy a string to the clipboard */ void toClipboard(String bsn, Version base) { Version nextMajor = new Version(base.getMajor() + 1, 0, 0); toClipboard(bsn + ";version='[" + base.getWithoutQualifier() + "," + nextMajor + ")'"); } void toClipboard(String s) { if (s == null) return; StringSelection stringSelection = new StringSelection(s); Clipboard clpbrd = Toolkit.getDefaultToolkit().getSystemClipboard(); clpbrd.setContents(stringSelection, null); } @Override public String toString() { byte[] digest; try { digest = getDigest(); } catch (Exception e) { throw new RuntimeException(e); } return "JpmRepository [writable=" + canWrite() + ", " + (getName() != null ? "name=" + getName() + ", " : "") + (getLocation() != null ? "location=" + getLocation() + ", " : "") + (digest != null ? "digest=" + Hex.toHexString(digest) : "") + "]"; } public JpmRepo getLibrary() throws URISyntaxException, Exception { if (libraryx == null) { libraryx = JSONRPCProxy.createRPC(JpmRepo.class, httpClient, new URI(url.toString() + "/" + JSONRPCProxy.JSONRPC_2_0 + "jpm")); } return libraryx; } /** * @return the cache * @throws Exception */ private StoredRevisionCache getCache() throws Exception { if (cachex == null) { cachex = new StoredRevisionCache(cacheDir, settings, httpClient); } return cachex; } }