package aQute.bnd.repository.maven.provider;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Manifest;
import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
import org.osgi.util.promise.Failure;
import org.osgi.util.promise.Promise;
import org.osgi.util.promise.Success;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.bnd.annotation.plugin.BndPlugin;
import aQute.bnd.build.Project;
import aQute.bnd.build.Workspace;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.http.HttpClient;
import aQute.bnd.maven.PomResource;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.osgi.repository.BaseRepository;
import aQute.bnd.repository.maven.provider.IndexFile.BundleDescriptor;
import aQute.bnd.repository.maven.provider.ReleaseDTO.JavadocPackages;
import aQute.bnd.repository.maven.provider.ReleaseDTO.ReleaseType;
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.maven.PomOptions;
import aQute.bnd.service.maven.ToDependencyPom;
import aQute.bnd.service.release.ReleaseBracketingPlugin;
import aQute.bnd.version.Version;
import aQute.lib.converter.Converter;
import aQute.lib.exceptions.Exceptions;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.libg.cryptography.SHA1;
import aQute.libg.glob.Glob;
import aQute.maven.api.Archive;
import aQute.maven.api.IMavenRepo;
import aQute.maven.api.IPom;
import aQute.maven.api.Release;
import aQute.maven.api.Revision;
import aQute.maven.provider.MavenBackingRepository;
import aQute.maven.provider.MavenRepository;
import aQute.maven.provider.PomGenerator;
import aQute.service.reporter.Reporter;
/**
* This is the Bnd repository for Maven.
*/
@BndPlugin(name = "MavenBndRepository")
public class MavenBndRepository extends BaseRepository implements RepositoryPlugin, RegistryPlugin, Plugin, Closeable,
Refreshable, Actionable, ToDependencyPom, ReleaseBracketingPlugin {
private final static Logger logger = LoggerFactory.getLogger(MavenBndRepository.class);
private static final String NONE = "NONE";
static final String MAVEN_REPO_LOCAL = System.getProperty("maven.repo.local",
"~/.m2/repository");
private Configuration configuration;
private Registry registry;
private File localRepo;
private Reporter reporter;
IMavenRepo storage;
private boolean inited;
private boolean ok = true;
IndexFile index;
private ScheduledFuture< ? > indexPoller;
private RepoActions actions = new RepoActions(this);
private String name;
private HttpClient client;
private ReleasePluginImpl releasePlugin = new ReleasePluginImpl(this, null);
static class LocalPutResult extends PutResult {
Archive binaryArchive;
PutOptions options;
String failed;
public Archive pomArchive;
}
@Override
public PutResult put(InputStream stream, PutOptions options) throws Exception {
init();
File binaryFile = File.createTempFile("put", ".jar");
File pomFile = File.createTempFile("pom", ".xml");
LocalPutResult result = new LocalPutResult();
try {
if (options == null)
options = new PutOptions();
else {
result.options = options;
}
IO.copy(stream, binaryFile);
if (options.digest != null) {
byte[] digest = SHA1.digest(binaryFile).digest();
if (!Arrays.equals(options.digest, digest))
throw new IllegalArgumentException("The given sha-1 does not match the contents sha-1");
}
if (options.context == null) {
options.context = registry.getPlugin(Workspace.class);
if (options.context == null)
options.context = new Processor();
}
ReleaseDTO instructions = getReleaseDTO(options.context);
try (Jar binary = new Jar(binaryFile)) {
Resource pomResource;
if (instructions.pom.path != null) {
File f = options.context.getFile(instructions.pom.path);
if (!f.isFile())
throw new IllegalArgumentException(
"-maven-release specifies " + f + " as pom file but this file is not found");
pomResource = new FileResource(f);
} else {
pomResource = getPomResource(binary);
if (pomResource == null) {
pomResource = createPomResource(binary, options.context);
if (pomResource == null)
throw new IllegalArgumentException(
"No POM resource in META-INF/maven/... The Maven Bnd Repository requires this pom.");
}
}
IO.copy(pomResource.openInputStream(), pomFile);
IPom pom;
try (InputStream fin = IO.stream(pomFile)) {
pom = storage.getPom(fin);
}
Archive binaryArchive = pom.binaryArchive();
checkRemotePossible(instructions, binaryArchive.isSnapshot());
if (!binaryArchive.isSnapshot()) {
releasePlugin.add(options.context, pom);
if (storage.exists(binaryArchive)) {
logger.debug("Already released {} to {}", pom.getRevision(), this);
result.alreadyReleased = true;
return result;
}
}
logger.debug("Put release {}", pom.getRevision());
try (Release releaser = storage.release(pom.getRevision(), options.context.getProperties())) {
if (releaser == null) {
logger.debug("Already released {}", pom.getRevision());
return result;
}
if (instructions.snapshot >= 0)
releaser.setBuild(instructions.snapshot, null);
if (isLocal(instructions))
releaser.setLocalOnly();
releaser.add(pom.getRevision().pomArchive(), pomFile);
releaser.add(binaryArchive, binaryFile);
result.binaryArchive = binaryArchive;
result.pomArchive = pom.getRevision().pomArchive();
if (!isLocal(instructions)) {
try (Tool tool = new Tool(options.context, binary)) {
if (instructions.javadoc != null) {
if (!NONE.equals(instructions.javadoc.path)) {
try (Jar jar = getJavadoc(tool, options.context, instructions.javadoc.path,
instructions.javadoc.options,
instructions.javadoc.packages == JavadocPackages.EXPORT)) {
save(releaser, pom.getRevision(), jar, "javadoc");
}
}
}
if (instructions.sources != null) {
if (!NONE.equals(instructions.sources.path)) {
try (Jar jar = getSource(tool, options.context, instructions.sources.path)) {
save(releaser, pom.getRevision(), jar, "sources");
}
}
}
}
}
}
if (configuration.noupdateOnRelease() == false && !binaryArchive.isSnapshot())
index.add(binaryArchive);
}
return result;
} catch (Exception e) {
result.failed = e.getMessage();
throw e;
} finally {
IO.delete(binaryFile);
IO.delete(pomFile);
}
}
void checkRemotePossible(ReleaseDTO instructions, boolean snapshot) {
if (instructions.type == ReleaseType.REMOTE) {
if (snapshot) {
if (this.storage.getSnapshotRepositories().isEmpty())
throw new IllegalArgumentException(
"Remote snapshot release requested but no snapshot repository set for " + getName());
} else if (this.storage.getReleaseRepositories().isEmpty())
throw new IllegalArgumentException(
"Remote release requested but no release repository set for " + getName());
}
}
boolean isLocal(ReleaseDTO instructions) {
return instructions.type == ReleaseType.LOCAL;
}
private Jar getSource(Tool tool, Processor context, String path) throws Exception {
Jar jar = toJar(context, path);
if (jar == null) {
jar = tool.doSource();
}
jar.ensureManifest();
tool.addClose(jar);
return jar;
}
private Jar getJavadoc(Tool tool, Processor context, String path, Map<String,String> options, boolean exports)
throws Exception {
Jar jar = toJar(context, path);
if (jar == null) {
jar = tool.doJavadoc(options, exports);
}
jar.ensureManifest();
tool.addClose(jar);
return jar;
}
private Jar toJar(Processor context, String path) throws Exception {
if (path == null)
return null;
File f = context.getFile(path);
if (f.exists())
return new Jar(f);
return null;
}
private void save(Release releaser, Revision revision, Jar jar, String classifier) throws Exception {
String extension = IO.getExtension(jar.getName(), "jar");
File tmp = File.createTempFile(classifier, extension);
try {
jar.write(tmp);
releaser.add(revision.archive(extension, classifier), tmp);
} finally {
IO.delete(tmp);
}
}
/*
* Parse the -maven-release header.
*/
private ReleaseDTO getReleaseDTO(Processor context) {
ReleaseDTO release = new ReleaseDTO();
if (context == null)
return release;
Parameters p = new Parameters(context.getProperty(Constants.MAVEN_RELEASE), reporter);
release.type = storage.isLocalOnly() ? ReleaseType.LOCAL : ReleaseType.REMOTE;
Attrs attrs = p.remove("remote");
if (attrs != null) {
release.type = ReleaseType.REMOTE;
String s = attrs.get("snapshot");
if (s != null)
release.snapshot = Long.parseLong(s);
} else {
attrs = p.remove("local");
if (attrs != null) {
release.type = ReleaseType.LOCAL;
}
}
Attrs javadoc = p.remove("javadoc");
if (javadoc != null) {
release.javadoc.path = javadoc.get("path");
if (NONE.equals(release.javadoc.path)) {
release.javadoc = null;
} else
release.javadoc.options = javadoc;
}
Attrs sources = p.remove("sources");
if (sources != null) {
release.sources.path = sources.get("path");
if (NONE.equals(release.sources.path))
release.sources = null;
}
Attrs pom = p.remove("pom");
if (pom != null) {
release.pom.path = pom.get("path");
}
if (!p.isEmpty()) {
reporter.warning("The -maven-release instruction contains unrecognized options: %s", p);
}
return release;
}
private Resource getPomResource(Jar jar) {
for (Map.Entry<String,Resource> e : jar.getResources().entrySet()) {
String path = e.getKey();
if (path.startsWith("META-INF/maven/") && path.endsWith("/pom.xml")) {
return e.getValue();
}
}
return null;
}
private Resource createPomResource(Jar binary, Processor context) throws Exception {
Manifest manifest = binary.getManifest();
if (manifest == null)
return null;
try (Processor scoped = context == null ? new Processor() : new Processor(context)) {
if (scoped.getProperty(Constants.GROUPID) == null)
scoped.setProperty(Constants.GROUPID, "osgi-bundle");
return new PomResource(scoped, manifest);
}
}
@Override
public File get(String bsn, Version version, Map<String,String> properties, final DownloadListener... listeners)
throws Exception {
init();
BundleDescriptor descriptor = index.getDescriptor(bsn, version);
if (descriptor == null)
return null;
Archive archive = descriptor.archive;
if (archive != null) {
final File file = storage.toLocalFile(archive);
final File withSources = new File(file.getParentFile(), "+" + file.getName());
if (withSources.isFile() && withSources.lastModified() > file.lastModified()) {
if (listeners.length == 0)
return withSources;
for (DownloadListener dl : listeners)
dl.success(withSources);
return withSources;
}
Promise<File> promise = index.updateAsync(descriptor, storage.get(archive));
if (listeners.length == 0)
return promise.getValue();
promise.then(new Success<File,Void>() {
@Override
public Promise<Void> call(Promise<File> resolved) throws Exception {
File value = resolved.getValue();
if (value == null) {
throw new FileNotFoundException("Download failed");
}
for (DownloadListener dl : listeners) {
try {
dl.success(value);
} catch (Exception e) {
reporter.exception(e, "Download listener failed in success callback %s", dl);
}
}
return null;
}
}).then(null, new Failure() {
@Override
public void fail(Promise< ? > resolved) throws Exception {
String reason = Exceptions.toString(resolved.getFailure());
for (DownloadListener dl : listeners) {
try {
dl.failure(file, reason);
} catch (Exception e) {
reporter.exception(e, "Download listener failed in failure callback %s", dl);
}
}
}
});
return file;
}
return null;
}
@Override
public boolean canWrite() {
return !configuration.readOnly();
}
@Override
public List<String> list(String pattern) throws Exception {
init();
Glob g = pattern == null ? null : new Glob(pattern);
List<String> bsns = new ArrayList<>();
for (String bsn : index.list()) {
if (g == null || g.matcher(bsn).matches())
bsns.add(bsn);
}
return bsns;
}
@Override
public SortedSet<Version> versions(String bsn) throws Exception {
init();
TreeSet<Version> versions = new TreeSet<Version>();
for (Version version : index.list(bsn)) {
versions.add(version);
}
return versions;
}
@Override
public String getName() {
init();
return name;
}
private synchronized void init() {
try {
if (inited)
return;
client = registry.getPlugin(HttpClient.class);
inited = true;
name = configuration.name("Maven");
localRepo = IO.getFile(configuration.local(MAVEN_REPO_LOCAL));
List<MavenBackingRepository> release = MavenBackingRepository.create(configuration.releaseUrl(), reporter,
localRepo, client);
List<MavenBackingRepository> snapshot = MavenBackingRepository.create(configuration.snapshotUrl(), reporter,
localRepo, client);
storage = new MavenRepository(localRepo, getName(), release, snapshot, Processor.getExecutor(), reporter,
getRefreshCallback());
File base = IO.work;
if (registry != null) {
Workspace ws = registry.getPlugin(Workspace.class);
if (ws != null)
base = ws.getBuildDir();
}
File indexFile = IO.getFile(base, configuration.index(name.toLowerCase() + ".mvn"));
IndexFile ixf = new IndexFile(reporter, indexFile, storage);
ixf.open();
this.index = ixf;
startPoll(index);
logger.debug("initing {}", this);
} catch (Exception e) {
reporter.exception(e, "Init for maven repo failed %s", configuration);
throw new RuntimeException(e);
}
}
private void startPoll(final IndexFile index) {
Workspace ws = registry.getPlugin(Workspace.class);
if ((ws != null) && (ws.getGestalt().containsKey(Constants.GESTALT_BATCH)
|| ws.getGestalt().containsKey(Constants.GESTALT_CI)
|| ws.getGestalt().containsKey(Constants.GESTALT_OFFLINE))) {
return;
}
final AtomicBoolean busy = new AtomicBoolean();
indexPoller = Processor.getScheduledExecutor().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (busy.getAndSet(true))
return;
try {
poll();
} catch (Exception e) {
reporter.error("Error when polling index for %s for change", this);
} finally {
busy.set(false);
}
}
}, 5000, 5000, TimeUnit.MILLISECONDS);
}
void poll() throws Exception {
if (index.refresh()) {
for (RepositoryListenerPlugin listener : registry.getPlugins(RepositoryListenerPlugin.class))
listener.repositoryRefreshed(this);
}
}
@Override
public String getLocation() {
return configuration.releaseUrl() == null ? configuration.local(MAVEN_REPO_LOCAL) : configuration.releaseUrl();
}
@Override
public void setProperties(Map<String,String> map) throws Exception {
configuration = Converter.cnv(Configuration.class, map);
}
@Override
public void setReporter(Reporter reporter) {
this.reporter = reporter;
}
@Override
public void setRegistry(Registry registry) {
this.registry = registry;
}
@Override
public void close() throws IOException {
IO.close(storage);
if (indexPoller != null)
indexPoller.cancel(true);
}
private Callable<Boolean> getRefreshCallback() {
return new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
for (RepositoryListenerPlugin rp : registry.getPlugins(RepositoryListenerPlugin.class)) {
try {
rp.repositoryRefreshed(MavenBndRepository.this);
} catch (Exception e) {
reporter.exception(e, "Updating listener plugin %s", rp);
}
}
return ok;
}
};
}
@Override
public boolean refresh() throws Exception {
return index.refresh();
}
@Override
public File getRoot() throws Exception {
return localRepo;
}
public BundleDescriptor getDescriptor(String bsn, Version version) throws Exception {
return index.getDescriptor(bsn, version);
}
@Override
public String toString() {
return "MavenBndRepository [localRepo=" + localRepo + ", storage=" + getName() + ", inited=" + inited + "]";
}
@Override
public Map<String,Runnable> actions(Object... target) throws Exception {
switch (target.length) {
case 0 :
return null;
case 1 :
return actions.getProgramActions((String) target[0]);
case 2 :
BundleDescriptor bd = getBundleDescriptor(target);
return actions.getRevisionActions(bd);
default :
}
return null;
}
@Override
public String tooltip(Object... target) throws Exception {
switch (target.length) {
case 0 :
try (Formatter f = new Formatter()) {
f.format("%s\n", getName());
f.format("Revisions %s\n", index.descriptors.size());
for (MavenBackingRepository mbr : storage.getReleaseRepositories())
f.format("Release %s (%s)\n", mbr, getUser(mbr));
for (MavenBackingRepository mbr : storage.getSnapshotRepositories())
f.format("Snapshot %s (%s)\n", mbr, getUser(mbr));
f.format("Storage %s\n", localRepo);
f.format("Index %s\n", index.indexFile);
f.format("Index Cache %s\n", index.cacheDir);
return f.toString();
}
case 1 :
try (Formatter f = new Formatter()) {
String name = (String) target[0];
Set<aQute.maven.api.Program> programs = index.getProgramsForBsn(name);
return programs.toString();
}
case 2 :
BundleDescriptor bd = getBundleDescriptor(target);
try (Formatter f = new Formatter()) {
f.format("%s\n", bd.archive);
f.format("Bundle-Version %s\n", bd.version);
f.format("Last Modified %s\n", new Date(bd.lastModified));
f.format("URL %s\n", bd.url);
f.format("SHA-1 %s\n", Hex.toHexString(bd.id).toLowerCase());
f.format("SHA-256 %s\n", Hex.toHexString(bd.sha256).toLowerCase());
File localFile = storage.toLocalFile(bd.archive);
f.format("Local %s%s\n", localFile, localFile.isFile() ? "" : " ?");
if (bd.description != null)
f.format("Description\n%s", bd.description);
return f.toString();
}
default :
}
return null;
}
private Object getUser(MavenBackingRepository remote) {
if (remote == null)
return "";
try {
return remote.getUser();
} catch (Exception e) {
return "error: " + e.toString();
}
}
BundleDescriptor getBundleDescriptor(Object... target) throws Exception {
String bsn = (String) target[0];
Version version = (Version) target[1];
BundleDescriptor bd = getDescriptor(bsn, version);
return bd;
}
@Override
public String title(Object... target) throws Exception {
switch (target.length) {
case 0 :
String name = getName();
int n = index.getErrors(null);
if (n > 0)
return name += " [" + n + "!]";
return name;
case 1 :
name = (String) target[0];
n = index.getErrors(name);
if (n > 0)
name += " [!]";
return name;
case 2 :
BundleDescriptor bd = getBundleDescriptor(target);
if (bd.error != null)
return bd.version + " [" + bd.error + "]";
else if (isLocal(bd.archive)) {
return bd.version.toString();
} else
return bd.version.toString() + " [?]";
default :
}
return null;
}
private boolean isLocal(Archive archive) {
return storage.toLocalFile(archive).isFile();
}
public boolean dropTarget(URI uri) throws Exception {
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);
}
if ("search.maven.org".equals(uri.getHost()) && "/remotecontent".equals(uri.getPath())) {
return doSearchMaven(uri);
}
if (uri.getPath() != null && uri.getPath().endsWith(".pom"))
return addPom(uri);
return false;
}
public boolean dropTarget(File file) throws Exception {
if (file.getName().equals("pom.xml")) {
return addPom(file.toURI());
}
return false;
}
private boolean addPom(URI uri) throws Exception {
try {
// http://search.maven.org/remotecontent?filepath=com/netflix/governator/governator-commons-cli/1.12.10/governator-commons-cli-1.12.10.pom
IPom pom = storage.getPom(client.connect(uri.toURL()));
Archive binaryArchive = pom.binaryArchive();
index.add(binaryArchive);
return true;
} catch (FileNotFoundException e) {
return false;
} catch (Exception e) {
logger.debug("Failure to parse {}", uri, e);
return false;
}
}
boolean doSearchMaven(URI uri) throws UnsupportedEncodingException, Exception {
Map<String,String> map = getMapFromQuery(uri);
String filePath = map.get("filepath");
if (filePath != null) {
Archive archive = Archive.fromFilepath(filePath);
if (archive != null) {
if (archive.extension.equals("pom"))
archive = archive.revision.archive("jar", null);
index.add(archive);
return true;
}
}
return false;
}
Map<String,String> getMapFromQuery(URI uri) throws UnsupportedEncodingException {
String rawQuery = uri.getRawQuery();
Map<String,String> map = new HashMap<>();
if (rawQuery != null) {
String parts[] = rawQuery.split("&");
for (String part : parts) {
String kv[] = part.split("=");
String key = URLDecoder.decode(kv[0], "UTF-8");
String value = kv.length > 1 ? URLDecoder.decode(kv[1], "UTF-8") : "";
String previous = map.put(key, value);
if (previous != null) {
map.put(key, previous + "," + value);
}
}
}
return map;
}
@Override
public void toPom(OutputStream out, PomOptions options) throws Exception {
init();
PomGenerator pg = new PomGenerator(index.getArchives());
pg.name(Revision.valueOf(options.gav))
.parent(Revision.valueOf(options.parent))
.dependencyManagement(options.dependencyManagement)
.out(out);
}
@Override
public Map<Requirement,Collection<Capability>> findProviders(Collection< ? extends Requirement> requirements) {
init();
return index.findProviders(requirements);
}
@Override
public void begin(Project project) {
releasePlugin = new ReleasePluginImpl(this, project);
}
@Override
public void end(Project p) {
System.out.println("Project ending is " + p);
try {
releasePlugin.end(p, storage);
} catch (Exception e) {
e.printStackTrace();
p.error("Could not end the release", e);
}
}
}