package aQute.bnd.repository.p2.provider; import static aQute.bnd.osgi.resource.ResourceUtils.toVersion; import static java.util.Collections.singleton; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.TimeUnit; import org.osgi.resource.Capability; import org.osgi.resource.Requirement; import org.osgi.resource.Resource; import org.osgi.service.repository.Repository; import org.osgi.util.function.Function; import org.osgi.util.promise.Promise; import org.osgi.util.promise.Promises; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import aQute.bnd.http.HttpClient; import aQute.bnd.osgi.Processor; import aQute.bnd.osgi.repository.BridgeRepository; import aQute.bnd.osgi.repository.ResourcesRepository; import aQute.bnd.osgi.repository.XMLResourceGenerator; import aQute.bnd.osgi.repository.XMLResourceParser; import aQute.bnd.osgi.resource.CapabilityBuilder; import aQute.bnd.osgi.resource.FilterBuilder; import aQute.bnd.osgi.resource.RequirementBuilder; import aQute.bnd.osgi.resource.ResourceBuilder; import aQute.bnd.osgi.resource.ResourceUtils; import aQute.bnd.osgi.resource.ResourceUtils.ContentCapability; import aQute.bnd.osgi.resource.ResourceUtils.IdentityCapability; import aQute.bnd.service.RepositoryPlugin.DownloadListener; import aQute.bnd.util.repository.DownloadListenerPromise; import aQute.bnd.version.Version; import aQute.lib.io.IO; import aQute.p2.api.Artifact; import aQute.p2.provider.P2Impl; import aQute.service.reporter.Reporter; class P2Indexer implements Closeable { private final static Logger logger = LoggerFactory.getLogger(P2Indexer.class); private static final long MAX_STALE = TimeUnit.DAYS.toMillis(100); private final Reporter reporter; final File location; private final HttpClient client; private final URI url; private final String name; private final File indexFile; private volatile BridgeRepository bridge; private static final Resource RECOVERY = new ResourceBuilder().build(); private static final String P2_CAPABILITY_NAMESPACE = "bnd.p2"; private static final String MD5_ATTRIBUTE = "md5"; private static final Requirement MD5_REQUIREMENT = new RequirementBuilder(P2_CAPABILITY_NAMESPACE) .addFilter(new FilterBuilder().isPresent(MD5_ATTRIBUTE)) .buildSyntheticRequirement(); P2Indexer(Reporter reporter, File location, HttpClient client, URI url, String name) throws Exception { this.reporter = reporter; this.location = location; this.indexFile = new File(location, "index.xml.gz"); this.client = client; this.url = url; this.name = name; IO.mkdirs(this.location); validate(); Repository r; if (this.indexFile.isFile()) r = readFile(); else { r = readRepository(); save(r); } bridge = new BridgeRepository(r); } private void validate() { if (!this.location.isDirectory()) throw new IllegalArgumentException("%s cannot be made a directory" + this.location); } File get(String bsn, Version version, Map<String,String> properties, DownloadListener... listeners) throws Exception { Resource resource = getBridge().get(bsn, version); if (resource == null) return null; ContentCapability contentCapability = ResourceUtils.getContentCapability(resource); if (contentCapability == null) return null; URI url = contentCapability.url(); final File source = client.getCacheFileFor(url); final File link = new File(location, bsn + "-" + version + ".jar"); IO.createSymbolicLinkOrCopy(link, source); Promise<File> go = client.build().useCache(MAX_STALE).async(url.toURL()).map(new Function<File,File>() { @Override public File apply(File t) { return link; } }); if (listeners.length == 0) return go.getValue(); new DownloadListenerPromise(reporter, name + ": get " + bsn + ";" + version + " " + url, go, listeners); return link; } List<String> list(String pattern) throws Exception { return getBridge().list(pattern); } SortedSet<Version> versions(String bsn) throws Exception { return getBridge().versions(bsn); } private Repository readFile() throws Exception { try (XMLResourceParser xp = new XMLResourceParser(this.indexFile.toURI())) { List<Resource> resources = xp.parse(); return new ResourcesRepository(resources); } } private Repository readRepository() throws Exception { P2Impl p2 = new P2Impl(client, this.url, Processor.getExecutor()); List<Artifact> artifacts = p2.getArtifacts(); List<Promise<Resource>> fetched = new ArrayList<>(artifacts.size()); Set<URI> visitedURIs = new HashSet<>(artifacts.size()); Set<ArtifactID> visitedArtifacts = new HashSet<>(artifacts.size()); Map<ArtifactID,Resource> knownResources = new HashMap<>(); if (getBridge() != null) { for (Capability capability : getBridge().getRepository() .findProviders(singleton(MD5_REQUIREMENT)) .get(MD5_REQUIREMENT)) { Resource resource = capability.getResource(); IdentityCapability identity = ResourceUtils.getIdentityCapability(resource); ArtifactID artifact = new ArtifactID(identity.osgi_identity(), identity.version(), (String) capability.getAttributes().get(MD5_ATTRIBUTE)); knownResources.put(artifact, resource); } } for (final Artifact a : artifacts) { if (!visitedURIs.add(a.uri)) continue; if (a.md5 != null) { ArtifactID id = new ArtifactID(a.id, toVersion(a.version), a.md5); if (!visitedArtifacts.add(id)) continue; if (knownResources.containsKey(id)) { fetched.add(Promises.resolved(knownResources.get(id))); continue; } } Promise<Resource> promise = client.build() .useCache(MAX_STALE) .async(a.uri.toURL()) .map(new Function<File,Resource>() { @Override public Resource apply(File file) { try { ResourceBuilder rb = new ResourceBuilder(); rb.addFile(file, a.uri); if (a.md5 != null) rb.addCapability(new CapabilityBuilder(P2_CAPABILITY_NAMESPACE) .addAttribute(MD5_ATTRIBUTE, a.md5)); return rb.build(); } catch (Exception e) { logger.debug("{}: Failed to create resource for %s from {}", name, a, file, e); return RECOVERY; } } }) .recover(new Function<Promise< ? >,Resource>() { @Override public Resource apply(Promise< ? > failed) { try { logger.debug("{}: Failed to create resource for {}", name, a, failed.getFailure()); } catch (InterruptedException e) { // impossible } return RECOVERY; } }); fetched.add(promise); } Promise<List<Resource>> all = Promises.all(fetched); return all.map(new Function<List<Resource>,ResourcesRepository>() { @Override public ResourcesRepository apply(List<Resource> resources) { ResourcesRepository rr = new ResourcesRepository(); for (Resource resource : resources) { if (resource != RECOVERY) { rr.add(resource); } } return rr; } }).getValue(); } private void save(Repository repository) throws IOException, Exception { XMLResourceGenerator xrg = new XMLResourceGenerator(); xrg.repository(repository).name(name).save(indexFile); } Map<Requirement,Collection<Capability>> findProviders(Collection< ? extends Requirement> requirements) { return getBridge().getRepository().findProviders(requirements); } public void refresh() throws Exception { Repository repository = readRepository(); save(repository); this.bridge = new BridgeRepository(repository); } @Override public void close() throws IOException {} BridgeRepository getBridge() { return bridge; } }