From d75f0370224ed81038388a937e2f120e7bd30ddf Mon Sep 17 00:00:00 2001
From: Andrzej Jarmoniuk <gh@jarmoniuk.nl>
Date: Tue, 24 Jan 2023 21:16:44 +0100
Subject: [PATCH] Issue #74: Add display-extension-updates

---
 pom.xml                                       |   7 +
 versions-common/pom.xml                       |   4 +
 .../versions/api/DefaultVersionsHelper.java   |  83 +++--
 .../mojo/versions/api/VersionsHelper.java     |  68 +++-
 .../versions/filtering/DependencyFilter.java  |  30 +-
 .../versions/utils/CoreExtensionUtils.java    |  64 ++++
 .../versions/utils/DependencyBuilder.java     |  32 +-
 .../mojo/versions/utils/ExtensionBuilder.java | 103 ++++++
 .../utils/CoreExtensionUtilsTest.java         |  77 +++++
 .../utils/core-extensions/.mvn/extensions.xml |  13 +
 .../invoker.properties                        |   8 +
 .../it-display-extension-updates-001/pom.xml  |  25 ++
 .../verify.groovy                             |  10 +
 .../.mvn/extensions.xml                       |  13 +
 .../invoker.properties                        |   8 +
 .../it-display-extension-updates-002/pom.xml  |  11 +
 .../verify.groovy                             |  10 +
 .../.mvn/extensions.xml                       |   8 +
 .../invoker.properties                        |   5 +
 .../it-display-extension-updates-003/pom.xml  |  21 ++
 .../verify.groovy                             |   7 +
 .../versions/DisplayExtensionUpdatesMojo.java | 293 ++++++++++++++++++
 .../DisplayExtensionUpdatesMojoTest.java      | 157 ++++++++++
 .../mojo/versions/utils/MockUtils.java        |  45 +--
 24 files changed, 1008 insertions(+), 94 deletions(-)
 create mode 100644 versions-common/src/main/java/org/codehaus/mojo/versions/utils/CoreExtensionUtils.java
 create mode 100644 versions-common/src/main/java/org/codehaus/mojo/versions/utils/ExtensionBuilder.java
 create mode 100644 versions-common/src/test/java/org/codehaus/mojo/versions/utils/CoreExtensionUtilsTest.java
 create mode 100644 versions-common/src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/.mvn/extensions.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-001/invoker.properties
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-001/pom.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-001/verify.groovy
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-002/.mvn/extensions.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-002/invoker.properties
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-002/pom.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-002/verify.groovy
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-003/.mvn/extensions.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-003/invoker.properties
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-003/pom.xml
 create mode 100644 versions-maven-plugin/src/it/it-display-extension-updates-003/verify.groovy
 create mode 100644 versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojo.java
 create mode 100644 versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojoTest.java

diff --git a/pom.xml b/pom.xml
index ea9d1135e8..d9a2750a02 100644
--- a/pom.xml
+++ b/pom.xml
@@ -126,6 +126,7 @@
     <maven.compiler.source>${mojo.java.target}</maven.compiler.source>
     <junitBomVersion>5.9.1</junitBomVersion>
     <mavenVersion>3.2.5</mavenVersion>
+    <mavenEmbedderVersion>3.3.9</mavenEmbedderVersion>
     <doxiaVersion>1.12.0</doxiaVersion>
     <doxia-sitetoolsVersion>1.11.1</doxia-sitetoolsVersion>
     <pluginVersion>${project.version}</pluginVersion>
@@ -177,6 +178,12 @@
         <artifactId>maven-compat</artifactId>
         <version>${mavenVersion}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.maven</groupId>
+        <artifactId>maven-embedder</artifactId>
+        <version>${mavenEmbedderVersion}</version>
+        <scope>provided</scope>
+      </dependency>
 
       <dependency>
         <groupId>org.apache.maven.enforcer</groupId>
diff --git a/versions-common/pom.xml b/versions-common/pom.xml
index 6276a72d71..a65e215b0e 100644
--- a/versions-common/pom.xml
+++ b/versions-common/pom.xml
@@ -50,6 +50,10 @@
       <groupId>org.apache.maven</groupId>
       <artifactId>maven-settings</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-embedder</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.fasterxml.woodstox</groupId>
       <artifactId>woodstox-core</artifactId>
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 d3844285c1..9a75757bf4 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
@@ -1,22 +1,18 @@
 package org.codehaus.mojo.versions.api;
 
 /*
- * 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
+ * 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
+ *    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.
+ * 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.BufferedInputStream;
@@ -45,6 +41,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 +86,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 +170,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<IgnoreVersion> ignoredVersions = getIgnoredVersions(artifact);
             if (!ignoredVersions.isEmpty() && getLog().isDebugEnabled()) {
                 getLog().debug("Found ignored versions: "
                         + ignoredVersions.stream().map(IgnoreVersion::toString).collect(Collectors.joining(", ")));
             }
+
+            final List<RemoteRepository> 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 +212,7 @@ public ArtifactVersions lookupArtifactVersions(
                                                                     .findFirst()
                                                                     .map(Restriction::toString))
                                                             .orElse("(,)")),
-                                            usePluginRepositories
-                                                    ? mavenSession
-                                                            .getCurrentProject()
-                                                            .getRemotePluginRepositories()
-                                                    : mavenSession
-                                                            .getCurrentProject()
-                                                            .getRemoteProjectRepositories(),
+                                            repositories,
                                             "lookupArtifactVersions"))
                             .getVersions()
                             .stream()
@@ -428,16 +443,20 @@ public ArtifactVersion createArtifactVersion(String version) {
         return DefaultArtifactVersionCache.of(version);
     }
 
-    @Override
     public Map<Dependency, ArtifactVersions> lookupDependenciesUpdates(
-            Set<Dependency> dependencies, boolean usePluginRepositories, boolean allowSnapshots)
+            Set<Dependency> dependencies,
+            boolean usePluginRepositories,
+            boolean useProjectRepositories,
+            boolean allowSnapshots)
             throws VersionRetrievalException {
         ExecutorService executor = Executors.newFixedThreadPool(LOOKUP_PARALLEL_THREADS);
         try {
             Map<Dependency, ArtifactVersions> dependencyUpdates = new TreeMap<>(DependencyComparator.INSTANCE);
             List<Future<? extends Pair<Dependency, ArtifactVersions>>> 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<? extends Pair<Dependency, ArtifactVersions>> details : futures) {
                 Pair<Dependency, ArtifactVersions> pair = details.get();
@@ -453,12 +472,22 @@ dependency, lookupDependencyUpdates(dependency, usePluginRepositories, allowSnap
         }
     }
 
+    @Override
+    public Map<Dependency, ArtifactVersions> lookupDependenciesUpdates(
+            Set<Dependency> 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 d10b4e0d92..0c3e2347e0 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
@@ -1,22 +1,18 @@
 package org.codehaus.mojo.versions.api;
 
 /*
- * 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
+ * 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
+ *    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.
+ * 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.Collection;
@@ -152,7 +148,7 @@ Artifact createDependencyArtifact(
      * <b>The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots.</b>
      *
      * @param artifact The artifact to look for versions of.
-     * @param usePluginRepositories <code>true</code> will consult the pluginRepositories, while <code>false</code> 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 +163,24 @@ ArtifactVersions lookupArtifactVersions(Artifact artifact, boolean usePluginRepo
      * <b>The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots.</b>
      *
      * @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.
+     * <b>The resulting {@link ArtifactVersions} instance will contain all versions, including snapshots.</b>
+     *
+     * @param artifact The artifact to look for versions of.
+     * @param versionRange versionRange to restrict the search, may be {@code null}
      * @param usePluginRepositories <code>true</code> will consult the pluginRepositories, while <code>false</code> will
      *            consult the repositories for normal dependencies.
      * @return The details of the available artifact versions.
@@ -190,18 +203,39 @@ Map<Dependency, ArtifactVersions> lookupDependenciesUpdates(
             Set<Dependency> 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<Dependency, ArtifactVersions> lookupDependenciesUpdates(
+            Set<Dependency> 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 4af2a4fbfa..d2808a0a76 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
@@ -1,22 +1,18 @@
 package org.codehaus.mojo.versions.filtering;
 
 /*
- * 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
+ * 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
+ *    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.
+ * 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.Collection;
@@ -66,10 +62,14 @@ public Set<Dependency> removingFrom(Collection<Dependency> 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<Dependency> filterBy(Collection<Dependency> dependencies, Predicate<Dependency> 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 0000000000..62638fac85
--- /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<Extension> 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 58ca87c2ff..6574f4b557 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
@@ -1,24 +1,20 @@
+package org.codehaus.mojo.versions.utils;
+
 /*
- * 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
+ * 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
+ *    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.
+ * 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.
  */
 
-package org.codehaus.mojo.versions.utils;
-
 import java.util.Optional;
 
 import org.apache.maven.model.Dependency;
@@ -136,7 +132,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 +152,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 0000000000..7d763606d2
--- /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<String> groupId = empty();
+    private Optional<String> artifactId = empty();
+    private Optional<String> version = empty();
+    private Map<Object, InputLocation> 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 0000000000..f0500f15dc
--- /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<Extension> 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 0000000000..172cdbc1f9
--- /dev/null
+++ b/versions-common/src/test/resources/org/codehaus/mojo/versions/utils/core-extensions/.mvn/extensions.xml
@@ -0,0 +1,13 @@
+<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
+  <extension>
+    <groupId>default-group</groupId>
+    <artifactId>artifactA</artifactId>
+    <version>1.0.0</version>
+  </extension>
+  <extension>
+    <groupId>default-group</groupId>
+    <artifactId>artifactB</artifactId>
+    <version>2.0.0</version>
+  </extension>
+</extensions>
\ 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 0000000000..5da846218c
--- /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 0000000000..fe8f43302d
--- /dev/null
+++ b/versions-maven-plugin/src/it/it-display-extension-updates-001/pom.xml
@@ -0,0 +1,25 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>localhost</groupId>
+  <artifactId>it-display-extension-updates-001</artifactId>
+
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <build>
+    <extensions>
+      <extension>
+        <groupId>localhost</groupId>
+        <artifactId>dummy-maven-plugin</artifactId>
+        <version>1.0</version>
+      </extension>
+      <extension>
+        <groupId>localhost</groupId>
+        <artifactId>dummy-api</artifactId>
+        <version>1.0</version>
+      </extension>
+    </extensions>
+  </build>
+</project>
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 0000000000..4f5fe3ab69
--- /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 0000000000..1f4f4fbac7
--- /dev/null
+++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/.mvn/extensions.xml
@@ -0,0 +1,13 @@
+<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
+  <extension>
+    <groupId>localhost</groupId>
+    <artifactId>dummy-maven-plugin</artifactId>
+    <version>1.0</version>
+  </extension>
+  <extension>
+    <groupId>localhost</groupId>
+    <artifactId>dummy-api</artifactId>
+    <version>1.0</version>
+  </extension>
+</extensions>
\ 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 0000000000..5da846218c
--- /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 0000000000..28820da695
--- /dev/null
+++ b/versions-maven-plugin/src/it/it-display-extension-updates-002/pom.xml
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>localhost</groupId>
+  <artifactId>it-display-extension-updates-002</artifactId>
+
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+</project>
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 0000000000..4f5fe3ab69
--- /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 0000000000..18a2a6a1d2
--- /dev/null
+++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/.mvn/extensions.xml
@@ -0,0 +1,8 @@
+<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
+  <extension>
+    <groupId>localhost</groupId>
+    <artifactId>dummy-api</artifactId>
+    <version>1.0</version>
+  </extension>
+</extensions>
\ 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 0000000000..0bd6e32e03
--- /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 0000000000..655a5ea9e1
--- /dev/null
+++ b/versions-maven-plugin/src/it/it-display-extension-updates-003/pom.xml
@@ -0,0 +1,21 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>localhost</groupId>
+  <artifactId>it-display-extension-updates-003</artifactId>
+
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <build>
+    <extensions>
+      <extension>
+        <groupId>localhost</groupId>
+        <artifactId>dummy-maven-plugin</artifactId>
+        <version>1.0</version>
+      </extension>
+    </extensions>
+  </build>
+
+</project>
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 0000000000..f2e88457db
--- /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 0000000000..d24c98b5fa
--- /dev/null
+++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/DisplayExtensionUpdatesMojo.java
@@ -0,0 +1,293 @@
+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 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 and core extensions that have newer versions available.
+ *
+ * @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;
+
+    /**
+     * <p>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.</p>
+     *
+     * <p>The wildcard "*" can be used as the only, first, last or both characters in each token.
+     * The version token does support version ranges.</p>
+     *
+     * Examples: {@code "mygroup:artifact:*"}, {@code "mygroup:artifact"}, {@code "mygroup"}
+     *
+     * @since 2.15.0
+     */
+    @Parameter(property = "extensionIncludes", defaultValue = WildcardMatcher.WILDCARD)
+    private List<String> extensionIncludes;
+
+    /**
+     * <p>Specifies a comma-separated list of GAV patterns to <b>NOT</b> consider
+     * when looking for updates. If the trailing parts of the GAV are omitted, then can assume any value.</p>
+     *
+     * <p>This list is taken into account <u>after</u> {@link #extensionIncludes}</p>.
+     *
+     * <p>The wildcard "*" can be used as the only, first, last or both characters in each token.
+     * The version token does support version ranges.</p>
+     *
+     * Examples: {@code "mygroup:artifact:*"}, {@code "mygroup:artifact"}, {@code "mygroup"}
+     *
+     * @since 2.15.0
+     */
+    @Parameter(property = "extensionExcludes")
+    private List<String> extensionExcludes;
+
+    /**
+     * Whether to allow the major version number to be changed.
+     *
+     * @since 2.15.0
+     */
+    @Parameter(property = "allowMajorUpdates", defaultValue = "true")
+    private boolean allowMajorUpdates = true;
+
+    /**
+     * <p>Whether to allow the minor version number to be changed.</p>
+     *
+     * <p><b>Note: {@code false} also implies {@linkplain #allowMajorUpdates}
+     * to be {@code false}</b></p>
+     *
+     * @since 2.15.0
+     */
+    @Parameter(property = "allowMinorUpdates", defaultValue = "true")
+    private boolean allowMinorUpdates = true;
+
+    /**
+     * <p>Whether to allow the incremental version number to be changed.</p>
+     *
+     * <p><b>Note: {@code false} also implies {@linkplain #allowMajorUpdates}
+     * and {@linkplain #allowMinorUpdates} to be {@code false}</b></p>
+     *
+     * @since 2.15.0
+     */
+    @Parameter(property = "allowIncrementalUpdates", defaultValue = "true")
+    private boolean allowIncrementalUpdates = true;
+
+    /**
+     * <p>Whether to process core extensions. Default is {@code true}.</p>
+     * @since 2.15.0
+     */
+    @Parameter(property = "processCoreExtensions", defaultValue = "true")
+    private boolean processCoreExtensions = true;
+
+    /**
+     * <p>Whether to process build extensions. Default is {@code true}.</p>
+     * @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<String, Wagon> wagonMap,
+            Map<String, ChangeRecorder> 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<Dependency> dependencies;
+        try {
+            Stream<Extension> 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<Segment> calculateUpdateScope() {
+        return of(SegmentUtils.determineUnchangedSegment(
+                        allowMajorUpdates, allowMinorUpdates, allowIncrementalUpdates, getLog())
+                .map(s -> Segment.of(s.value() + 1))
+                .orElse(MAJOR));
+    }
+
+    private void logUpdates(Map<Dependency, ArtifactVersions> updates) {
+        List<String> withUpdates = new ArrayList<>();
+        List<String> 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<String> 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 0000000000..6e6b6ab7c1
--- /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<String, Object>() {
+            {
+                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 92a1d0cab7..e08b9322ab 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
@@ -1,24 +1,20 @@
+package org.codehaus.mojo.versions.utils;
+
 /*
- * 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
+ * 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
+ *    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.
+ * 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.
  */
 
-package org.codehaus.mojo.versions.utils;
-
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -152,11 +148,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;
     }
 }