diff --git a/pom.xml b/pom.xml index dd9daa58f..0c636c7d2 100644 --- a/pom.xml +++ b/pom.xml @@ -126,6 +126,7 @@ ${mojo.java.target} 5.9.1 3.2.5 + 3.3.9 1.12.0 1.11.1 ${project.version} @@ -177,6 +178,12 @@ maven-compat ${mavenVersion} + + org.apache.maven + maven-embedder + ${mavenEmbedderVersion} + provided + org.apache.maven.enforcer diff --git a/versions-common/pom.xml b/versions-common/pom.xml index 6276a72d7..a65e215b0 100644 --- a/versions-common/pom.xml +++ b/versions-common/pom.xml @@ -50,6 +50,10 @@ org.apache.maven maven-settings + + org.apache.maven + maven-embedder + com.fasterxml.woodstox woodstox-core diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java index d3844285c..f94a1ca16 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/DefaultVersionsHelper.java @@ -45,6 +45,7 @@ import java.util.concurrent.Future; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -89,6 +90,7 @@ import org.eclipse.aether.resolution.VersionRangeRequest; import org.eclipse.aether.resolution.VersionRangeResolutionException; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Optional.empty; import static java.util.Optional.of; @@ -172,12 +174,35 @@ public Log getLog() { public ArtifactVersions lookupArtifactVersions( Artifact artifact, VersionRange versionRange, boolean usePluginRepositories) throws VersionRetrievalException { + return lookupArtifactVersions(artifact, versionRange, usePluginRepositories, !usePluginRepositories); + } + + @Override + public ArtifactVersions lookupArtifactVersions( + Artifact artifact, VersionRange versionRange, boolean usePluginRepositories, boolean useProjectRepositories) + throws VersionRetrievalException { try { Collection ignoredVersions = getIgnoredVersions(artifact); if (!ignoredVersions.isEmpty() && getLog().isDebugEnabled()) { getLog().debug("Found ignored versions: " + ignoredVersions.stream().map(IgnoreVersion::toString).collect(Collectors.joining(", "))); } + + final List repositories; + if (usePluginRepositories && !useProjectRepositories) { + repositories = mavenSession.getCurrentProject().getRemotePluginRepositories(); + } else if (!usePluginRepositories && useProjectRepositories) { + repositories = mavenSession.getCurrentProject().getRemoteProjectRepositories(); + } else if (usePluginRepositories) { + repositories = Stream.concat( + mavenSession.getCurrentProject().getRemoteProjectRepositories().stream(), + mavenSession.getCurrentProject().getRemotePluginRepositories().stream()) + .distinct() + .collect(Collectors.toList()); + } else { + // testing? + repositories = emptyList(); + } return new ArtifactVersions( artifact, aetherRepositorySystem @@ -191,13 +216,7 @@ public ArtifactVersions lookupArtifactVersions( .findFirst() .map(Restriction::toString)) .orElse("(,)")), - usePluginRepositories - ? mavenSession - .getCurrentProject() - .getRemotePluginRepositories() - : mavenSession - .getCurrentProject() - .getRemoteProjectRepositories(), + repositories, "lookupArtifactVersions")) .getVersions() .stream() @@ -428,16 +447,20 @@ public ArtifactVersion createArtifactVersion(String version) { return DefaultArtifactVersionCache.of(version); } - @Override public Map lookupDependenciesUpdates( - Set dependencies, boolean usePluginRepositories, boolean allowSnapshots) + Set dependencies, + boolean usePluginRepositories, + boolean useProjectRepositories, + boolean allowSnapshots) throws VersionRetrievalException { ExecutorService executor = Executors.newFixedThreadPool(LOOKUP_PARALLEL_THREADS); try { Map dependencyUpdates = new TreeMap<>(DependencyComparator.INSTANCE); List>> futures = dependencies.stream() .map(dependency -> executor.submit(() -> new ImmutablePair<>( - dependency, lookupDependencyUpdates(dependency, usePluginRepositories, allowSnapshots)))) + dependency, + lookupDependencyUpdates( + dependency, usePluginRepositories, useProjectRepositories, allowSnapshots)))) .collect(Collectors.toList()); for (Future> details : futures) { Pair pair = details.get(); @@ -453,12 +476,22 @@ dependency, lookupDependencyUpdates(dependency, usePluginRepositories, allowSnap } } + @Override + public Map lookupDependenciesUpdates( + Set dependencies, boolean usePluginRepositories, boolean allowSnapshots) + throws VersionRetrievalException { + return lookupDependenciesUpdates(dependencies, usePluginRepositories, !usePluginRepositories, allowSnapshots); + } + @Override public ArtifactVersions lookupDependencyUpdates( - Dependency dependency, boolean usePluginRepositories, boolean allowSnapshots) + Dependency dependency, + boolean usePluginRepositories, + boolean useProjectRepositories, + boolean allowSnapshots) throws VersionRetrievalException { - ArtifactVersions allVersions = - lookupArtifactVersions(createDependencyArtifact(dependency), usePluginRepositories); + ArtifactVersions allVersions = lookupArtifactVersions( + createDependencyArtifact(dependency), null, usePluginRepositories, useProjectRepositories); return new ArtifactVersions( allVersions.getArtifact(), Arrays.stream(allVersions.getAllUpdates(allowSnapshots)).collect(Collectors.toList()), diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java index d10b4e0d9..102f0a6b3 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/VersionsHelper.java @@ -152,7 +152,7 @@ Artifact createDependencyArtifact( * The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots. * * @param artifact The artifact to look for versions of. - * @param usePluginRepositories true will consult the pluginRepositories, while false will + * @param usePluginRepositories {@code true} will consult the pluginRepositories, while {@code false} will * consult the repositories for normal dependencies. * @return The details of the available artifact versions. * @throws VersionRetrievalException thrown if version resolution fails @@ -167,7 +167,24 @@ ArtifactVersions lookupArtifactVersions(Artifact artifact, boolean usePluginRepo * The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots. * * @param artifact The artifact to look for versions of. - * @param versionRange versionRange to restrict the search + * @param versionRange versionRange to restrict the search, may be {@code null} + * @param usePluginRepositories {@code true} will consult the pluginRepositories + * @param useProjectRepositories {@code true} will consult regular project repositories + * @return The details of the available artifact versions. + * @throws VersionRetrievalException thrown if version resolution fails + * @since 2.15.0 + */ + ArtifactVersions lookupArtifactVersions( + Artifact artifact, VersionRange versionRange, boolean usePluginRepositories, boolean useProjectRepositories) + throws VersionRetrievalException; + + /** + * Looks up the versions of the specified artifact that are available in either the local repository, or the + * appropriate remote repositories. + * The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots. + * + * @param artifact The artifact to look for versions of. + * @param versionRange versionRange to restrict the search, may be {@code null} * @param usePluginRepositories true will consult the pluginRepositories, while false will * consult the repositories for normal dependencies. * @return The details of the available artifact versions. @@ -190,18 +207,39 @@ Map lookupDependenciesUpdates( Set dependencies, boolean usePluginRepositories, boolean allowSnapshots) throws VersionRetrievalException; + /** + * Returns a map of all possible updates per dependency. The lookup is done in parallel using + * {@code LOOKUP_PARALLEL_THREADS} threads. + * + * @param dependencies The set of {@link Dependency} instances to look up. + * @param usePluginRepositories Search the plugin repositories. + * @param useProjectRepositories whether to use regular project repositories + * @param allowSnapshots whether snapshots should be included + * @return map containing the ArtifactVersions object per dependency + */ + Map lookupDependenciesUpdates( + Set dependencies, + boolean usePluginRepositories, + boolean useProjectRepositories, + boolean allowSnapshots) + throws VersionRetrievalException; + /** * Creates an {@link org.codehaus.mojo.versions.api.ArtifactVersions} instance from a dependency. * * @param dependency The dependency. * @param usePluginRepositories Search the plugin repositories. + * @param useProjectRepositories whether to use regular project repositories * @param allowSnapshots whether snapshots should be included * @return The details of updates to the dependency. * @throws VersionRetrievalException thrown if version resolution fails * @since 1.0-beta-1 */ ArtifactVersions lookupDependencyUpdates( - Dependency dependency, boolean usePluginRepositories, boolean allowSnapshots) + Dependency dependency, + boolean usePluginRepositories, + boolean useProjectRepositories, + boolean allowSnapshots) throws VersionRetrievalException; /** diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java b/versions-common/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java index 4af2a4fbf..6b6475dc5 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java @@ -66,10 +66,14 @@ public Set removingFrom(Collection dependencies) { return filterBy(dependencies, not(this::matchersMatch)); } - private boolean matchersMatch(Dependency dependency) { + public boolean matchersMatch(Dependency dependency) { return matchers.stream().anyMatch(m -> m.test(dependency)); } + public boolean matchersDontMatch(Dependency dependency) { + return !matchersMatch(dependency); + } + private TreeSet filterBy(Collection dependencies, Predicate predicate) { return dependencies.stream() .filter(predicate) diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/utils/CoreExtensionUtils.java b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/CoreExtensionUtils.java new file mode 100644 index 000000000..62638fac8 --- /dev/null +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/CoreExtensionUtils.java @@ -0,0 +1,64 @@ +package org.codehaus.mojo.versions.utils; + +/* + * Copyright MojoHaus and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.apache.maven.cli.internal.extension.model.io.xpp3.CoreExtensionsXpp3Reader; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Extension; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +/** + * Utilities for reading and handling core extensions. + * + * @author Andrzej Jarmoniuk + * @since 2.15.0 + */ +public final class CoreExtensionUtils { + /** + * Reads the core extensions (not build extensions) configured for the given project + * from the {@code ${project}/.mvn/extensions.xml} file. + * + * @param session {@link MavenSession} instance + * @return stream of core extensions defined in the {@code ${project}/.mvn/extensions.xml} file + * @throws IOException thrown if a file I/O operation fails + * @throws XmlPullParserException thrown if the file cannot be parsed + * @since 2.15.0 + */ + public static Stream getCoreExtensions(MavenSession session) throws IOException, XmlPullParserException { + Path extensionsFile = session.getCurrentProject().getBasedir().toPath().resolve(".mvn/extensions.xml"); + if (!Files.isRegularFile(extensionsFile)) { + return Stream.empty(); + } + + try (Reader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(extensionsFile)))) { + return new CoreExtensionsXpp3Reader() + .read(reader).getExtensions().stream().map(ex -> ExtensionBuilder.newBuilder() + .withGroupId(ex.getGroupId()) + .withArtifactId(ex.getArtifactId()) + .withVersion(ex.getVersion()) + .build()); + } + } +} diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/utils/DependencyBuilder.java b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/DependencyBuilder.java index 58ca87c2f..c5daba94c 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/utils/DependencyBuilder.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/DependencyBuilder.java @@ -136,7 +136,9 @@ public static DependencyBuilder newBuilder() { * @param artifactId artifactId of the dependency * @param version version of the dependency * @return new instance of {@linkplain Dependency} + * @deprecated please use the {@link #newBuilder()} method instead */ + @Deprecated public static Dependency dependencyWith(String groupId, String artifactId, String version) { return newBuilder() .withGroupId(groupId) @@ -154,7 +156,9 @@ public static Dependency dependencyWith(String groupId, String artifactId, Strin * @param classifier classifier of the dependency * @param scope scope of the dependency * @return new instance of {@linkplain Dependency} + * @deprecated please use the {@link #newBuilder()} method instead */ + @Deprecated public static Dependency dependencyWith( String groupId, String artifactId, String version, String type, String classifier, String scope) { return newBuilder() diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/utils/ExtensionBuilder.java b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/ExtensionBuilder.java new file mode 100644 index 000000000..7d763606d --- /dev/null +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/utils/ExtensionBuilder.java @@ -0,0 +1,103 @@ +package org.codehaus.mojo.versions.utils; + +/* + * Copyright MojoHaus and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Extension; +import org.apache.maven.model.InputLocation; + +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; + +/** + * Builder class for {@linkplain Extension} + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class ExtensionBuilder { + private Optional groupId = empty(); + private Optional artifactId = empty(); + private Optional version = empty(); + private Map locations = new HashMap<>(); + + private ExtensionBuilder() {} + + /** + * Passes groupId to the builder + * @param groupId given groupId + * @return builder instance + */ + public ExtensionBuilder withGroupId(String groupId) { + this.groupId = ofNullable(groupId); + return this; + } + + /** + * Passes artifactId to the builder + * @param artifactId given artifactId + * @return builder instance + */ + public ExtensionBuilder withArtifactId(String artifactId) { + this.artifactId = ofNullable(artifactId); + return this; + } + + /** + * Passes version to the builder + * @param version given version + * @return builder instance + */ + public ExtensionBuilder withVersion(String version) { + this.version = ofNullable(version); + return this; + } + + /** + * Passes type to the builder + * @param key location key + * @param location input location + * @return builder instance + */ + public ExtensionBuilder withLocation(Object key, InputLocation location) { + this.locations.put(key, location); + return this; + } + + /** + * Creates a new instance of the builder + * @return new instance of the builder + */ + public static ExtensionBuilder newBuilder() { + return new ExtensionBuilder(); + } + + /** + * Builds the {@linkplain Dependency} instance + * @return {@linkplain Dependency} instance + */ + public Extension build() { + Extension inst = new Extension(); + groupId.ifPresent(inst::setGroupId); + artifactId.ifPresent(inst::setArtifactId); + version.ifPresent(inst::setVersion); + locations.forEach(inst::setLocation); + return inst; + } +} diff --git a/versions-common/src/test/java/org/codehaus/mojo/versions/utils/CoreExtensionUtilsTest.java b/versions-common/src/test/java/org/codehaus/mojo/versions/utils/CoreExtensionUtilsTest.java new file mode 100644 index 000000000..f0500f15d --- /dev/null +++ b/versions-common/src/test/java/org/codehaus/mojo/versions/utils/CoreExtensionUtilsTest.java @@ -0,0 +1,77 @@ +package org.codehaus.mojo.versions.utils; +/* + * Copyright MojoHaus and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Extension; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link CoreExtensionUtils} + * + * @author Andrzej Jarmoniuk + */ +public class CoreExtensionUtilsTest { + + @Test + public void testNoExtensions() throws XmlPullParserException, IOException { + MavenProject project = mock(MavenProject.class); + when(project.getBasedir()) + .thenReturn( + new File("src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/no-extensions")); + MavenSession session = mock(MavenSession.class); + when(session.getCurrentProject()).thenReturn(project); + assertThat(CoreExtensionUtils.getCoreExtensions(session).findAny(), is(Optional.empty())); + } + + @Test + public void testExtensionsFound() throws XmlPullParserException, IOException { + MavenProject project = mock(MavenProject.class); + when(project.getBasedir()) + .thenReturn(new File("src/test/resources/org/codehaus/mojo/versions/utils/core-extensions")); + MavenSession session = mock(MavenSession.class); + when(session.getCurrentProject()).thenReturn(project); + Set extensions = + CoreExtensionUtils.getCoreExtensions(session).collect(Collectors.toSet()); + assertThat( + extensions, + hasItems( + ExtensionBuilder.newBuilder() + .withGroupId("default-group") + .withArtifactId("artifactA") + .withVersion("1.0.0") + .build(), + ExtensionBuilder.newBuilder() + .withGroupId("default-group") + .withArtifactId("artifactB") + .withVersion("2.0.0") + .build())); + } +} diff --git a/versions-common/src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/.mvn/extensions.xml b/versions-common/src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/.mvn/extensions.xml new file mode 100644 index 000000000..172cdbc1f --- /dev/null +++ b/versions-common/src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/.mvn/extensions.xml @@ -0,0 +1,13 @@ + + + default-group + artifactA + 1.0.0 + + + default-group + artifactB + 2.0.0 + + \ No newline at end of file diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-001/invoker.properties b/versions-maven-plugin/src/it/it-display-extension-updates-001/invoker.properties new file mode 100644 index 000000000..5da846218 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-001/invoker.properties @@ -0,0 +1,8 @@ +invoker.goals.1 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.1 = -Dversions.outputFile=./output1.txt -DoutputEncoding=UTF-8 + +invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.2 = -Dversions.outputFile=./output2.txt -DoutputEncoding=UTF-8 -DextensionExcludes=localhost + +invoker.goals.3 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.3 = -Dversions.outputFile=./output3.txt -DoutputEncoding=UTF-8 -DextensionIncludes=localhost -DextensionExcludes=localhost:dummy-api diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-001/pom.xml b/versions-maven-plugin/src/it/it-display-extension-updates-001/pom.xml new file mode 100644 index 000000000..fe8f43302 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-001/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + localhost + it-display-extension-updates-001 + + 1.0 + pom + + + + + localhost + dummy-maven-plugin + 1.0 + + + localhost + dummy-api + 1.0 + + + + diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-001/verify.groovy b/versions-maven-plugin/src/it/it-display-extension-updates-001/verify.groovy new file mode 100644 index 000000000..4f5fe3ab6 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-001/verify.groovy @@ -0,0 +1,10 @@ +def output1 = new File( basedir, "output1.txt").text +assert output1 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ +assert output1 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ + +def output2 = new File( basedir, "output2.txt") +assert !output2.exists() + +def output3 = new File( basedir, "output3.txt").text +assert output3 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ +assert !( output3 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ ) diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-002/.mvn/extensions.xml b/versions-maven-plugin/src/it/it-display-extension-updates-002/.mvn/extensions.xml new file mode 100644 index 000000000..1f4f4fbac --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/.mvn/extensions.xml @@ -0,0 +1,13 @@ + + + localhost + dummy-maven-plugin + 1.0 + + + localhost + dummy-api + 1.0 + + \ No newline at end of file diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-002/invoker.properties b/versions-maven-plugin/src/it/it-display-extension-updates-002/invoker.properties new file mode 100644 index 000000000..5da846218 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/invoker.properties @@ -0,0 +1,8 @@ +invoker.goals.1 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.1 = -Dversions.outputFile=./output1.txt -DoutputEncoding=UTF-8 + +invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.2 = -Dversions.outputFile=./output2.txt -DoutputEncoding=UTF-8 -DextensionExcludes=localhost + +invoker.goals.3 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.3 = -Dversions.outputFile=./output3.txt -DoutputEncoding=UTF-8 -DextensionIncludes=localhost -DextensionExcludes=localhost:dummy-api diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-002/pom.xml b/versions-maven-plugin/src/it/it-display-extension-updates-002/pom.xml new file mode 100644 index 000000000..28820da69 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/pom.xml @@ -0,0 +1,11 @@ + + 4.0.0 + localhost + it-display-extension-updates-002 + + 1.0 + pom + + diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-002/verify.groovy b/versions-maven-plugin/src/it/it-display-extension-updates-002/verify.groovy new file mode 100644 index 000000000..4f5fe3ab6 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/verify.groovy @@ -0,0 +1,10 @@ +def output1 = new File( basedir, "output1.txt").text +assert output1 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ +assert output1 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ + +def output2 = new File( basedir, "output2.txt") +assert !output2.exists() + +def output3 = new File( basedir, "output3.txt").text +assert output3 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ +assert !( output3 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ ) diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-003/.mvn/extensions.xml b/versions-maven-plugin/src/it/it-display-extension-updates-003/.mvn/extensions.xml new file mode 100644 index 000000000..18a2a6a1d --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/.mvn/extensions.xml @@ -0,0 +1,8 @@ + + + localhost + dummy-api + 1.0 + + \ No newline at end of file diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-003/invoker.properties b/versions-maven-plugin/src/it/it-display-extension-updates-003/invoker.properties new file mode 100644 index 000000000..0bd6e32e0 --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/invoker.properties @@ -0,0 +1,5 @@ +invoker.goals.1 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.1 = -DprocessBuildExtensions=false -Dversions.outputFile=./output1.txt -DoutputEncoding=UTF-8 + +invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:display-extension-updates +invoker.mavenOpts.2 = -DprocessCoreExtensions=false -Dversions.outputFile=./output2.txt -DoutputEncoding=UTF-8 diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-003/pom.xml b/versions-maven-plugin/src/it/it-display-extension-updates-003/pom.xml new file mode 100644 index 000000000..655a5ea9e --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + localhost + it-display-extension-updates-003 + + 1.0 + pom + + + + + localhost + dummy-maven-plugin + 1.0 + + + + + diff --git a/versions-maven-plugin/src/it/it-display-extension-updates-003/verify.groovy b/versions-maven-plugin/src/it/it-display-extension-updates-003/verify.groovy new file mode 100644 index 000000000..f2e88457d --- /dev/null +++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/verify.groovy @@ -0,0 +1,7 @@ +def output1 = new File( basedir, "output1.txt").text +assert !( output1 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ ) +assert output1 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ + +def output2 = new File( basedir, "output2.txt").text +assert output2 =~ /\Qlocalhost:dummy-maven-plugin\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.1\E/ +assert !( output2 =~ /\Qlocalhost:dummy-api\E\s*\.*\s*\Q1.0\E\s+->\s+\Q3.0\E/ ) diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojo.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojo.java new file mode 100644 index 000000000..c73f5d1ec --- /dev/null +++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojo.java @@ -0,0 +1,298 @@ +package org.codehaus.mojo.versions; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Extension; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.repository.RepositorySystem; +import org.apache.maven.wagon.Wagon; +import org.codehaus.mojo.versions.api.ArtifactVersions; +import org.codehaus.mojo.versions.api.Segment; +import org.codehaus.mojo.versions.api.VersionRetrievalException; +import org.codehaus.mojo.versions.api.recording.ChangeRecorder; +import org.codehaus.mojo.versions.filtering.DependencyFilter; +import org.codehaus.mojo.versions.filtering.WildcardMatcher; +import org.codehaus.mojo.versions.rewriting.ModifiedPomXMLEventReader; +import org.codehaus.mojo.versions.utils.CoreExtensionUtils; +import org.codehaus.mojo.versions.utils.DependencyBuilder; +import org.codehaus.mojo.versions.utils.SegmentUtils; +import org.codehaus.plexus.util.StringUtils; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +import static java.util.Optional.of; +import static org.codehaus.mojo.versions.api.Segment.MAJOR; + +/** + * Displays all build extensions that have newer versions available. + * Does not check the core extensions. + * + * @author Andrzej Jarmoniuk + * @since 2.15.0 + */ +@Mojo(name = "display-extension-updates", threadSafe = true) +public class DisplayExtensionUpdatesMojo extends AbstractVersionsDisplayMojo { + + // ------------------------------ FIELDS ------------------------------ + + /** + * The width to pad info messages. + * + * @since 1.0-alpha-1 + */ + private static final int INFO_PAD_SIZE = 72; + + /** + *

Specifies a comma-separated list of GAV patterns to consider + * when looking for updates. If the trailing parts of the GAV are omitted, then can assume any value.

+ * + *

The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges.

+ * + * Examples: {@code "mygroup:artifact:*"}, {@code "mygroup:artifact"}, {@code "mygroup"} + * + * @since 2.15.0 + */ + @Parameter(property = "extensionIncludes", defaultValue = WildcardMatcher.WILDCARD) + private List extensionIncludes; + + /** + *

Specifies a comma-separated list of GAV patterns to NOT consider + * when looking for updates. If the trailing parts of the GAV are omitted, then can assume any value.

+ * + *

This list is taken into account after {@link #extensionIncludes}

. + * + *

The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges.

+ * + * Examples: {@code "mygroup:artifact:*"}, {@code "mygroup:artifact"}, {@code "mygroup"} + * + * @since 2.15.0 + */ + @Parameter(property = "extensionExcludes") + private List extensionExcludes; + + /** + * Whether to allow the major version number to be changed. + * + * @since 2.15.0 + */ + @Parameter(property = "allowMajorUpdates", defaultValue = "true") + private boolean allowMajorUpdates = true; + + /** + *

Whether to allow the minor version number to be changed.

+ * + *

Note: {@code false} also implies {@linkplain #allowMajorUpdates} + * to be {@code false}

+ * + * @since 2.15.0 + */ + @Parameter(property = "allowMinorUpdates", defaultValue = "true") + private boolean allowMinorUpdates = true; + + /** + *

Whether to allow the incremental version number to be changed.

+ * + *

Note: {@code false} also implies {@linkplain #allowMajorUpdates} + * and {@linkplain #allowMinorUpdates} to be {@code false}

+ * + * @since 2.15.0 + */ + @Parameter(property = "allowIncrementalUpdates", defaultValue = "true") + private boolean allowIncrementalUpdates = true; + + /** + *

Whether to process core extensions. Default is {@code true}.

+ * @since 2.15.0 + */ + @Parameter(property = "processCoreExtensions", defaultValue = "true") + private boolean processCoreExtensions = true; + + /** + *

Whether to process build extensions. Default is {@code true}.

+ * @since 2.15.0 + */ + @Parameter(property = "processBuildExtensions", defaultValue = "true") + private boolean processBuildExtensions = true; + + /** + * Whether to show additional information such as extensions that do not need updating. Defaults to false. + * + * @since 2.15.0 + */ + @Parameter(property = "verbose", defaultValue = "false") + private boolean verbose; + + @Inject + public DisplayExtensionUpdatesMojo( + RepositorySystem repositorySystem, + org.eclipse.aether.RepositorySystem aetherRepositorySystem, + Map wagonMap, + Map changeRecorders) { + super(repositorySystem, aetherRepositorySystem, wagonMap, changeRecorders); + } + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + logInit(); + validateInput(); + + if (!processCoreExtensions && !processBuildExtensions) { + getLog().info("Neither core nor build extensions are to be processed. Nothing to do."); + return; + } + + DependencyFilter includeFilter = DependencyFilter.parseFrom(extensionIncludes); + DependencyFilter excludeFilter = DependencyFilter.parseFrom(extensionExcludes); + + Set dependencies; + try { + Stream extensions; + if (processCoreExtensions) { + extensions = CoreExtensionUtils.getCoreExtensions(session); + } else { + extensions = Stream.empty(); + } + if (processBuildExtensions) { + extensions = Stream.concat(extensions, session.getCurrentProject().getBuildExtensions().stream()); + } + + dependencies = extensions + .map(e -> DependencyBuilder.newBuilder() + .withGroupId(e.getGroupId()) + .withArtifactId(e.getArtifactId()) + .withVersion(e.getVersion()) + .build()) + .filter(includeFilter::matchersMatch) + .filter(excludeFilter::matchersDontMatch) + .collect(Collectors.toSet()); + } catch (IOException | XmlPullParserException e) { + throw new MojoExecutionException(e.getMessage()); + } + if (dependencies.isEmpty()) { + getLog().info("Extensions set filtered by include- and extensions-filters is empty. Nothing to do."); + return; + } + + try { + logUpdates(getHelper().lookupDependenciesUpdates(dependencies, true, true, allowSnapshots)); + } catch (VersionRetrievalException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + private Optional calculateUpdateScope() { + return of(SegmentUtils.determineUnchangedSegment( + allowMajorUpdates, allowMinorUpdates, allowIncrementalUpdates, getLog()) + .map(s -> Segment.of(s.value() + 1)) + .orElse(MAJOR)); + } + + private void logUpdates(Map updates) { + List withUpdates = new ArrayList<>(); + List usingCurrent = new ArrayList<>(); + for (ArtifactVersions versions : updates.values()) { + String left = " " + ArtifactUtils.versionlessKey(versions.getArtifact()) + " "; + final String current; + ArtifactVersion latest; + if (versions.isCurrentVersionDefined()) { + current = versions.getCurrentVersion().toString(); + latest = versions.getNewestUpdate(calculateUpdateScope(), allowSnapshots); + } else { + ArtifactVersion newestVersion = + versions.getNewestVersion(versions.getArtifact().getVersionRange(), allowSnapshots); + current = versions.getArtifact().getVersionRange().toString(); + latest = newestVersion == null + ? null + : versions.getNewestUpdate(newestVersion, calculateUpdateScope(), allowSnapshots); + if (latest != null + && ArtifactVersions.isVersionInRange( + latest, versions.getArtifact().getVersionRange())) { + latest = null; + } + } + String right = " " + (latest == null ? current : current + " -> " + latest); + List t = latest == null ? usingCurrent : withUpdates; + if (right.length() + left.length() + 3 > INFO_PAD_SIZE + getOutputLineWidthOffset()) { + t.add(left + "..."); + t.add(StringUtils.leftPad(right, INFO_PAD_SIZE + getOutputLineWidthOffset())); + + } else { + t.add(StringUtils.rightPad(left, INFO_PAD_SIZE + getOutputLineWidthOffset() - right.length(), ".") + + right); + } + } + + if (verbose) { + if (usingCurrent.isEmpty()) { + if (!withUpdates.isEmpty()) { + logLine(false, "No extensions are using the newest version."); + logLine(false, ""); + } + } else { + logLine(false, "The following extensions are using the newest version:"); + for (String s : usingCurrent) { + logLine(false, s); + } + logLine(false, ""); + } + } + + if (withUpdates.isEmpty()) { + if (!usingCurrent.isEmpty()) { + logLine(false, "No extensions have newer versions."); + logLine(false, ""); + } + } else { + logLine(false, "The following extensions have newer versions:"); + for (String withUpdate : withUpdates) { + logLine(false, withUpdate); + } + logLine(false, ""); + } + } + + /** + * @param pom the pom to update. + * @see AbstractVersionsUpdaterMojo#update(ModifiedPomXMLEventReader) + * @since 1.0-alpha-1 + */ + @Override + protected void update(ModifiedPomXMLEventReader pom) { + // do nothing + } +} diff --git a/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java new file mode 100644 index 000000000..6e6b6ab7c --- /dev/null +++ b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java @@ -0,0 +1,157 @@ +package org.codehaus.mojo.versions; + +/* + * Copyright MojoHaus and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; + +import org.apache.maven.model.Build; +import org.apache.maven.model.Model; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.project.MavenProject; +import org.codehaus.mojo.versions.utils.ExtensionBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.apache.maven.plugin.testing.ArtifactStubFactory.setVariableValueToObject; +import static org.codehaus.mojo.versions.utils.MockUtils.mockAetherRepositorySystem; +import static org.codehaus.mojo.versions.utils.MockUtils.mockMavenSession; +import static org.codehaus.mojo.versions.utils.MockUtils.mockRepositorySystem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; + +/** + * Basic tests for {@linkplain DisplayExtensionUpdatesMojo}. + * + * @author Andrzej Jarmoniuk + */ +public class DisplayExtensionUpdatesMojoTest { + private DisplayExtensionUpdatesMojo mojo; + private Path tempPath; + + @Before + public void setUp() throws IllegalAccessException, IOException { + mojo = new DisplayExtensionUpdatesMojo(mockRepositorySystem(), mockAetherRepositorySystem(), null, null); + mojo.project = new MavenProject() { + { + setModel(new Model() { + { + setGroupId("default-group"); + setArtifactId("default-artifact"); + setVersion("1.0.0"); + } + }); + } + }; + mojo.project.setRemoteArtifactRepositories(emptyList()); + mojo.project.setPluginArtifactRepositories(emptyList()); + mojo.session = mockMavenSession(mojo.project); + tempPath = Files.createTempFile("display-extension-updates-", ".log"); + mojo.outputFile = tempPath.toFile(); + mojo.outputEncoding = "UTF-8"; + setVariableValueToObject(mojo, "processCoreExtensions", false); + + mojo.setPluginContext(new HashMap() { + { + put( + "org.codehaus.mojo.versions.AbstractVersionsDisplayMojo.outputFile", + singleton(tempPath.toFile().getCanonicalPath())); + } + }); + } + + @After + public void tearDown() throws IOException { + Files.deleteIfExists(tempPath); + } + + @Test + public void testNoBuildExists() + throws MojoExecutionException, MojoFailureException, IllegalAccessException, IOException { + setVariableValueToObject(mojo, "extensionIncludes", singletonList("*")); + setVariableValueToObject(mojo, "extensionExcludes", emptyList()); + mojo.execute(); + } + + @Test + public void testIncludesMakesSetEmpty() + throws MojoExecutionException, MojoFailureException, IllegalAccessException, IOException { + setVariableValueToObject(mojo, "extensionIncludes", singletonList("other-group")); + setVariableValueToObject(mojo, "extensionExcludes", emptyList()); + + mojo.getProject().setBuild(new Build()); + mojo.getProject() + .getBuild() + .setExtensions(Collections.singletonList(ExtensionBuilder.newBuilder() + .withGroupId("default-group") + .withArtifactId("artifactA") + .withVersion("1.0.0") + .build())); + mojo.execute(); + + assertThat(Files.readAllLines(tempPath), empty()); + } + + @Test + public void testIncludesMakesSetNonEmpty() + throws MojoExecutionException, MojoFailureException, IllegalAccessException, IOException { + setVariableValueToObject(mojo, "extensionIncludes", singletonList("default-group")); + setVariableValueToObject(mojo, "extensionExcludes", emptyList()); + + mojo.getProject().setBuild(new Build()); + mojo.getProject() + .getBuild() + .setExtensions(Collections.singletonList(ExtensionBuilder.newBuilder() + .withGroupId("default-group") + .withArtifactId("artifactA") + .withVersion("1.0.0") + .build())); + mojo.execute(); + + assertThat( + String.join("", Files.readAllLines(tempPath)), + containsString("default-group:artifactA ... 1.0.0 -> 2.0.0")); + } + + @Test + public void testIncludesExcludesMakesSetEmpty() + throws MojoExecutionException, MojoFailureException, IllegalAccessException, IOException { + setVariableValueToObject(mojo, "extensionIncludes", singletonList("default-group")); + setVariableValueToObject(mojo, "extensionExcludes", singletonList("default-group:artifactA")); + + mojo.getProject().setBuild(new Build()); + mojo.getProject() + .getBuild() + .setExtensions(Collections.singletonList(ExtensionBuilder.newBuilder() + .withGroupId("default-group") + .withArtifactId("artifactA") + .withVersion("1.0.0") + .build())); + mojo.execute(); + + assertThat(Files.readAllLines(tempPath), empty()); + } +} diff --git a/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java b/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java index 92a1d0cab..f604c6f90 100644 --- a/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java +++ b/versions-test/src/main/java/org/codehaus/mojo/versions/utils/MockUtils.java @@ -152,11 +152,22 @@ public static RepositorySystem mockRepositorySystem() { * @return mocked {@link MavenSession} */ public static MavenSession mockMavenSession() { + MavenProject project = mock(MavenProject.class); + when(project.getRemotePluginRepositories()).thenReturn(emptyList()); + when(project.getRemoteProjectRepositories()).thenReturn(emptyList()); + return mockMavenSession(project); + } + + /** + * Creates a very simple mock of {@link MavenSession} + * by providing only a non-{@code null} implementation of its {@link MavenSession#getRepositorySession()} method. + * @param project {@link MavenProject} to link to + * @return mocked {@link MavenSession} + */ + public static MavenSession mockMavenSession(MavenProject project) { MavenSession session = mock(MavenSession.class); when(session.getRepositorySession()).thenReturn(mock(RepositorySystemSession.class)); - when(session.getCurrentProject()).thenReturn(mock(MavenProject.class)); - when(session.getCurrentProject().getRemotePluginRepositories()).thenReturn(emptyList()); - when(session.getCurrentProject().getRemoteProjectRepositories()).thenReturn(emptyList()); + when(session.getCurrentProject()).thenReturn(project); return session; } }