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