From 79ec7f791784a93e651fddecb285127a47296836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sun, 12 Jan 2025 17:48:44 +0100 Subject: [PATCH] Create a tycho-baseline:check-dependencies mojo to validate versions If version ranges on packages or bundles are used it is currently quite hard to ensure these are actually work for the provided ranges especially if they evolve over a long time. This now adds a new tycho-baseline:check-dependencies mojo that can help in this task by inspecting the byte-code of the project and the dependencies if there is any inconsistency in any of the dependencies matching the version range. --- tycho-baseline-plugin/pom.xml | 14 + .../tycho/baseline/DependencyCheckMojo.java | 299 +++++ .../EclipseIndexArtifactVersionProvider.java | 142 +++ .../MavenArtifactVersionProvider.java | 178 +++ .../eclipse/tycho/copyfrom/oomph/P2Index.java | 11 +- .../tycho/copyfrom/oomph/P2IndexImpl.java | 1073 ++++++++--------- .../core/resolver/target/ArtifactMatcher.java | 28 +- .../tycho/artifacts/ArtifactVersion.java | 26 + .../artifacts/ArtifactVersionProvider.java | 26 + 9 files changed, 1229 insertions(+), 568 deletions(-) create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/EclipseIndexArtifactVersionProvider.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/MavenArtifactVersionProvider.java create mode 100644 tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java create mode 100644 tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java diff --git a/tycho-baseline-plugin/pom.xml b/tycho-baseline-plugin/pom.xml index 5acee8e8ba..f76e211228 100644 --- a/tycho-baseline-plugin/pom.xml +++ b/tycho-baseline-plugin/pom.xml @@ -53,6 +53,16 @@ asciitable 0.3.2 + + org.ow2.asm + asm + 9.7.1 + + + org.eclipse.emf + org.eclipse.emf.ecore + 2.38.0 + @@ -60,6 +70,10 @@ org.codehaus.plexus plexus-component-metadata + + org.eclipse.sisu + sisu-maven-plugin + org.apache.maven.plugins maven-plugin-plugin diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java new file mode 100644 index 0000000000..0e229c0cad --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java @@ -0,0 +1,299 @@ +/******************************************************************************* + * Copyright (c) 2024 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.baseline; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.osgi.container.ModuleRevisionBuilder; +import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo; +import org.eclipse.osgi.container.builders.OSGiManifestBuilderFactory; +import org.eclipse.osgi.internal.framework.FilterImpl; +import org.eclipse.tycho.DependencyArtifacts; +import org.eclipse.tycho.artifacts.ArtifactVersion; +import org.eclipse.tycho.artifacts.ArtifactVersionProvider; +import org.eclipse.tycho.core.TychoProjectManager; +import org.eclipse.tycho.core.osgitools.BundleReader; +import org.eclipse.tycho.core.osgitools.OsgiManifest; +import org.eclipse.tycho.core.resolver.target.ArtifactMatcher; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.osgi.framework.BundleException; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Namespace; + +/** + * This mojos performs deep inspections of dependencies to find out if a version + * range is actually valid. For this the following steps are performed: + *
    + *
  1. The current project artifact is analyzed for method signatures it + * calls
  2. + *
  3. Then it is checked what of these match to a given dependency
  4. + *
  5. All dependency versions matching the range are fetched and inspected + * using {@link ArtifactVersionProvider}s
  6. + *
  7. Then it checks if there are any missing signatures or inconsistencies and + * possibly failing the build
  8. + *
+ */ +@Mojo(defaultPhase = LifecyclePhase.VERIFY, name = "check-dependencies", threadSafe = true, requiresProject = true) +public class DependencyCheckMojo extends AbstractMojo { + + private static final String CLASS_SUFFIX = ".class"; + + @Parameter(property = "project", readonly = true) + private MavenProject project; + + @Component + private TychoProjectManager projectManager; + + @Component + private List versionProvider; + + @Component + private BundleReader bundleReader; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + // TODO check for packaging types...? But maven now also provides profiles on + // packaging types! + DependencyArtifacts artifacts = projectManager.getDependencyArtifacts(project).orElse(null); + File file = project.getArtifact().getFile(); + if (file == null || !file.isFile()) { + throw new MojoFailureException("Project artifact is not a valid file"); + } + ClassUsage usages = analyzeUsage(file); + Collection units = artifacts.getInstallableUnits(); + ModuleRevisionBuilder builder = readOSGiInfo(file); + List requirements = builder.getRequirements(); + List dependencyProblems = new ArrayList<>(); + for (GenericInfo genericInfo : requirements) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(genericInfo.getNamespace())) { + Map pkgInfo = getVersionInfo(genericInfo, + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE); + String packageVersion = pkgInfo.getOrDefault(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, "0.0.0"); + String packageName = pkgInfo.get(PackageNamespace.PACKAGE_NAMESPACE); + Optional packageProvidingUnit = ArtifactMatcher.findPackage(packageName, units); + if (packageProvidingUnit.isEmpty()) { + continue; + } + Set packageMethods = usages.signatures().stream() + .filter(ms -> packageName.equals(ms.packageName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (packageMethods.isEmpty()) { + // it could be that actually no methods referenced (e.g. interface is only + // referencing a type) + // TODO we need to check that the types used are present in all versions as + // otherwise we will get CNF exception! + continue; + } + IInstallableUnit unit = packageProvidingUnit.get(); + VersionRange versionRange = VersionRange.valueOf(packageVersion); + System.out.println("== " + packageName + " " + packageVersion + " is provided by " + unit + + " with version range " + versionRange + ", used method signatures from package are:"); + for (MethodSignature signature : packageMethods) { + System.out.println("\t" + signature.id()); + } + List list = versionProvider.stream() + .flatMap(avp -> avp.getPackageVersions(unit, packageName, versionRange, project)).toList(); + System.out.println("Matching versions:"); + // now we need to inspect all jars + for (ArtifactVersion v : list) { + System.out.println("\t" + v); + Path artifact = v.getArtifact(); + if (artifact == null) { + // Retrieval of artifacts might be lazy and we can't get this one --> error? + continue; + } + ClassProvides provides = analyzeProvides(artifact.toFile()); + for (MethodSignature mthd : packageMethods) { + if (!provides.signatures().contains(mthd)) { + dependencyProblems.add(new DependencyVersionProblem( + String.format( + "Import-Package '%s %s (compiled against %s %s) includes %s (provided by %s) but this version is missing the method %s", + packageName, packageVersion, unit.getId(), unit.getVersion(), + v.getVersion(), v.getProvider(), mthd.id()), + usages.classRef().get(mthd))); + } + } + } + // TODO we should emit a warning if the lower bound is not part of the + // discovered versions (or even fail?) + + } + } + for (DependencyVersionProblem problem : dependencyProblems) { + getLog().error(String.format("%s, referenced by:%s%s", problem.message(), System.lineSeparator(), + problem.references().stream().collect(Collectors.joining(System.lineSeparator())))); + } + } + + private Map getVersionInfo(GenericInfo genericInfo, String versionAttribute) { + Map directives = new HashMap<>(genericInfo.getDirectives()); + String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + FilterImpl filterImpl; + try { + filterImpl = FilterImpl.newInstance(filter); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$ + } + return filterImpl.getStandardOSGiAttributes(versionAttribute); + } + + private ModuleRevisionBuilder readOSGiInfo(File file) throws MojoFailureException { + OsgiManifest manifest = bundleReader.loadManifest(file); + ModuleRevisionBuilder builder; + try { + builder = OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders()); + } catch (BundleException e) { + throw new MojoFailureException(e); + } + return builder; + } + + private ClassUsage analyzeUsage(File file) throws MojoFailureException { + try { + Set usedMethodSignatures = new TreeSet<>(); + Map> classRef = new HashMap<>(); + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(CLASS_SUFFIX)) { + String classname = name.substring(0, name.length() - CLASS_SUFFIX.length()).replace('/', '.'); + InputStream stream = jar.getInputStream(jarEntry); + ClassReader reader = new ClassReader(stream.readAllBytes()); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, + String descriptor, boolean isInterface) { + if (owner.startsWith("java/")) { + // ignore references to java core classes + return; + } + MethodSignature sig = new MethodSignature(owner.replace('/', '.'), name, + descriptor); + classRef.computeIfAbsent(sig, nil -> new TreeSet<>()).add(classname); + usedMethodSignatures.add(sig); + } + }; + } + }, ClassReader.SKIP_FRAMES); + } + } + } + return new ClassUsage(usedMethodSignatures, classRef); + } catch (IOException e) { + throw new MojoFailureException(e); + } + } + + private ClassProvides analyzeProvides(File file) throws MojoFailureException { + try { + Set providedMethodSignatures = new TreeSet<>(); + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(CLASS_SUFFIX)) { + String classname = name.substring(0, name.length() - CLASS_SUFFIX.length()).replace('/', '.'); + InputStream stream = jar.getInputStream(jarEntry); + ClassReader reader = new ClassReader(stream.readAllBytes()); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + providedMethodSignatures.add(new MethodSignature(classname, name, descriptor)); + return null; + } + }, ClassReader.SKIP_FRAMES); + } + } + } + return new ClassProvides(providedMethodSignatures); + } catch (IOException e) { + // TODO + System.err.println(e); + return new ClassProvides(List.of()); + // throw new MojoFailureException(e); + } + } + + private static record ClassUsage(Collection signatures, + Map> classRef) { + + } + + private static record ClassProvides(Collection signatures) { + + } + + private static record MethodSignature(String className, String methodName, String signature) + implements Comparable { + public String packageName() { + String cn = className(); + int idx = cn.lastIndexOf('.'); + if (idx > 0) { + String substring = cn.substring(0, idx); + return substring; + } + return cn; + } + + public String id() { + return className() + "#" + methodName() + signature(); + } + + @Override + public int compareTo(MethodSignature o) { + return id().compareTo(o.id()); + } + } + + private static record DependencyVersionProblem(String message, Collection references) { + + } +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/EclipseIndexArtifactVersionProvider.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/EclipseIndexArtifactVersionProvider.java new file mode 100644 index 0000000000..df38af6688 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/EclipseIndexArtifactVersionProvider.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2024 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.baseline; + +import java.io.File; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.maven.project.MavenProject; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.equinox.p2.metadata.Version; +import org.eclipse.equinox.p2.query.QueryUtil; +import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; +import org.eclipse.equinox.spi.p2.publisher.PublisherHelper; +import org.eclipse.tycho.artifacts.ArtifactVersion; +import org.eclipse.tycho.artifacts.ArtifactVersionProvider; +import org.eclipse.tycho.copyfrom.oomph.P2Index; +import org.eclipse.tycho.copyfrom.oomph.P2Index.Repository; +import org.eclipse.tycho.copyfrom.oomph.P2IndexImpl; +import org.eclipse.tycho.core.resolver.target.ArtifactMatcher; +import org.eclipse.tycho.p2maven.repository.P2RepositoryManager; +import org.eclipse.tycho.p2maven.transport.TransportCacheConfig; +import org.osgi.framework.VersionRange; + +/** + * {@link ArtifactVersionProvider} using eclipse index + */ +@Named +public class EclipseIndexArtifactVersionProvider implements ArtifactVersionProvider { + + private P2Index p2Index; + private P2RepositoryManager repositoryManager; + + @Inject + public EclipseIndexArtifactVersionProvider(TransportCacheConfig cacheConfig, + P2RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + p2Index = new P2IndexImpl(new File(cacheConfig.getCacheLocation(), "index")); + } + + @Override + public Stream getPackageVersions(IInstallableUnit unit, String packageName, + VersionRange versionRange, MavenProject mavenProject) { + Map> map = p2Index.lookupCapabilities(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE, + packageName); + Set found = new HashSet<>(); + String id = unit.getId(); + return map.entrySet().stream() + .flatMap(entry -> entry.getValue().stream().filter(v -> v.isOSGiCompatible()).filter(v -> found.add(v)) + .map(version -> new EclipseIndexArtifactVersion(entry.getKey(), id, packageName, version))) + .filter(eia -> versionRange.includes(eia.getVersion())) + .sorted(Comparator.comparing(EclipseIndexArtifactVersion::getVersion).reversed()) + .map(ArtifactVersion.class::cast); + } + + private class EclipseIndexArtifactVersion implements ArtifactVersion { + + private Version version; + private org.apache.maven.model.Repository repository; + private String packageName; + private Path tempFile; + private String unitId; + + public EclipseIndexArtifactVersion(Repository repository, String unitId, String packageName, Version version) { + this.repository = new org.apache.maven.model.Repository(); + this.repository.setUrl(repository.getLocation().toString()); + this.unitId = unitId; + this.packageName = packageName; + this.version = version; + } + + @Override + public Path getArtifact() { + if (tempFile == null) { + try { + IInstallableUnit unit = getUnit(); + if (unit != null) { + tempFile = Files.createTempFile(unit.getId(), ".jar"); + tempFile.toFile().deleteOnExit(); + try (OutputStream stream = Files.newOutputStream(tempFile)) { + repositoryManager.downloadArtifact(unit, + repositoryManager.getArtifactRepository(repository), stream); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return tempFile; + } + + private IInstallableUnit getUnit() { + try { + IMetadataRepository metadataRepository = repositoryManager.getMetadataRepository(repository); + return ArtifactMatcher.findPackage(packageName, + metadataRepository.query(QueryUtil.createIUQuery(unitId), null), version).orElse(null); + } catch (Exception e) { + return null; + } + } + + @Override + public org.osgi.framework.Version getVersion() { + return org.osgi.framework.Version.parseVersion(version.getOriginal()); + } + + @Override + public String toString() { + return getVersion() + " (from repository " + repository + ")"; + } + + @Override + public String getProvider() { + IInstallableUnit unit = getUnit(); + if (unit != null) { + return unit.getId() + " " + unit.getVersion(); + } + return null; + } + + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/MavenArtifactVersionProvider.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/MavenArtifactVersionProvider.java new file mode 100644 index 0000000000..c15ff976f4 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/MavenArtifactVersionProvider.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (c) 2024 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.baseline; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.SessionScoped; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.version.Version; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.osgi.container.ModuleRevisionBuilder; +import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo; +import org.eclipse.osgi.container.builders.OSGiManifestBuilderFactory; +import org.eclipse.tycho.TychoConstants; +import org.eclipse.tycho.artifacts.ArtifactVersion; +import org.eclipse.tycho.artifacts.ArtifactVersionProvider; +import org.eclipse.tycho.core.osgitools.BundleReader; +import org.eclipse.tycho.core.osgitools.OsgiManifest; +import org.osgi.framework.BundleException; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.PackageNamespace; + +/** + * A {@link ArtifactVersionProvider} that checks maven repository for possible + * candidates + */ +@Named +@SessionScoped +public class MavenArtifactVersionProvider implements ArtifactVersionProvider { + + private MavenSession session; + private RepositorySystem repoSystem; + private BundleReader bundleReader; + + @Inject + public MavenArtifactVersionProvider(MavenSession session, RepositorySystem repoSystem, BundleReader bundleReader) { + this.session = session; + this.repoSystem = repoSystem; + this.bundleReader = bundleReader; + } + + @Override + public Stream getPackageVersions(IInstallableUnit unit, String packageName, + VersionRange versionRange, MavenProject mavenProject) { + String groupId = unit.getProperty(TychoConstants.PROP_GROUP_ID); + String artifactId = unit.getProperty(TychoConstants.PROP_ARTIFACT_ID); + String classifier = unit.getProperty(TychoConstants.PROP_CLASSIFIER); + if (groupId != null && artifactId != null && !"sources".equals(classifier)) { + System.out.println("Checking for " + groupId + "//" + artifactId); + List repositories = RepositoryUtils.toRepos(mavenProject.getRemoteArtifactRepositories()); + DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, classifier, "jar", "[0,)"); + // as we have no mean for a package version in maven we can only fetch all + // versions an check if the match + VersionRangeRequest rangeRequest = new VersionRangeRequest(artifact, repositories, ""); + try { + VersionRangeResult range = repoSystem.resolveVersionRange(session.getRepositorySession(), rangeRequest); + // now we sort from highest > lowest version + List versions = range.getVersions().stream() + .sorted(Comparator.naturalOrder().reversed()).toList(); + return versions.stream() + .map(v -> new MavenPackageArtifactVersion(artifact, v, packageName, repositories)) + .filter(mav -> mav.getVersion() != null) + // and drop all until we find a matching version + .dropWhile(mav -> !versionRange.includes(mav.getVersion())) + // and stop when we find the first non matching version + .takeWhile(mav -> versionRange.includes(mav.getVersion())) + // cast to make compiler happy + .map(ArtifactVersion.class::cast); + } catch (VersionRangeResolutionException e) { + // can't provide any useful data then... + } + } + return Stream.empty(); + } + + private class MavenPackageArtifactVersion implements ArtifactVersion { + + private Artifact artifact; + private List repositories; + private Path path; + private String packageName; + private org.osgi.framework.Version packageVersion; + + public MavenPackageArtifactVersion(Artifact artifact, Version version, String packageName, + List repositories) { + this.artifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), + artifact.getExtension(), version.toString()); + this.packageName = packageName; + this.repositories = repositories; + } + + @Override + public Path getArtifact() { + try { + ArtifactRequest request = new ArtifactRequest(artifact, repositories, ""); + ArtifactResult result = repoSystem.resolveArtifact(session.getRepositorySession(), request); + path = result.getArtifact().getFile().toPath(); + } catch (ArtifactResolutionException e) { + } + return path; + } + + @Override + public org.osgi.framework.Version getVersion() { + if (packageVersion == null) { + ModuleRevisionBuilder builder = readOSGiInfo(getArtifact()); + if (builder != null) { + List capabilities = builder.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE); + for (GenericInfo info : capabilities) { + Map attributes = info.getAttributes(); + if (packageName.equals(attributes.get(PackageNamespace.PACKAGE_NAMESPACE))) { + packageVersion = (org.osgi.framework.Version) attributes.getOrDefault( + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, + org.osgi.framework.Version.emptyVersion); + } + } + } + } + return packageVersion; + } + + @Override + public String toString() { + return getVersion() + " (maven artifact " + artifact + ")"; + } + + @Override + public String getProvider() { + ModuleRevisionBuilder info = readOSGiInfo(getArtifact()); + if (info != null) { + return info.getSymbolicName() + " " + info.getVersion(); + } + return null; + } + + } + + private ModuleRevisionBuilder readOSGiInfo(Path path) { + if (path != null) { + OsgiManifest manifest = bundleReader.loadManifest(path.toFile()); + try { + return OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders()); + } catch (BundleException e) { + } + } + return null; + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java index 03cc9203fa..7e1db8dbbb 100644 --- a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java @@ -8,15 +8,14 @@ * Contributors: * Eike Stepper - initial API and implementation */ -package org.eclipse.oomph.p2.internal.core; - -import org.eclipse.emf.common.util.URI; - -import org.eclipse.equinox.p2.metadata.Version; +package org.eclipse.tycho.copyfrom.oomph; import java.util.Map; import java.util.Set; +import org.eclipse.emf.common.util.URI; +import org.eclipse.equinox.p2.metadata.Version; + /** * @author Eike Stepper */ @@ -26,8 +25,6 @@ public interface P2Index public static final int COMPOSED_REPOSITORY = 1; - public static final P2Index INSTANCE = P2IndexImpl.INSTANCE; - public Repository[] getRepositories(); public Map> getCapabilities(); diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java index b7df4d3433..a728ea9032 100644 --- a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java @@ -8,22 +8,10 @@ * Contributors: * Eike Stepper - initial API and implementation */ -package org.eclipse.oomph.p2.internal.core; - -import org.eclipse.oomph.util.CollectionUtil; -import org.eclipse.oomph.util.IOUtil; -import org.eclipse.oomph.util.StringUtil; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl; -import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl.EObjectInputStream; - -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Path; -import org.eclipse.equinox.p2.metadata.Version; +package org.eclipse.tycho.copyfrom.oomph; import java.io.BufferedReader; +import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -38,555 +26,528 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl.EObjectInputStream; +import org.eclipse.equinox.p2.metadata.Version; + /** * @author Eike Stepper */ -public class P2IndexImpl implements P2Index -{ - public static final P2IndexImpl INSTANCE = new P2IndexImpl(); - - private static final String INDEX_BASE = "https://download.eclipse.org/oomph/index/"; //$NON-NLS-1$ - - private long timeStamp; - - private Map repositories; - - private Repository[] repositoriesArray; - - private Map> capabilitiesMap; - - private File repositoriesCacheFile; - - private File capabilitiesCacheFile; - - private int capabilitiesRefreshHours = -1; - - private int repositoriesRefreshHours = -1; - - private P2IndexImpl() - { - } - - private synchronized void initCapabilities() - { - if (capabilitiesMap == null || capabilitiesCacheFile.lastModified() + capabilitiesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) - { - capabilitiesMap = new LinkedHashMap<>(); - - ZipFile zipFile = null; - InputStream inputStream = null; - - try - { - initCapabilitiesCacheFile(); - - zipFile = new ZipFile(capabilitiesCacheFile); - ZipEntry zipEntry = zipFile.getEntry("capabilities"); //$NON-NLS-1$ - - inputStream = zipFile.getInputStream(zipEntry); - - Map options = new HashMap<>(); - options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); - options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); - options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); - - EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); - capabilitiesRefreshHours = stream.readInt(); - - int mapSize = stream.readCompressedInt(); - for (int i = 0; i < mapSize; ++i) - { - String key = stream.readSegmentedString(); - int valuesSize = stream.readCompressedInt(); - for (int j = 0; j < valuesSize; ++j) - { - String value = stream.readSegmentedString(); - CollectionUtil.add(capabilitiesMap, key, value); - } - } - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.closeSilent(inputStream); - if (zipFile != null) - { - try - { - zipFile.close(); - } - catch (IOException ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - } - } - } - } - - private synchronized void initRepositories(boolean force) - { - if (repositories == null || force || repositoriesCacheFile.lastModified() + repositoriesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) - { - repositories = new HashMap<>(); - - ZipFile zipFile = null; - InputStream inputStream = null; - - try - { - initRepositoriesCacheFile(); - - zipFile = new ZipFile(repositoriesCacheFile); - ZipEntry zipEntry = zipFile.getEntry("repositories"); //$NON-NLS-1$ - - inputStream = zipFile.getInputStream(zipEntry); - - Map options = new HashMap<>(); - options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); - options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); - options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); - - EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); - - timeStamp = stream.readLong(); - repositoriesRefreshHours = stream.readInt(); - int repositoryCount = stream.readInt(); - - Map> composedRepositories = new HashMap<>(); - for (int id = 1; id <= repositoryCount; id++) - { - RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories); - repositories.put(id, repository); - } - - for (Map.Entry> entry : composedRepositories.entrySet()) - { - RepositoryImpl repository = entry.getKey(); - for (int compositeID : entry.getValue()) - { - RepositoryImpl composite = repositories.get(compositeID); - if (composite != null) - { - composite.addChild(repository); - repository.addComposite(composite); - } - } - } - - try - { - int problematicRepositories = stream.readInt(); - for (int i = 0; i < problematicRepositories; i++) - { - int id = stream.readInt(); - int unresolvedChildren = stream.readInt(); - - RepositoryImpl repository = repositories.get(id); - repository.unresolvedChildren = unresolvedChildren; - } - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - - repositoriesArray = repositories.values().toArray(new Repository[repositories.size()]); - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.close(inputStream); - if (zipFile != null) - { - try - { - zipFile.close(); - } - catch (IOException ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - } - } - } - } - - private boolean initRepositoriesCacheFile() throws Exception - { - if (repositoriesCacheFile == null) - { - IPath stateLocation = P2CorePlugin.INSTANCE.isOSGiRunning() ? P2CorePlugin.INSTANCE.getStateLocation() : new Path("."); //$NON-NLS-1$ - repositoriesCacheFile = new File(stateLocation.toOSString(), "repositories"); //$NON-NLS-1$ - } - - downloadIfModifiedSince(new URL(INDEX_BASE + "repositories"), repositoriesCacheFile); //$NON-NLS-1$ - - return true; - } - - private boolean initCapabilitiesCacheFile() throws Exception - { - if (capabilitiesCacheFile == null) - { - IPath stateLocation = P2CorePlugin.INSTANCE.isOSGiRunning() ? P2CorePlugin.INSTANCE.getStateLocation() : new Path("."); //$NON-NLS-1$ - capabilitiesCacheFile = new File(stateLocation.toOSString(), "capabilities"); //$NON-NLS-1$ - } - - downloadIfModifiedSince(new URL(INDEX_BASE + "capabilities"), capabilitiesCacheFile); //$NON-NLS-1$ - - return true; - } - - @Override - public Repository[] getRepositories() - { - initRepositories(false); - return repositoriesArray; - } - - @Override - public Map> getCapabilities() - { - initCapabilities(); - return Collections.unmodifiableMap(capabilitiesMap); - } - - @Override - public Map> lookupCapabilities(String namespace, String name) - { - Map> capabilities = new HashMap<>(); - if (!StringUtil.isEmpty(namespace) && !StringUtil.isEmpty(name)) - { - namespace = URI.encodeSegment(namespace, false); - name = URI.encodeSegment(name, false); - - BufferedReader reader = null; - - try - { - InputStream inputStream = new URL(INDEX_BASE + namespace + "/" + name).openStream(); //$NON-NLS-1$ - reader = new BufferedReader(new InputStreamReader(inputStream)); - - String line = reader.readLine(); - if (line == null) - { - return capabilities; - } - - long timeStamp = Long.parseLong(line); - initRepositories(timeStamp != this.timeStamp); - - while ((line = reader.readLine()) != null) - { - String[] tokens = line.split(","); //$NON-NLS-1$ - int repositoryID = Integer.parseInt(tokens[0]); - Repository repository = repositories.get(repositoryID); - if (repository != null) - { - Set versions = new HashSet<>(); - for (int i = 1; i < tokens.length; i++) - { - versions.add(Version.parseVersion(tokens[i])); - } - - capabilities.put(repository, versions); - } - } - } - catch (FileNotFoundException ex) - { - // Ignore. - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.close(reader); - } - } - - return capabilities; - } - - @Override - public Map> generateCapabilitiesFromComposedRepositories(Map> capabilitiesFromSimpleRepositories) - { - Map> capabilities = new HashMap<>(); - for (Map.Entry> entry : capabilitiesFromSimpleRepositories.entrySet()) - { - Repository repository = entry.getKey(); - Set versions = entry.getValue(); - recurseComposedRepositories(capabilities, repository, versions); - } - - return capabilities; - } - - private void recurseComposedRepositories(Map> capabilities, Repository repository, Set versions) - { - for (Repository composite : repository.getComposites()) - { - Set set = capabilities.get(composite); - if (set == null) - { - set = new HashSet<>(); - capabilities.put(composite, set); - } - - set.addAll(versions); - recurseComposedRepositories(capabilities, composite, versions); - } - } - - private static void downloadIfModifiedSince(URL url, File file) throws IOException - { - long lastModified = -1L; - if (file.isFile()) - { - lastModified = file.lastModified(); - } - - InputStream inputStream = null; - OutputStream outputStream = null; - - try - { - HttpURLConnection connection = (HttpURLConnection)url.openConnection(); - if (lastModified != -1) - { - connection.setIfModifiedSince(lastModified); - } - - connection.connect(); - inputStream = connection.getInputStream(); - if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) - { - return; - } - - outputStream = new FileOutputStream(file); - IOUtil.copy(inputStream, outputStream); - outputStream.close(); - file.setLastModified(connection.getLastModified()); - } - finally - { - IOUtil.close(outputStream); - IOUtil.close(inputStream); - } - } - - /** - * @author Eike Stepper - */ - public static final class RepositoryImpl implements Repository - { - public static final int UNINITIALIZED = -1; - - private static final Repository[] NO_REPOSITORIES = {}; - - private final URI location; - - private final int id; - - private final boolean composed; - - private final boolean compressed; - - private final long timestamp; - - private int capabilityCount; - - private int unresolvedChildren; - - private Repository[] children; - - private Repository[] composites; - - public RepositoryImpl(EObjectInputStream stream, int id, Map> composedRepositories) throws IOException - { - this.id = id; - location = stream.readURI(); - composed = stream.readBoolean(); - compressed = stream.readBoolean(); - timestamp = stream.readLong(); - - if (composed) - { - capabilityCount = UNINITIALIZED; - } - else - { - capabilityCount = stream.readInt(); - } - - List composites = null; - while (stream.readBoolean()) - { - if (composites == null) - { - composites = new ArrayList<>(); - composedRepositories.put(this, composites); - } - - int composite = stream.readInt(); - composites.add(composite); - } - } - - @Override - public URI getLocation() - { - return location; - } - - @Override - public int getID() - { - return id; - } - - @Override - public boolean isComposed() - { - return composed; - } - - @Override - public boolean isCompressed() - { - return compressed; - } - - @Override - public long getTimestamp() - { - return timestamp; - } - - @Override - public int getCapabilityCount() - { - if (composed && capabilityCount == UNINITIALIZED) - { - capabilityCount = 0; - for (Repository child : getChildren()) - { - capabilityCount += child.getCapabilityCount(); - } - } - - return capabilityCount; - } - - @Override - public int getUnresolvedChildren() - { - return unresolvedChildren; - } - - @Override - public Repository[] getChildren() - { - if (children == null) - { - return NO_REPOSITORIES; - } - - return children; - } - - @Override - public Repository[] getComposites() - { - if (composites == null) - { - return NO_REPOSITORIES; - } - - return composites; - } - - @Override - public int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + id; - return result; - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - { - return true; - } - - if (obj == null || getClass() != obj.getClass()) - { - return false; - } - - RepositoryImpl other = (RepositoryImpl)obj; - if (id != other.id) - { - return false; - } - - return true; - } - - @Override - public int compareTo(Repository o) - { - return location.toString().compareTo(o.getLocation().toString()); - } - - @Override - public String toString() - { - return location.toString(); - } - - public void addChild(Repository child) - { - children = addRepository(children, child); - } - - public void addComposite(Repository composite) - { - composites = addRepository(composites, composite); - } - - private Repository[] addRepository(Repository[] repositories, Repository repository) - { - if (repositories == null) - { - return new Repository[] { repository }; - } - - int length = repositories.length; - Repository[] newRepositories = new Repository[length + 1]; - System.arraycopy(repositories, 0, newRepositories, 0, length); - newRepositories[length] = repository; - return newRepositories; - } - } +public class P2IndexImpl implements P2Index { + + private static final String INDEX_BASE = "https://download.eclipse.org/oomph/index/"; //$NON-NLS-1$ + + private long timeStamp; + + private Map repositories; + + private Repository[] repositoriesArray; + + private Map> capabilitiesMap; + + private File repositoriesCacheFile; + + private File capabilitiesCacheFile; + + private int capabilitiesRefreshHours = -1; + + private int repositoriesRefreshHours = -1; + + private File basedir; + + public P2IndexImpl(File basedir) { + this.basedir = basedir; + basedir.mkdirs(); + } + + private synchronized void initCapabilities() { + if (capabilitiesMap == null || capabilitiesCacheFile.lastModified() + + capabilitiesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + capabilitiesMap = new LinkedHashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initCapabilitiesCacheFile(); + + zipFile = new ZipFile(capabilitiesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("capabilities"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + capabilitiesRefreshHours = stream.readInt(); + + int mapSize = stream.readCompressedInt(); + for (int i = 0; i < mapSize; ++i) { + String key = stream.readSegmentedString(); + int valuesSize = stream.readCompressedInt(); + for (int j = 0; j < valuesSize; ++j) { + String value = stream.readSegmentedString(); + add(capabilitiesMap, key, value); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + closeSilent(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private synchronized void initRepositories(boolean force) { + if (repositories == null || force || repositoriesCacheFile.lastModified() + + repositoriesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + repositories = new HashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initRepositoriesCacheFile(); + + zipFile = new ZipFile(repositoriesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("repositories"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + + timeStamp = stream.readLong(); + repositoriesRefreshHours = stream.readInt(); + int repositoryCount = stream.readInt(); + + Map> composedRepositories = new HashMap<>(); + for (int id = 1; id <= repositoryCount; id++) { + RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories); + repositories.put(id, repository); + } + + for (Map.Entry> entry : composedRepositories.entrySet()) { + RepositoryImpl repository = entry.getKey(); + for (int compositeID : entry.getValue()) { + RepositoryImpl composite = repositories.get(compositeID); + if (composite != null) { + composite.addChild(repository); + repository.addComposite(composite); + } + } + } + + try { + int problematicRepositories = stream.readInt(); + for (int i = 0; i < problematicRepositories; i++) { + int id = stream.readInt(); + int unresolvedChildren = stream.readInt(); + + RepositoryImpl repository = repositories.get(id); + repository.unresolvedChildren = unresolvedChildren; + } + } catch (Exception ex) { + } + + repositoriesArray = repositories.values().toArray(new Repository[repositories.size()]); + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private boolean initRepositoriesCacheFile() throws Exception { + if (repositoriesCacheFile == null) { + repositoriesCacheFile = new File(basedir, "repositories"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "repositories"), repositoriesCacheFile); //$NON-NLS-1$ + + return true; + } + + private boolean initCapabilitiesCacheFile() throws Exception { + if (capabilitiesCacheFile == null) { + capabilitiesCacheFile = new File(basedir, "capabilities"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "capabilities"), capabilitiesCacheFile); //$NON-NLS-1$ + + return true; + } + + @Override + public Repository[] getRepositories() { + initRepositories(false); + return repositoriesArray; + } + + @Override + public Map> getCapabilities() { + initCapabilities(); + return Collections.unmodifiableMap(capabilitiesMap); + } + + @Override + public Map> lookupCapabilities(String namespace, String name) { + initCapabilities(); + Map> capabilities = new HashMap<>(); + if (!isEmpty(namespace) && !isEmpty(name)) { + namespace = URI.encodeSegment(namespace, false); + name = URI.encodeSegment(name, false); + + BufferedReader reader = null; + + try { + InputStream inputStream = new URL(INDEX_BASE + namespace + "/" + name).openStream(); //$NON-NLS-1$ + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String line = reader.readLine(); + if (line == null) { + return capabilities; + } + + long timeStamp = Long.parseLong(line); + initRepositories(timeStamp != this.timeStamp); + + while ((line = reader.readLine()) != null) { + String[] tokens = line.split(","); //$NON-NLS-1$ + int repositoryID = Integer.parseInt(tokens[0]); + Repository repository = repositories.get(repositoryID); + if (repository != null) { + Set versions = new HashSet<>(); + for (int i = 1; i < tokens.length; i++) { + versions.add(Version.parseVersion(tokens[i])); + } + + capabilities.put(repository, versions); + } + } + } catch (FileNotFoundException ex) { + // Ignore. + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(reader); + } + } + + return capabilities; + } + + @Override + public Map> generateCapabilitiesFromComposedRepositories( + Map> capabilitiesFromSimpleRepositories) { + Map> capabilities = new HashMap<>(); + for (Map.Entry> entry : capabilitiesFromSimpleRepositories.entrySet()) { + Repository repository = entry.getKey(); + Set versions = entry.getValue(); + recurseComposedRepositories(capabilities, repository, versions); + } + + return capabilities; + } + + private void recurseComposedRepositories(Map> capabilities, Repository repository, + Set versions) { + for (Repository composite : repository.getComposites()) { + Set set = capabilities.get(composite); + if (set == null) { + set = new HashSet<>(); + capabilities.put(composite, set); + } + + set.addAll(versions); + recurseComposedRepositories(capabilities, composite, versions); + } + } + + private static void downloadIfModifiedSince(URL url, File file) throws IOException { + long lastModified = -1L; + if (file.isFile()) { + lastModified = file.lastModified(); + } + + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (lastModified != -1) { + connection.setIfModifiedSince(lastModified); + } + + connection.connect(); + inputStream = connection.getInputStream(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return; + } + + outputStream = new FileOutputStream(file); + copy(inputStream, outputStream); + outputStream.close(); + file.setLastModified(connection.getLastModified()); + } finally { + close(outputStream); + close(inputStream); + } + } + + /** + * @author Eike Stepper + */ + public static final class RepositoryImpl implements Repository { + public static final int UNINITIALIZED = -1; + + private static final Repository[] NO_REPOSITORIES = {}; + + private final URI location; + + private final int id; + + private final boolean composed; + + private final boolean compressed; + + private final long timestamp; + + private int capabilityCount; + + private int unresolvedChildren; + + private Repository[] children; + + private Repository[] composites; + + public RepositoryImpl(EObjectInputStream stream, int id, + Map> composedRepositories) throws IOException { + this.id = id; + location = stream.readURI(); + composed = stream.readBoolean(); + compressed = stream.readBoolean(); + timestamp = stream.readLong(); + + if (composed) { + capabilityCount = UNINITIALIZED; + } else { + capabilityCount = stream.readInt(); + } + + List composites = null; + while (stream.readBoolean()) { + if (composites == null) { + composites = new ArrayList<>(); + composedRepositories.put(this, composites); + } + + int composite = stream.readInt(); + composites.add(composite); + } + } + + @Override + public URI getLocation() { + return location; + } + + @Override + public int getID() { + return id; + } + + @Override + public boolean isComposed() { + return composed; + } + + @Override + public boolean isCompressed() { + return compressed; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public int getCapabilityCount() { + if (composed && capabilityCount == UNINITIALIZED) { + capabilityCount = 0; + for (Repository child : getChildren()) { + capabilityCount += child.getCapabilityCount(); + } + } + + return capabilityCount; + } + + @Override + public int getUnresolvedChildren() { + return unresolvedChildren; + } + + @Override + public Repository[] getChildren() { + if (children == null) { + return NO_REPOSITORIES; + } + + return children; + } + + @Override + public Repository[] getComposites() { + if (composites == null) { + return NO_REPOSITORIES; + } + + return composites; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + RepositoryImpl other = (RepositoryImpl) obj; + if (id != other.id) { + return false; + } + + return true; + } + + @Override + public int compareTo(Repository o) { + return location.toString().compareTo(o.getLocation().toString()); + } + + @Override + public String toString() { + return location.toString(); + } + + public void addChild(Repository child) { + children = addRepository(children, child); + } + + public void addComposite(Repository composite) { + composites = addRepository(composites, composite); + } + + private Repository[] addRepository(Repository[] repositories, Repository repository) { + if (repositories == null) { + return new Repository[] { repository }; + } + + int length = repositories.length; + Repository[] newRepositories = new Repository[length + 1]; + System.arraycopy(repositories, 0, newRepositories, 0, length); + newRepositories[length] = repository; + return newRepositories; + } + } + + public static boolean add(Map> map, K key, V value) { + Set set = getSet(map, key); + return set.add(value); + } + + public static Set getSet(Map> map, K key) { + Set set = map.get(key); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(key, set); + } + + return set; + } + + public static void close(Closeable closeable) throws RuntimeException { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static Exception closeSilent(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + + return null; + } catch (Exception ex) { + return ex; + } + } + + public static long copy(InputStream input, OutputStream output) { + byte buffer[] = new byte[8192]; + try { + long length = 0; + int n; + + while ((n = input.read(buffer)) != -1) { + output.write(buffer, 0, n); + length += n; + } + + return length; + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + } \ No newline at end of file diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java index f2a463844b..9c0f789faa 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java @@ -14,11 +14,13 @@ package org.eclipse.tycho.core.resolver.target; import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.metadata.IProvidedCapability; @@ -27,6 +29,7 @@ import org.eclipse.equinox.p2.publisher.eclipse.Feature; import org.eclipse.equinox.p2.publisher.eclipse.FeatureEntry; import org.eclipse.equinox.p2.publisher.eclipse.FeaturesAction; +import org.eclipse.equinox.p2.query.CollectionResult; import org.eclipse.equinox.p2.query.IQuery; import org.eclipse.equinox.p2.query.IQueryResult; import org.eclipse.equinox.p2.query.QueryUtil; @@ -53,16 +56,31 @@ public static IInstallableUnit resolveReference(String type, String id, VersionR return ius.iterator().next(); } if (PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(type)) { - return ius.stream().flatMap(iu -> getPackageVersion(iu, id).map(v -> new SimpleEntry<>(iu, v)).stream()) - .max((o1, o2) -> { - return o1.getValue().compareTo(o2.getValue()); - }).map(Entry::getKey).orElse(null); + return findPackage(id, ius).orElse(null); } else { return ius.iterator().next(); } } - private static Optional getPackageVersion(IInstallableUnit unit, String packageName) { + public static Optional findPackage(String packageName, Collection ius) { + return findPackage(packageName, new CollectionResult<>(ius), null); + + } + + public static Optional findPackage(String packageName, IQueryResult query, + Version version) { + Stream> stream = query.stream() + .flatMap(iu -> getPackageVersion(iu, packageName).map(v -> new SimpleEntry<>(iu, v)).stream()); + if (version == null) { + return stream.max((o1, o2) -> { + return o1.getValue().compareTo(o2.getValue()); + }).map(Entry::getKey); + } else { + return stream.filter(e -> version.equals(e.getValue())).map(Entry::getKey).findFirst(); + } + } + + public static Optional getPackageVersion(IInstallableUnit unit, String packageName) { return unit.getProvidedCapabilities().stream() .filter(capability -> PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(capability.getNamespace())) diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java new file mode 100644 index 0000000000..894b9459c0 --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.artifacts; + +import java.nio.file.Path; + +import org.osgi.framework.Version; + +public interface ArtifactVersion { + + Path getArtifact(); + + Version getVersion(); + + String getProvider(); +} diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java new file mode 100644 index 0000000000..cf41de1764 --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.artifacts; + +import java.util.stream.Stream; + +import org.apache.maven.project.MavenProject; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.osgi.framework.VersionRange; + +public interface ArtifactVersionProvider { + + Stream getPackageVersions(IInstallableUnit unit, String packageName, VersionRange versionRange, + MavenProject mavenProject); + +}