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;
	}

}
