package aQute.bnd.jpm;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import aQute.bnd.http.HttpClient;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.json.JSONCodec;
import aQute.lib.settings.Settings;
import aQute.service.library.Library.Program;
import aQute.service.library.Library.RevisionRef;
public class StoredRevisionCache {
private final File root;
final File tmpdir;
final File repodir;
final File programdir;
final Settings settings;
boolean sign;
final Map<File,Exception> errors = new HashMap<File,Exception>();
private Map<String,Program> programs = new HashMap<String,Program>();
private static final JSONCodec codec = new JSONCodec();
File refresh;
private HttpClient httpc = new HttpClient();
public StoredRevisionCache(File root, Settings settings, HttpClient client) throws Exception {
if (client != null)
this.httpc = client;
this.root = root;
this.settings = settings;
IO.mkdirs(this.getRoot());
this.tmpdir = new File(root, "tmp");
IO.mkdirs(this.tmpdir);
this.repodir = new File(root, "shas");
IO.mkdirs(this.repodir);
this.programdir = new File(root, "programs");
IO.mkdirs(this.programdir);
this.refresh = new File(root, ".refreshed");
if (!this.refresh.isFile())
this.refresh.createNewFile();
}
/*
* We create a path that ends in a readable name since Eclipse shows the
* last segement. The name is superfluous
*/
public File getPath(String bsn, String version, byte[] sha) {
return getPath(bsn, version, sha, false);
}
public File getPath(String bsn, String version, byte[] sha, boolean withsource) {
if (withsource)
return IO.getFile(repodir, Hex.toHexString(sha) + "/+" + bsn + "-" + version + ".jar");
else
return IO.getFile(repodir, Hex.toHexString(sha) + "/" + bsn + "-" + version + ".jar");
}
/*
* Delete a revision
*/
public void delete(byte[] sha) {
File f = IO.getFile(getRoot(), Hex.toHexString(sha));
IO.delete(f);
}
static class Download {
File tmp;
byte[] md5;
byte[] sha;
URI uri;
}
/*
* Actually download a file. First in a temp directory, then it is renamed
*/
public void download(final File file, final Set<URI> urls, byte[] sha) throws Exception {
if (errors.containsKey(file))
throw errors.get(file);
if (urls.isEmpty())
throw new Exception("No URLs to download " + file);
for (URI url : urls) {
// Fixup for older bug that put the file url in the urls :-(
Download d = null;
try {
d = doDownload(url);
if (d == null)
continue;
if (!Arrays.equals(sha, d.sha))
throw new Exception("Shas did not match (expected)" + Hex.toHexString(sha) + " (downloaded)" + d.tmp
+ " (" + Hex.toHexString(d.sha) + ")");
IO.mkdirs(file.getParentFile());
IO.rename(d.tmp, file);
return;
} catch (Exception e) {
if (d != null)
IO.delete(d.tmp);
errors.put(file, e);
throw e;
} finally {
if (d != null)
IO.delete(d.tmp);
}
}
throw new FileNotFoundException(urls.toString());
}
/*
* Download an URI into a temporary file while calculating SHA & MD5. The
* connection uses the normal protections
*/
Download doDownload(URI url) throws Exception {
InputStream connect = httpc.connect(url.toURL());
if (connect == null)
return null;
Download d = new Download();
d.tmp = IO.createTempFile(tmpdir, "tmp", ".tmp");
MessageDigest sha = MessageDigest.getInstance("SHA1");
MessageDigest md5 = MessageDigest.getInstance("MD5");
DigestInputStream shaIn = new DigestInputStream(connect, sha);
DigestInputStream md5In = new DigestInputStream(shaIn, md5);
IO.copy(md5In, d.tmp);
d.sha = sha.digest();
d.md5 = md5.digest();
return d;
}
public void add(RevisionRef d, File file) throws IOException {
File path = getPath(d.bsn, d.version, d.revision);
IO.mkdirs(path.getParentFile());
IO.copy(file, path);
long modified = file.lastModified();
if (modified > 0)
path.setLastModified(modified);
}
public File getRoot() {
return root;
}
public void refresh() {
errors.clear();
refresh.setLastModified(System.currentTimeMillis());
}
public void deleteAll() throws IOException {
IO.delete(root);
IO.mkdirs(root);
}
public Program getProgram(String bsn) {
Program p = programs.get(bsn);
if (p != null)
return p;
File pf = IO.getFile(programdir, bsn + ".json");
if (pf != null && pf.isFile() && pf.lastModified() >= refresh.lastModified()) {
try {
p = codec.dec().from(pf).get(Program.class);
programs.put(bsn, p);
return p;
} catch (Exception e) {
//
return null;
}
} else
return null;
}
public void putProgram(String bsn, Program p) throws IOException, Exception {
programs.put(bsn, p);
File pf = IO.getFile(root, bsn + ".json");
codec.enc().to(pf).put(p);
}
/**
* Check if a revision has sources ...
*
*/
public boolean hasSources(String bsn, String version, byte[] sha) {
return getPath(bsn, version, sha, true).isFile();
}
/**
* Remove the sources from the cache.
*
* @param bsn
* @param version
* @param sha
*/
public void removeSources(String bsn, String version, byte[] sha) {
IO.delete(getPath(bsn, version, sha, true));
}
/*
* After we used download, we need to create the file in the cache area
*/
public void makePermanent(RevisionRef ref, Download d) throws Exception {
File f = getPath(ref.bsn, ref.version, ref.revision);
IO.mkdirs(f.getParentFile());
IO.rename(d.tmp, f);
}
}