├── .mvn └── .gitkeep ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── maven.yml │ └── release-to-central.yml ├── src ├── main │ └── java │ │ └── com │ │ └── giovds │ │ ├── dto │ │ ├── MavenCentralResponse.java │ │ ├── Response.java │ │ └── DependencyResponse.java │ │ ├── DependenciesToQueryMapper.java │ │ ├── QueryClient.java │ │ ├── AverageAgeMojo.java │ │ └── CheckMojo.java └── test │ ├── java │ └── com │ │ └── giovds │ │ ├── TestLogger.java │ │ ├── DependenciesToQueryMapperTest.java │ │ ├── QueryClientTest.java │ │ ├── AverageAgeMojoTest.java │ │ ├── TestFakes.java │ │ └── CheckMojoTest.java │ └── resources │ └── __files │ └── exampleResponse.json ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── pom.xml /.mvn/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | **/.attach_pid* 4 | 5 | .DS_STORE 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "01:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/dto/MavenCentralResponse.java: -------------------------------------------------------------------------------- 1 | package com.giovds.dto; 2 | 3 | /** 4 | * The HTTP response from Maven Central 5 | * 6 | * @param response the response from Maven Central 7 | */ 8 | public record MavenCentralResponse(Response response) {} 9 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/dto/Response.java: -------------------------------------------------------------------------------- 1 | package com.giovds.dto; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * The dependencies and pagination information from Maven Central 7 | * 8 | * @param docs the dependencies from Maven Central 9 | */ 10 | public record Response(List docs) {} 11 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out the repository 15 | uses: actions/checkout@v4 16 | with: 17 | # Disabling shallow clone is recommended for improving relevancy of reporting 18 | fetch-depth: 0 19 | - name: Set up JDK 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: '17' 23 | distribution: 'adopt' 24 | cache: maven 25 | - name: Cache local Maven repository 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/.m2/repository 29 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: | 31 | ${{ runner.os }}-maven- 32 | - name: Build with Maven 33 | run: mvn -B verify 34 | -------------------------------------------------------------------------------- /src/test/java/com/giovds/TestLogger.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import org.apache.maven.plugin.logging.SystemStreamLog; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | class TestLogger extends SystemStreamLog { 9 | private final List infoLogs = new ArrayList<>(); 10 | private final List warningLogs = new ArrayList<>(); 11 | private final List errorLogs = new ArrayList<>(); 12 | 13 | @Override 14 | public void info(final CharSequence content) { 15 | infoLogs.add(content.toString()); 16 | } 17 | 18 | @Override 19 | public void warn(final CharSequence content) { 20 | warningLogs.add(content.toString()); 21 | } 22 | 23 | @Override 24 | public void error(final CharSequence content) { 25 | errorLogs.add(content.toString()); 26 | } 27 | 28 | public List getInfoLogs() { 29 | return infoLogs; 30 | } 31 | 32 | public List getWarningLogs() { 33 | return warningLogs; 34 | } 35 | 36 | public List getErrorLogs() { 37 | return errorLogs; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Giovanni van der Schelde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/dto/DependencyResponse.java: -------------------------------------------------------------------------------- 1 | package com.giovds.dto; 2 | 3 | import java.time.Instant; 4 | import java.time.LocalDate; 5 | import java.time.ZoneId; 6 | 7 | /** 8 | * The dependency returned by the Maven Central response 9 | * 10 | * @param id GAV (groupId:artifactId:version) 11 | * @param g groupId 12 | * @param a artifactId 13 | * @param v version 14 | * @param timestamp The date this GAV id was released to Maven Central 15 | * @param p packaging type 16 | */ 17 | public record DependencyResponse(String id, String g, String a, String v, long timestamp, String p) { 18 | /** 19 | * Return the timestamp as {@link LocalDate} 20 | * 21 | * @param timestamp the timestamp to convert 22 | * @return the {@link LocalDate} 23 | */ 24 | public static LocalDate getDateTime(long timestamp) { 25 | return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate(); 26 | } 27 | 28 | /** 29 | * Check if the dependency is a plugin 30 | * 31 | * @return true if the packaging type is "maven-plugin" 32 | */ 33 | public boolean isPlugin() { 34 | return "maven-plugin".equals(p); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering contributing to this project! Here are some guidelines to help you get started. 4 | 5 | ## Reporting Issues 6 | 7 | If you find a bug, have an idea for an enhancement, or want to start a discussion, please file an issue in the repository. Make sure to provide as much detail as possible to help us understand and address the issue. A reproduction of the issue, including steps to reproduce, is always helpful. 8 | 9 | ## Branch Naming 10 | 11 | When working on an issue, create a new branch starting with `issue-xx` where `xx` is the number of the issue. For example, if you are working on issue #42, your branch should be named `issue-42-the-issue-to-be-fixed`. 12 | 13 | ## Submitting a Pull Request 14 | 15 | 1. Run `mvn verify` to ensure your changes pass all tests and styling. 16 | 2. Commit your changes to your branch on your fork. [How to create a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) 17 | 3. Push your branch to your forked repository. 18 | 4. Submit a pull request (PR) from your forked repository to the main repository. 19 | 5. Reference the issue number in your PR description. 20 | 21 | ## Code Style 22 | 23 | Please ensure your code adheres to the project's coding standards and passes all tests. This helps maintain code quality and consistency. 24 | 25 | ## Thank You 26 | 27 | We appreciate your contributions and efforts to improve this project. Thank you for your support! 28 | -------------------------------------------------------------------------------- /.github/workflows/release-to-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish artifact 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: write 10 | packages: write 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v4 14 | with: 15 | # Disabling shallow clone is recommended for improving relevancy of reporting 16 | fetch-depth: 0 17 | - name: Setup agent for using Java and Maven 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | cache: maven 23 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 24 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 25 | server-id: central 26 | server-username: MAVEN_USERNAME 27 | server-password: MAVEN_CENTRAL_TOKEN 28 | - name: Package and verify 29 | env: 30 | MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 31 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 32 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }} 33 | run: | 34 | mvn \ 35 | -Pprepare-release \ 36 | -B \ 37 | deploy 38 | - name: Release 39 | env: 40 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} 41 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 42 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }} 43 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 45 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 46 | run: | 47 | mvn -Prelease jreleaser:full-release -------------------------------------------------------------------------------- /src/test/java/com/giovds/DependenciesToQueryMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import org.junit.jupiter.api.DisplayNameGeneration; 4 | import org.junit.jupiter.api.DisplayNameGenerator; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.IntStream; 12 | 13 | import static com.giovds.TestFakes.createDependency; 14 | import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; 15 | 16 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 17 | class DependenciesToQueryMapperTest { 18 | 19 | @Test 20 | void should_return_lucene_query_based_on_given_dependencies() { 21 | final var luceneQuery = "(g:com.giovds AND a:foo-artifact AND v:1.0.0) OR (g:org.apache.maven AND a:bar-artifact AND v:1.2.3)"; 22 | final var expectedUriQuery = luceneQuery.replace(' ', '+'); 23 | final var firstDep = createDependency("com.giovds", "foo-artifact", "1.0.0"); 24 | final var secondDep = createDependency("org.apache.maven", "bar-artifact", "1.2.3"); 25 | 26 | final List actualQuery = DependenciesToQueryMapper.mapToQueries(List.of(firstDep, secondDep)); 27 | 28 | assertThat(actualQuery).hasSize(1) 29 | .contains(expectedUriQuery); 30 | } 31 | 32 | @ParameterizedTest 33 | @ValueSource(ints = {1, 2, 19, 20, 25, 30, 40, 59, 60}) 34 | void should_return_multiple_lucene_query_based_on_given_dependencies_when_exceeding_limit(final int numberOfDeps) { 35 | final var dependencies = IntStream.rangeClosed(1, numberOfDeps) 36 | .mapToObj(i -> String.format("bar-artifact-%d", i)) 37 | .map(artifactId -> createDependency("org.apache.maven", artifactId, "1.2.3")) 38 | .collect(Collectors.toSet()); 39 | 40 | final var actualQueries = DependenciesToQueryMapper.mapToQueries(dependencies); 41 | 42 | final var expectedQueryCount = (int) Math.ceil((double) numberOfDeps / 20); 43 | assertThat(actualQueries).hasSize(expectedQueryCount).allMatch(query -> query.endsWith(")")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Outdated Maven Plugin 2 | 3 | > Stay up-to-date and secure with The Outdated Maven Plugin! 4 | 5 | The Outdated Maven Plugin is a tool designed to help developers identify outdated dependencies in their Maven projects. 6 | By scanning the dependencies of your project, this plugin determines if they are no longer actively maintained 7 | based on a user-defined threshold of inactivity in years. This ensures that your project remains up-to-date with the 8 | latest and most secure versions of its dependencies. 9 | 10 | ## Usage 11 | 12 | You can use the plugin as standalone for a quick check by simply running the following command in your favourite 13 | project:\ 14 | `mvn com.giovds:outdated-maven-plugin:check -Dyears= -DincludePlugins=` 15 | 16 | Or you can use the plugin to get the average and total age of all the dependencies in your project:\ 17 | `mvn com.giovds:outdated-maven-plugin:average` 18 | 19 | Alternatively, you can integrate the plugin into your Maven project by adding the following configuration to your `pom.xml` file: 20 | ```xml 21 | 22 | 23 | 24 | 25 | com.giovds 26 | outdated-maven-plugin 27 | 1.4.0 28 | 29 | 30 | 1 31 | 32 | false 33 | 34 | false 35 | 36 | 37 | 38 | outdated-check 39 | 40 | check 41 | 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | ## Contributing 50 | 51 | Contributions are welcome! \ 52 | Please verify if a similar issue is not reported already. If it is not create one, if it is. 53 | 54 | ## License 55 | 56 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 57 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/DependenciesToQueryMapper.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import org.apache.maven.model.Dependency; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | /** 10 | * Converts Maven {@link Dependency} to a single Lucene query that can be sent to Maven Central. 11 | * This in order to not spam the endpoint with a request per dependency. 12 | */ 13 | public class DependenciesToQueryMapper { 14 | private static final int maxItemsPerQuery = 20; 15 | private static final String JOIN_CHARS = "+OR+"; 16 | 17 | private DependenciesToQueryMapper() { 18 | } 19 | 20 | /** 21 | * Creates multiple Lucene queries from 22 | * a collection of {@link org.apache.maven.project.MavenProject} {@link Dependency} 23 | * 24 | * @param dependencies A collection of Maven {@link Dependency} to convert to query params 25 | * @return A {@link List} of Lucene queries of {@link String} with spaces escaped by '+' 26 | */ 27 | public static List mapToQueries(final Collection dependencies) { 28 | final List result = new ArrayList<>(); 29 | 30 | int itemsInQuery = 0; 31 | StringBuilder query = new StringBuilder(); 32 | for (Dependency dependency : dependencies) { 33 | final String nextQueryParam = GroupArtifactIdMapper.toQueryString(dependency); 34 | query.append(nextQueryParam); 35 | itemsInQuery++; 36 | 37 | if (itemsInQuery == maxItemsPerQuery) { 38 | result.add(query.toString()); 39 | itemsInQuery = 0; 40 | query = new StringBuilder(); 41 | } else { 42 | query.append(JOIN_CHARS); 43 | } 44 | } 45 | 46 | removeTrailingJoin(query); 47 | if (!query.isEmpty()) { 48 | result.add(query.toString()); 49 | } 50 | 51 | return result; 52 | } 53 | 54 | private static void removeTrailingJoin(final StringBuilder query) { 55 | if (query.length() > JOIN_CHARS.length()) { 56 | query.setLength(query.length() - JOIN_CHARS.length()); 57 | } 58 | } 59 | 60 | private static class GroupArtifactIdMapper { 61 | private static String toQueryString(final String groupId, final String artifactId, final String version) { 62 | return String.format("(g:%s+AND+a:%s+AND+v:%s)", groupId, artifactId, version); 63 | } 64 | 65 | public static String toQueryString(final Dependency dependency) { 66 | return GroupArtifactIdMapper.toQueryString(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/giovds/QueryClientTest.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import com.giovds.dto.DependencyResponse; 4 | import com.github.tomakehurst.wiremock.http.Fault; 5 | import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 6 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 7 | import org.apache.maven.plugin.MojoExecutionException; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 14 | import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; 15 | import static com.github.tomakehurst.wiremock.client.WireMock.forbidden; 16 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 17 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 20 | 21 | @WireMockTest 22 | class QueryClientTest { 23 | 24 | @Test 25 | void should_return_mapped_result_when_endpoint_called(final WireMockRuntimeInfo wmRuntimeInfo) throws MojoExecutionException { 26 | stubFor(get(anyUrl()) 27 | .willReturn(aResponse() 28 | .withBodyFile("exampleResponse.json"))); 29 | 30 | final Set result = new QueryClient(wmRuntimeInfo.getHttpBaseUrl()).search(List.of("any-query")); 31 | 32 | assertThat(result).isNotEmpty(); 33 | assertThat(result).hasSize(6); 34 | assertThat(result).contains( 35 | new DependencyResponse("org.apache.maven:maven-core:3.9.8", "org.apache.maven", "maven-core", "3.9.8", 1718267050000L, "jar"), 36 | new DependencyResponse("org.apache.maven.plugin-tools:maven-plugin-annotations:3.13.1", "org.apache.maven.plugin-tools", "maven-plugin-annotations", "3.13.1", 1716884186000L, "jar"), 37 | new DependencyResponse("org.apache.maven.plugins:maven-help-plugin:3.5.1", "org.apache.maven.plugins", "maven-help-plugin", "3.5.1", 1718267050000L, "maven-plugin") 38 | ); 39 | } 40 | 41 | @Test 42 | void should_throw_exception_when_failed_to_connect(final WireMockRuntimeInfo wmRuntimeInfo) { 43 | stubFor(get(anyUrl()) 44 | .willReturn(aResponse() 45 | .withFault(Fault.CONNECTION_RESET_BY_PEER))); 46 | 47 | assertThatThrownBy(() -> new QueryClient(wmRuntimeInfo.getHttpBaseUrl()).search(List.of("any-query"))) 48 | .isInstanceOf(MojoExecutionException.class); 49 | } 50 | 51 | @Test 52 | void should_report_error_when_failed_to_connect(final WireMockRuntimeInfo wmRuntimeInfo) { 53 | final String forbiddenBody = """ 54 | 55 | 403 Forbidden 56 | 57 |

403 Forbidden

58 | 59 | 60 | """; 61 | stubFor(get(anyUrl()) 62 | .willReturn(forbidden() 63 | .withBody(forbiddenBody))); 64 | 65 | assertThatThrownBy(() -> new QueryClient(wmRuntimeInfo.getHttpBaseUrl()).search(List.of("any-query"))) 66 | .isInstanceOf(MojoExecutionException.class) 67 | .rootCause() 68 | .hasMessageContaining(forbiddenBody); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/giovds/AverageAgeMojoTest.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import org.apache.maven.plugin.MojoExecutionException; 4 | import org.junit.jupiter.api.DisplayNameGeneration; 5 | import org.junit.jupiter.api.DisplayNameGenerator; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.time.LocalDate; 9 | 10 | import static com.giovds.TestFakes.createProjectWithDependencies; 11 | import static com.giovds.TestFakes.createDependency; 12 | import static com.giovds.TestFakes.mockClientResponseForDependency; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | 16 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 17 | class AverageAgeMojoTest { 18 | private final QueryClient client = mock(QueryClient.class); 19 | private final AverageAgeMojo mojo = new AverageAgeMojo(client); 20 | 21 | @Test 22 | void should_not_perform_query_when_no_dependencies() throws MojoExecutionException { 23 | // Arrange 24 | var project = createProjectWithDependencies(); 25 | 26 | var logger = new TestLogger(); 27 | 28 | // Act 29 | mojo.setLog(logger); 30 | mojo.setProject(project); 31 | mojo.execute(); 32 | 33 | // Assert 34 | assertThat(logger.getInfoLogs()).containsExactly("No dependencies found"); 35 | } 36 | 37 | @Test 38 | void should_not_consider_dependencies_with_same_groupId() throws MojoExecutionException { 39 | // Arrange 40 | var dependency1 = createDependency("com.external", "external-library", "1.0.0"); 41 | var dependency2 = createDependency("com.giovds", "test-library", "1.0.0"); 42 | var project = createProjectWithDependencies("com.giovds", "test-app", "1.0.0", dependency1, dependency2); 43 | mockClientResponseForDependency(client, dependency1, LocalDate.now().minusYears(1)); 44 | 45 | var logger = new TestLogger(); 46 | 47 | // Act 48 | mojo.setLog(logger); 49 | mojo.setProject(project); 50 | mojo.execute(); 51 | 52 | // Assert 53 | assertThat(logger.getInfoLogs()).contains( 54 | "Dependency com.external:external-library:1.0.0 is 365 days old", 55 | "Total age: 365 days", 56 | "Average age: 365 days" 57 | ); 58 | } 59 | 60 | @Test 61 | void should_not_consider_dependencies_with_test_scope() throws MojoExecutionException { 62 | // Arrange 63 | var dependency1 = createDependency("com.external", "external-library-1", "1.0.0"); 64 | var dependency2 = createDependency("com.external", "external-library-2", "1.0.0"); 65 | dependency2.setScope("test"); 66 | var project = createProjectWithDependencies("com.giovds", "external-app", "1.0.0", dependency1, dependency2); 67 | mockClientResponseForDependency(client, dependency1, LocalDate.now().minusYears(1)); 68 | 69 | var logger = new TestLogger(); 70 | 71 | // Act 72 | mojo.setLog(logger); 73 | mojo.setProject(project); 74 | mojo.execute(); 75 | 76 | // Assert 77 | assertThat(logger.getInfoLogs()).noneMatch(line -> line.contains("external-library-2:1.0.0 is ")); 78 | assertThat(logger.getInfoLogs()).contains( 79 | "Dependency com.external:external-library-1:1.0.0 is 365 days old", 80 | "Total age: 365 days", 81 | "Average age: 365 days" 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/QueryClient.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import com.fasterxml.jackson.jr.ob.JSON; 4 | import com.giovds.dto.DependencyResponse; 5 | import com.giovds.dto.MavenCentralResponse; 6 | import org.apache.maven.plugin.MojoExecutionException; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.net.URI; 11 | import java.net.http.HttpClient; 12 | import java.net.http.HttpRequest; 13 | import java.net.http.HttpResponse; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | 19 | /** 20 | * Search Maven Central with a Lucene query 21 | */ 22 | public class QueryClient { 23 | private final String main_uri; 24 | private static final HttpClient client = HttpClient.newHttpClient(); 25 | 26 | /** 27 | * Visible for testing purposes 28 | */ 29 | QueryClient(final String uri) { 30 | this.main_uri = uri; 31 | } 32 | 33 | /** 34 | * Defaults to connect to actual Maven Central endpoint 35 | */ 36 | public QueryClient() { 37 | this("https://search.maven.org/solrsearch/select"); 38 | } 39 | 40 | /** 41 | * Send a GET request to Maven Central with the provided query to 42 | * 43 | * @param queries The Lucene query as {@link String} 44 | * @return A set of dependencies returned by Maven Central mapped to {@link DependencyResponse} 45 | * @throws MojoExecutionException when something failed when sending the request 46 | */ 47 | public Set search(final List queries) throws MojoExecutionException { 48 | try { 49 | final Set allDependencies = new HashSet<>(); 50 | for (final String query : queries) { 51 | final HttpRequest request = buildHttpRequest(query); 52 | allDependencies.addAll(client.send(request, new SearchResponseBodyHandler()).body()); 53 | } 54 | return allDependencies; 55 | } catch (Exception e) { 56 | throw new MojoExecutionException("Failed to connect.", e); 57 | } 58 | } 59 | 60 | private HttpRequest buildHttpRequest(final String query) { 61 | final String uri = String.format("%s?q=%s&wt=json", main_uri, query); 62 | return HttpRequest.newBuilder() 63 | .GET() 64 | .version(HttpClient.Version.HTTP_2) 65 | .uri(URI.create(uri)) 66 | .build(); 67 | } 68 | 69 | private static class SearchResponseBodyHandler implements HttpResponse.BodyHandler> { 70 | @Override 71 | public HttpResponse.BodySubscriber> apply(final HttpResponse.ResponseInfo responseInfo) { 72 | if (responseInfo.statusCode() > 201) { 73 | return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), s -> { 74 | throw new RuntimeException("Search failed: status: " + responseInfo.statusCode() + " body: " + s); 75 | }); 76 | } 77 | HttpResponse.BodySubscriber stream = HttpResponse.BodySubscribers.ofInputStream(); 78 | return HttpResponse.BodySubscribers.mapping(stream, this::toSearchResponse); 79 | } 80 | 81 | Set toSearchResponse(final InputStream inputStream) { 82 | try (final InputStream input = inputStream) { 83 | final MavenCentralResponse mavenCentralResponse = JSON.std.beanFrom(MavenCentralResponse.class, input); 84 | return new HashSet<>(mavenCentralResponse.response().docs()); 85 | } catch (IOException e) { 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/resources/__files/exampleResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseHeader": { 3 | "status": 0, 4 | "QTime": 132, 5 | "params": { 6 | "q": "(g:org.apache.maven AND a:maven-core AND v:3.9.8) OR (g:org.apache.maven AND a:maven-plugin-api AND v:3.9.8) OR (g:com.fasterxml.jackson.jr AND a:jackson-jr-objects AND v:2.17.0) OR (g:org.apache.commons AND a:commons-collections4 AND v:4.4) OR (g:org.apache.maven.plugin-tools AND a:maven-plugin-annotations AND v:3.13.1) OR (g:org.apache.maven.plugins AND a:maven-help-plugin AND v:3.5.1)", 7 | "core": "", 8 | "indent": "off", 9 | "fl": "id,g,a,v,p,ec,timestamp,tags", 10 | "start": "", 11 | "sort": "score desc,timestamp desc,g asc,a asc,v desc", 12 | "rows": "20", 13 | "wt": "json", 14 | "version": "2.2" 15 | } 16 | }, 17 | "response": { 18 | "numFound": 6, 19 | "start": 0, 20 | "docs": [ 21 | { 22 | "id": "org.apache.maven.plugin-tools:maven-plugin-annotations:3.13.1", 23 | "g": "org.apache.maven.plugin-tools", 24 | "a": "maven-plugin-annotations", 25 | "v": "3.13.1", 26 | "p": "jar", 27 | "timestamp": 1716884186000, 28 | "ec": [ 29 | "-cyclonedx.json", 30 | "-sources.jar", 31 | "-cyclonedx.xml", 32 | ".pom", 33 | "-javadoc.jar", 34 | ".jar" 35 | ], 36 | "tags": [ 37 | "java", 38 | "mojos", 39 | "annotations" 40 | ] 41 | }, 42 | { 43 | "id": "org.apache.maven:maven-plugin-api:3.9.8", 44 | "g": "org.apache.maven", 45 | "a": "maven-plugin-api", 46 | "v": "3.9.8", 47 | "p": "jar", 48 | "timestamp": 1718267011000, 49 | "ec": [ 50 | "-cyclonedx.json", 51 | "-sources.jar", 52 | "-cyclonedx.xml", 53 | ".pom", 54 | "-javadoc.jar", 55 | ".jar" 56 | ], 57 | "tags": [ 58 | "development", 59 | "plugins", 60 | "mojos" 61 | ] 62 | }, 63 | { 64 | "id": "org.apache.maven:maven-core:3.9.8", 65 | "g": "org.apache.maven", 66 | "a": "maven-core", 67 | "v": "3.9.8", 68 | "p": "jar", 69 | "timestamp": 1718267050000, 70 | "ec": [ 71 | "-cyclonedx.json", 72 | "-sources.jar", 73 | "-cyclonedx.xml", 74 | ".pom", 75 | "-javadoc.jar", 76 | ".jar" 77 | ], 78 | "tags": [ 79 | "core", 80 | "maven", 81 | "classes" 82 | ] 83 | }, 84 | { 85 | "id": "com.fasterxml.jackson.jr:jackson-jr-objects:2.17.0", 86 | "g": "com.fasterxml.jackson.jr", 87 | "a": "jackson-jr-objects", 88 | "v": "2.17.0", 89 | "p": "bundle", 90 | "timestamp": 1710280484611, 91 | "ec": [ 92 | ".module", 93 | "-sources.jar", 94 | ".pom", 95 | "-javadoc.jar", 96 | ".jar" 97 | ], 98 | "tags": [ 99 | "other", 100 | "data", 101 | "additional", 102 | "binding", 103 | "generator", 104 | "simple", 105 | "content", 106 | "dependencies", 107 | "that", 108 | "core", 109 | "streaming", 110 | "jackson", 111 | "provides", 112 | "builds", 113 | "builder", 114 | "style", 115 | "directly" 116 | ] 117 | }, 118 | { 119 | "id": "org.apache.commons:commons-collections4:4.4", 120 | "g": "org.apache.commons", 121 | "a": "commons-collections4", 122 | "v": "4.4", 123 | "p": "jar", 124 | "timestamp": 1562350218000, 125 | "ec": [ 126 | "-sources.jar", 127 | "-javadoc.jar", 128 | "-test-sources.jar", 129 | "-tests.jar", 130 | ".jar", 131 | ".pom" 132 | ], 133 | "tags": [ 134 | "that", 135 | "extend", 136 | "commons", 137 | "package", 138 | "apache", 139 | "contains", 140 | "java", 141 | "types", 142 | "augment", 143 | "framework", 144 | "collections" 145 | ] 146 | }, 147 | { 148 | "id": "org.apache.maven.plugins:maven-help-plugin:3.5.1", 149 | "g": "org.apache.maven.plugins", 150 | "a": "maven-help-plugin", 151 | "v": "3.5.1", 152 | "p": "maven-plugin", 153 | "timestamp": 1718267050000, 154 | "ec": [ 155 | "-sources.jar", 156 | "-javadoc.jar", 157 | ".pom", 158 | ".jar" 159 | ], 160 | "tags": [ 161 | "maven", 162 | "plugin", 163 | "help" 164 | ] 165 | } 166 | ] 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/com/giovds/TestFakes.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import com.giovds.dto.DependencyResponse; 4 | import org.apache.maven.artifact.Artifact; 5 | import org.apache.maven.artifact.DefaultArtifact; 6 | import org.apache.maven.artifact.handler.DefaultArtifactHandler; 7 | import org.apache.maven.model.Dependency; 8 | import org.apache.maven.plugin.MojoExecutionException; 9 | import org.apache.maven.project.MavenProject; 10 | 11 | import java.time.LocalDate; 12 | import java.time.ZoneOffset; 13 | import java.util.List; 14 | import java.util.Set; 15 | import java.util.stream.Collectors; 16 | 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.when; 19 | 20 | class TestFakes { 21 | public static Dependency createDependency() { 22 | return createDependency("com.giovds", "test-example", "1.0.0"); 23 | } 24 | 25 | public static Dependency createDependency(String groupId, String artifactId, String version) { 26 | final Dependency dependency = new Dependency(); 27 | dependency.setGroupId(groupId); 28 | dependency.setArtifactId(artifactId); 29 | dependency.setVersion(version); 30 | dependency.setScope("compile"); 31 | 32 | return dependency; 33 | } 34 | 35 | public static Artifact createPlugin(String groupId, String artifactId, String version) { 36 | return new DefaultArtifact( 37 | groupId, 38 | artifactId, 39 | version, 40 | "build", 41 | "maven-plugin", 42 | null, 43 | new DefaultArtifactHandler("maven-plugin") 44 | ); 45 | } 46 | 47 | public static MavenProject createProjectWithDependencies(String groupId, String artifactId, String version, Dependency... dependencies) { 48 | final MavenProject project = new MavenProject(); 49 | project.setPackaging("jar"); 50 | project.setGroupId(groupId); 51 | project.setArtifactId(artifactId); 52 | project.setVersion(version); 53 | project.setDependencies(List.of(dependencies)); 54 | // Translate dependencies into (resolved) artifacts 55 | project.setArtifacts(dependenciesToArtifacts(project.getDependencies())); 56 | return project; 57 | } 58 | 59 | public static MavenProject createProjectWithPlugins(String groupId, String artifactId, String version, Artifact... plugins) { 60 | final MavenProject project = new MavenProject(); 61 | project.setPackaging("jar"); 62 | project.setGroupId(groupId); 63 | project.setArtifactId(artifactId); 64 | project.setVersion(version); 65 | project.setPluginArtifacts(Set.of(plugins)); 66 | return project; 67 | } 68 | 69 | private static Set dependenciesToArtifacts(List dependencies) { 70 | return dependencies.stream() 71 | .map(dependency -> new DefaultArtifact( 72 | dependency.getGroupId(), 73 | dependency.getArtifactId(), 74 | dependency.getVersion(), 75 | dependency.getScope(), 76 | dependency.getType(), 77 | dependency.getClassifier(), 78 | new DefaultArtifactHandler(dependency.getType()) 79 | )) 80 | .collect(Collectors.toSet()); 81 | } 82 | 83 | public static MavenProject createProjectWithDependencies(Dependency... dependencies) { 84 | return createProjectWithDependencies("com.giovds", "test-project", "1.0.0", dependencies); 85 | } 86 | 87 | public static void mockClientResponseForDependency(QueryClient client, Dependency dependency, LocalDate date) throws MojoExecutionException { 88 | var epochMilliseconds = date.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000; 89 | var response = new DependencyResponse(createDependencyKey(dependency), dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), epochMilliseconds, "jar"); 90 | when(client.search(any())).thenReturn(Set.of(response)); 91 | } 92 | 93 | public static void mockClientResponseForPlugin(QueryClient client, Artifact plugin, LocalDate date) throws MojoExecutionException { 94 | var epochMilliseconds = date.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000; 95 | var response = new DependencyResponse(createPluginKey(plugin), plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion(), epochMilliseconds, "maven-plugin"); 96 | when(client.search(any())).thenReturn(Set.of(response)); 97 | } 98 | 99 | private static String createDependencyKey(final Dependency dependency) { 100 | return String.format("%s:%s:%s", dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); 101 | } 102 | 103 | private static String createPluginKey(final Artifact plugin) { 104 | return String.format("%s:%s:%s", plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/AverageAgeMojo.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import com.giovds.dto.DependencyResponse; 4 | import org.apache.maven.artifact.Artifact; 5 | import org.apache.maven.model.Dependency; 6 | import org.apache.maven.plugin.AbstractMojo; 7 | import org.apache.maven.plugin.MojoExecutionException; 8 | import org.apache.maven.plugins.annotations.LifecyclePhase; 9 | import org.apache.maven.plugins.annotations.Mojo; 10 | import org.apache.maven.plugins.annotations.Parameter; 11 | import org.apache.maven.plugins.annotations.ResolutionScope; 12 | import org.apache.maven.project.MavenProject; 13 | 14 | import java.time.LocalDate; 15 | import java.time.Period; 16 | import java.util.Comparator; 17 | import java.util.List; 18 | import java.util.Set; 19 | import java.util.stream.Collectors; 20 | 21 | import static com.giovds.dto.DependencyResponse.getDateTime; 22 | 23 | /** 24 | * This plugin goal prints the total and average age of all project dependencies. It excludes dependencies with the 25 | * same
groupId
(assuming they are part of the same project, they would skew the outcome). 26 | */ 27 | @Mojo(name = "average", 28 | defaultPhase = LifecyclePhase.VERIFY, 29 | requiresOnline = true, 30 | requiresProject = true, 31 | requiresDependencyResolution = ResolutionScope.COMPILE) 32 | public class AverageAgeMojo extends AbstractMojo { 33 | private QueryClient client = new QueryClient(); 34 | 35 | @Parameter(readonly = true, required = true, defaultValue = "${project}") 36 | private MavenProject project; 37 | 38 | private final LocalDate today = LocalDate.now(); 39 | 40 | /** 41 | * Necessary for Maven as there already is an all-arguments constructor for testing purposes. 42 | */ 43 | public AverageAgeMojo() { 44 | } 45 | 46 | @Override 47 | public void execute() throws MojoExecutionException { 48 | final Set dependenciesToConsider = project.getArtifacts().stream() 49 | .peek(artifact -> info("Found artifact %s", artifact.getId())) 50 | .filter(this::isCompileTimeDependency) 51 | .peek(artifact -> info("Found compile-time artifact %s", artifact.getId())) 52 | .filter(this::isDependencyOutsideProject) 53 | .peek(artifact -> info("Found compile-time artifact outside this project %s", artifact.getId())) 54 | .map(this::convertToDependency) 55 | .collect(Collectors.toSet()); 56 | 57 | if (dependenciesToConsider.isEmpty()) { 58 | info("No dependencies found"); 59 | return; 60 | } 61 | 62 | final List queries = DependenciesToQueryMapper.mapToQueries(dependenciesToConsider); 63 | debug("Queries: %s", queries); 64 | 65 | final Set results = client.search(queries); 66 | debug("Results: %s", results); 67 | 68 | final var stats = results.stream() 69 | .sorted(Comparator.comparing(DependencyResponse::id)) 70 | .mapToLong(dependency -> { 71 | final long ageInDays = this.calculateDependencyAge(dependency); 72 | info("Dependency %s is %d days old", dependency.id(), ageInDays); 73 | return ageInDays; 74 | }) 75 | .summaryStatistics(); 76 | 77 | info("Found %d dependencies", stats.getCount()); 78 | info("Total age: %d days", stats.getSum()); 79 | info("Average age: %d days", (int) stats.getAverage()); 80 | } 81 | 82 | private Dependency convertToDependency(Artifact artifact) { 83 | var result = new Dependency(); 84 | result.setGroupId(artifact.getGroupId()); 85 | result.setArtifactId(artifact.getArtifactId()); 86 | result.setVersion(artifact.getVersion()); 87 | result.setClassifier(artifact.getClassifier()); 88 | result.setType(artifact.getType()); 89 | return result; 90 | } 91 | 92 | private long calculateDependencyAge(DependencyResponse dependency) { 93 | var between = Period.between(getDateTime(dependency.timestamp()), today); 94 | return 365L * between.getYears() + (between.getMonths() * 365L / 12L) + between.getDays(); 95 | } 96 | 97 | private boolean isCompileTimeDependency(Artifact artifact) { 98 | return Artifact.SCOPE_COMPILE.equals(artifact.getScope()) 99 | || Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) 100 | || Artifact.SCOPE_SYSTEM.equals(artifact.getScope()); 101 | } 102 | 103 | private boolean isDependencyOutsideProject(Artifact artifact) { 104 | return !project.getGroupId().equals(artifact.getGroupId()); 105 | } 106 | 107 | // 108 | // Utilities for logging 109 | // 110 | 111 | private void debug(String message, Object... args) { 112 | if (getLog().isDebugEnabled()) { 113 | getLog().debug(String.format(message, args)); 114 | } 115 | } 116 | 117 | private void info(String message, Object... args) { 118 | if (getLog().isInfoEnabled()) { 119 | getLog().info(String.format(message, args)); 120 | } 121 | } 122 | 123 | // 124 | // Only for testing 125 | // 126 | 127 | AverageAgeMojo(final QueryClient client) { 128 | this.client = client; 129 | } 130 | 131 | void setProject(final MavenProject project) { 132 | this.project = project; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/giovds/CheckMojo.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import com.giovds.dto.DependencyResponse; 4 | import org.apache.maven.artifact.Artifact; 5 | import org.apache.maven.model.Dependency; 6 | import org.apache.maven.plugin.AbstractMojo; 7 | import org.apache.maven.plugin.MojoExecutionException; 8 | import org.apache.maven.plugin.MojoFailureException; 9 | import org.apache.maven.plugins.annotations.LifecyclePhase; 10 | import org.apache.maven.plugins.annotations.Mojo; 11 | import org.apache.maven.plugins.annotations.Parameter; 12 | import org.apache.maven.plugins.annotations.ResolutionScope; 13 | import org.apache.maven.project.MavenProject; 14 | 15 | import java.time.LocalDate; 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Set; 21 | 22 | import static com.giovds.dto.DependencyResponse.getDateTime; 23 | 24 | /** 25 | * This plugin goal determines if the project dependencies are no longer actively maintained 26 | * based on a user-defined threshold of inactivity in years. 27 | */ 28 | @Mojo(name = "check", 29 | defaultPhase = LifecyclePhase.TEST_COMPILE, 30 | requiresOnline = true, 31 | requiresProject = true, 32 | requiresDependencyResolution = ResolutionScope.TEST) 33 | public class CheckMojo extends AbstractMojo { 34 | private QueryClient client = new QueryClient(); 35 | 36 | /** 37 | * Required for initialization by Maven 38 | */ 39 | public CheckMojo() { 40 | } 41 | 42 | /** 43 | * Visible for testing purposes 44 | */ 45 | CheckMojo(final QueryClient client) { 46 | this.client = client; 47 | } 48 | 49 | @Parameter(property = "shouldFailBuild") 50 | private boolean shouldFailBuild = false; 51 | 52 | @Parameter(property = "years") 53 | private int years = 1; 54 | 55 | @Parameter(property = "includePlugins") 56 | private boolean includePlugins = false; 57 | 58 | @Parameter(readonly = true, required = true, defaultValue = "${project}") 59 | private MavenProject project; 60 | 61 | @Override 62 | public void execute() throws MojoExecutionException, MojoFailureException { 63 | final Set dependenciesToConsider = new HashSet<>(project.getDependencies()); 64 | if (includePlugins) { 65 | dependenciesToConsider.addAll(mapArtifactsToDependencies(project.getPluginArtifacts())); 66 | } 67 | 68 | if (dependenciesToConsider.isEmpty()) { 69 | // When building a POM without any dependencies or plugins there will be nothing to query. 70 | return; 71 | } 72 | 73 | final List queryForAllDependencies = DependenciesToQueryMapper.mapToQueries(dependenciesToConsider); 74 | 75 | final Set result = client.search(queryForAllDependencies); 76 | 77 | final Collection outdatedDependencies = new ArrayList<>(); 78 | final Collection outdatedPlugins = new ArrayList<>(); 79 | for (final Dependency currentDependency : dependenciesToConsider) { 80 | result.stream() 81 | .filter(dep -> currentDependency.getGroupId().equals(dep.g()) && currentDependency.getArtifactId().equals(dep.a())) 82 | .filter(dep -> getDateTime(dep.timestamp()).isBefore(LocalDate.now().minusYears(years))) 83 | .findAny() 84 | .ifPresent(dep -> { 85 | if (dep.isPlugin()) { 86 | outdatedPlugins.add(dep); 87 | } else { 88 | outdatedDependencies.add(dep); 89 | } 90 | }); 91 | } 92 | 93 | if (!outdatedDependencies.isEmpty()) { 94 | outdatedDependencies.forEach(this::logWarning); 95 | } 96 | 97 | if (!outdatedPlugins.isEmpty()) { 98 | outdatedPlugins.forEach(this::logWarning); 99 | } 100 | 101 | if (shouldFailBuild && (!outdatedDependencies.isEmpty() || !outdatedPlugins.isEmpty())) { 102 | throw new MojoFailureException("There are dependencies or plugins that are outdated."); 103 | } 104 | } 105 | 106 | private static Set mapArtifactsToDependencies(final Set artifacts) { 107 | final Set dependencies = new HashSet<>(); 108 | for (Artifact artifact : artifacts) { 109 | dependencies.add(mapArtifactToDependency(artifact)); 110 | } 111 | return dependencies; 112 | } 113 | 114 | private static Dependency mapArtifactToDependency(final Artifact artifact) { 115 | final Dependency dependency = new Dependency(); 116 | dependency.setGroupId(artifact.getGroupId()); 117 | dependency.setArtifactId(artifact.getArtifactId()); 118 | dependency.setVersion(artifact.getVersion()); 119 | return dependency; 120 | } 121 | 122 | private void logWarning(final DependencyResponse dep) { 123 | final String message = 124 | String.format("%s '%s' has not received an update since version '%s' was last uploaded '%s'.", 125 | dep.isPlugin() ? "Plugin" : "Dependency", dep.id(), dep.v(), getDateTime(dep.timestamp())); 126 | if (shouldFailBuild) { 127 | getLog().error(message); 128 | } else { 129 | getLog().warn(message); 130 | } 131 | } 132 | 133 | void setProject(final MavenProject project) { 134 | this.project = project; 135 | } 136 | 137 | void setShouldFailBuild(final boolean shouldFailBuild) { 138 | this.shouldFailBuild = shouldFailBuild; 139 | } 140 | 141 | void setIncludePlugins(final boolean includePlugins) { 142 | this.includePlugins = includePlugins; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/com/giovds/CheckMojoTest.java: -------------------------------------------------------------------------------- 1 | package com.giovds; 2 | 3 | import org.apache.maven.plugin.MojoExecutionException; 4 | import org.apache.maven.plugin.MojoFailureException; 5 | import org.apache.maven.project.MavenProject; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.time.LocalDate; 9 | import java.util.Collections; 10 | 11 | import static com.giovds.TestFakes.createDependency; 12 | import static com.giovds.TestFakes.createPlugin; 13 | import static com.giovds.TestFakes.createProjectWithDependencies; 14 | import static com.giovds.TestFakes.createProjectWithPlugins; 15 | import static com.giovds.TestFakes.mockClientResponseForDependency; 16 | import static com.giovds.TestFakes.mockClientResponseForPlugin; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; 19 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 20 | import static org.mockito.ArgumentMatchers.anyList; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.never; 23 | import static org.mockito.Mockito.verify; 24 | 25 | class CheckMojoTest { 26 | 27 | private final QueryClient client = mock(QueryClient.class); 28 | private final CheckMojo mojo = new CheckMojo(client); 29 | 30 | @Test 31 | void should_throw_exception_when_shouldFailBuild_and_outdatedDependencies() throws Exception { 32 | var dependency = createDependency(); 33 | var project = createProjectWithDependencies(dependency); 34 | mojo.setProject(project); 35 | 36 | mockClientResponseForDependency(client, dependency, LocalDate.now().minusYears(10)); 37 | 38 | mojo.setProject(project); 39 | 40 | mojo.setShouldFailBuild(true); 41 | assertThatThrownBy(mojo::execute) 42 | .isInstanceOf(MojoFailureException.class) 43 | .hasMessage("There are dependencies or plugins that are outdated."); 44 | } 45 | 46 | @Test 47 | void should_not_throw_exception_by_default_when_outdatedDependencies() throws Exception { 48 | var dependency = createDependency(); 49 | var project = createProjectWithDependencies(dependency); 50 | mojo.setProject(project); 51 | 52 | mockClientResponseForDependency(client, dependency, LocalDate.now().minusYears(10)); 53 | 54 | mojo.setProject(project); 55 | 56 | assertThatCode(mojo::execute).doesNotThrowAnyException(); 57 | } 58 | 59 | @Test 60 | void should_not_throw_exception_when_no_dependencies() throws MojoExecutionException { 61 | final MavenProject project = new MavenProject(); 62 | project.setDependencies(Collections.emptyList()); 63 | mojo.setProject(project); 64 | 65 | verify(client, never()).search(anyList()); 66 | } 67 | 68 | @Test 69 | void should_log_warning_when_dependency_is_outdated() throws MojoExecutionException { 70 | var dependency = createDependency(); 71 | var project = createProjectWithDependencies(dependency); 72 | mojo.setProject(project); 73 | 74 | mockClientResponseForDependency(client, dependency, LocalDate.of(1998, 2, 19)); 75 | 76 | TestLogger logger = new TestLogger(); 77 | mojo.setLog(logger); 78 | 79 | assertThatCode(mojo::execute).doesNotThrowAnyException(); 80 | assertThat(logger.getWarningLogs()).containsExactly( 81 | "Dependency 'com.giovds:test-example:1.0.0' has not received an update since version '1.0.0' was last uploaded '1998-02-19'." 82 | ).hasSize(1); 83 | assertThat(logger.getErrorLogs()).isEmpty(); 84 | } 85 | 86 | @Test 87 | void should_log_error_when_dependency_is_outdated_and_shouldFailBuild() throws MojoExecutionException { 88 | var dependency = createDependency(); 89 | var project = createProjectWithDependencies(dependency); 90 | mojo.setProject(project); 91 | 92 | mockClientResponseForDependency(client, dependency, LocalDate.of(1998, 2, 19)); 93 | 94 | TestLogger logger = new TestLogger(); 95 | mojo.setLog(logger); 96 | mojo.setShouldFailBuild(true); 97 | 98 | assertThatThrownBy(mojo::execute) 99 | .isInstanceOf(MojoFailureException.class) 100 | .hasMessage("There are dependencies or plugins that are outdated."); 101 | assertThat(logger.getWarningLogs()).isEmpty(); 102 | assertThat(logger.getErrorLogs()).containsExactly( 103 | "Dependency 'com.giovds:test-example:1.0.0' has not received an update since version '1.0.0' was last uploaded '1998-02-19'." 104 | ).hasSize(1); 105 | } 106 | 107 | @Test 108 | void should_log_warning_when_plugin_is_outdated() throws MojoExecutionException { 109 | var plugin = createPlugin("org.apache.maven.plugins", "maven-compiler-plugin", "3.8.1"); 110 | var project = createProjectWithPlugins("com.giovds", "test-project", "1.0.0", plugin); 111 | mojo.setProject(project); 112 | 113 | mockClientResponseForPlugin(client, plugin, LocalDate.of(2018, 11, 18)); 114 | 115 | TestLogger logger = new TestLogger(); 116 | mojo.setLog(logger); 117 | mojo.setIncludePlugins(true); 118 | 119 | assertThatCode(mojo::execute).doesNotThrowAnyException(); 120 | assertThat(logger.getWarningLogs()).containsExactly( 121 | "Plugin 'org.apache.maven.plugins:maven-compiler-plugin:3.8.1' has not received an update since version '3.8.1' was last uploaded '2018-11-18'." 122 | ).hasSize(1); 123 | assertThat(logger.getErrorLogs()).isEmpty(); 124 | } 125 | 126 | @Test 127 | void should_log_error_when_plugin_is_outdated_and_shouldFailBuild() throws MojoExecutionException { 128 | var plugin = createPlugin("org.apache.maven.plugins", "maven-compiler-plugin", "3.8.1"); 129 | var project = createProjectWithPlugins("com.giovds", "test-project", "1.0.0", plugin); 130 | mojo.setProject(project); 131 | 132 | mockClientResponseForPlugin(client, plugin, LocalDate.of(2018, 11, 18)); 133 | 134 | TestLogger logger = new TestLogger(); 135 | mojo.setLog(logger); 136 | mojo.setShouldFailBuild(true); 137 | mojo.setIncludePlugins(true); 138 | 139 | assertThatThrownBy(mojo::execute) 140 | .isInstanceOf(MojoFailureException.class) 141 | .hasMessage("There are dependencies or plugins that are outdated."); 142 | assertThat(logger.getWarningLogs()).isEmpty(); 143 | assertThat(logger.getErrorLogs()).containsExactly( 144 | "Plugin 'org.apache.maven.plugins:maven-compiler-plugin:3.8.1' has not received an update since version '3.8.1' was last uploaded '2018-11-18'." 145 | ).hasSize(1); 146 | } 147 | 148 | @Test 149 | void should_not_log_plugin_when_includePlugins_is_false() throws MojoExecutionException { 150 | var plugin = createPlugin("org.apache.maven.plugins", "maven-compiler-plugin", "3.8.1"); 151 | var project = createProjectWithPlugins("com.giovds", "test-project", "1.0.0", plugin); 152 | mojo.setProject(project); 153 | 154 | mockClientResponseForPlugin(client, plugin, LocalDate.of(2018, 11, 18)); 155 | 156 | TestLogger logger = new TestLogger(); 157 | mojo.setLog(logger); 158 | mojo.setIncludePlugins(false); 159 | 160 | assertThatCode(mojo::execute).doesNotThrowAnyException(); 161 | assertThat(logger.getWarningLogs()).isEmpty(); 162 | assertThat(logger.getErrorLogs()).isEmpty(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.giovds 7 | outdated-maven-plugin 8 | 1.5.0-SNAPSHOT 9 | maven-plugin 10 | 11 | The Outdated Maven Plugin 12 | The Outdated Maven Plugin is a tool designed to help developers identify outdated dependencies in their 13 | Maven projects. By scanning the dependencies of your project, this plugin determines if they are no longer 14 | actively maintained based on a user-defined threshold of inactivity in years. This ensures that your project 15 | remains up-to-date with the latest and most secure versions of its dependencies. 16 | 17 | https://github.com/giovds/outdated-maven-plugin/ 18 | 19 | 20 | scm:git:git://github.com/Giovds/outdated-maven-plugin.git 21 | scm:git:ssh://github.com:Giovds/outdated-maven-plugin.git 22 | https://github.com/Giovds/outdated-maven-plugin/tree/main 23 | 24 | 25 | GitHub 26 | https://github.com/Giovds/outdated-maven-plugin/issues 27 | 28 | 29 | 30 | 31 | MIT License 32 | https://www.opensource.org/licenses/mit-license.php 33 | 34 | 35 | 36 | 37 | 38 | Giovanni van der Schelde 39 | https://github.com/Giovds 40 | 41 | author 42 | 43 | 44 | 45 | 46 | 47 | UTF-8 48 | 17 49 | 50 | 2.20.1 51 | 6.0.1 52 | 3.27.4 53 | 5.20.0 54 | 3.13.2 55 | 56 | 3.9.11 57 | 3.15.2 58 | 3.14.0 59 | 3.5.4 60 | 3.5.0 61 | 3.1.4 62 | 3.3.1 63 | 3.3.1 64 | 3.12.0 65 | 3.1.4 66 | 3.2.8 67 | 68 | 1.21.0 69 | 3.0.0 70 | 2.0.17 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven 78 | maven-model 79 | ${maven-dependencies.version} 80 | provided 81 | 82 | 83 | org.apache.maven 84 | maven-settings 85 | ${maven-dependencies.version} 86 | provided 87 | 88 | 89 | org.apache.maven 90 | maven-settings-builder 91 | ${maven-dependencies.version} 92 | provided 93 | 94 | 95 | org.apache.maven 96 | maven-builder-support 97 | ${maven-dependencies.version} 98 | provided 99 | 100 | 101 | org.apache.maven 102 | maven-repository-metadata 103 | ${maven-dependencies.version} 104 | provided 105 | 106 | 107 | org.apache.maven 108 | maven-artifact 109 | ${maven-dependencies.version} 110 | provided 111 | 112 | 113 | org.apache.maven 114 | maven-model-builder 115 | ${maven-dependencies.version} 116 | provided 117 | 118 | 119 | org.apache.maven 120 | maven-resolver-provider 121 | ${maven-dependencies.version} 122 | provided 123 | 124 | 125 | org.apache.maven 126 | maven-plugin-api 127 | ${maven-dependencies.version} 128 | provided 129 | 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven 136 | maven-core 137 | ${maven-dependencies.version} 138 | provided 139 | 140 | 141 | org.apache.maven.plugin-tools 142 | maven-plugin-annotations 143 | ${maven-plugin-tools.version} 144 | provided 145 | 146 | 147 | 148 | com.fasterxml.jackson.jr 149 | jackson-jr-objects 150 | ${jackson-jr-objects.version} 151 | 152 | 153 | 154 | org.junit.jupiter 155 | junit-jupiter 156 | ${junit-jupiter.version} 157 | test 158 | 159 | 160 | org.assertj 161 | assertj-core 162 | ${assertj-core.version} 163 | test 164 | 165 | 166 | org.mockito 167 | mockito-junit-jupiter 168 | ${mockito-junit-jupiter.version} 169 | test 170 | 171 | 172 | org.wiremock 173 | wiremock-standalone 174 | ${wiremock-standalone.version} 175 | test 176 | 177 | 178 | org.slf4j 179 | slf4j-api 180 | ${slf4j.version} 181 | 182 | 183 | 184 | org.slf4j 185 | slf4j-nop 186 | ${slf4j.version} 187 | test 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | org.apache.maven.plugins 196 | maven-compiler-plugin 197 | ${maven-compiler-plugin.version} 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-surefire-plugin 202 | ${maven-surefire-plugin.version} 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-jar-plugin 207 | ${maven-jar-plugin.version} 208 | 209 | 210 | org.apache.maven.plugins 211 | maven-install-plugin 212 | ${maven-install-plugin.version} 213 | 214 | 215 | org.apache.maven.plugins 216 | maven-resources-plugin 217 | ${maven-resources-plugin.version} 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-deploy-plugin 222 | ${maven-deploy-plugin.version} 223 | 224 | 225 | org.apache.maven.plugins 226 | maven-gpg-plugin 227 | ${maven-gpg-plugin.version} 228 | 229 | 230 | org.jreleaser 231 | jreleaser-maven-plugin 232 | ${jreleaser-maven-plugin.version} 233 | 234 | 235 | com.diffplug.spotless 236 | spotless-maven-plugin 237 | ${spotless-maven-plugin.version} 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-plugin-plugin 242 | ${maven-plugin-tools.version} 243 | 244 | 245 | org.apache.maven.plugins 246 | maven-source-plugin 247 | ${maven-source-plugin.version} 248 | 249 | 250 | org.apache.maven.plugins 251 | maven-javadoc-plugin 252 | ${maven-javadoc-plugin.version} 253 | 254 | 255 | 256 | 257 | 258 | org.apache.maven.plugins 259 | maven-plugin-plugin 260 | 261 | 262 | help-mojo 263 | 264 | helpmojo 265 | 266 | 267 | 268 | 269 | 270 | org.apache.maven.plugins 271 | maven-source-plugin 272 | 273 | 274 | attach-sources 275 | 276 | jar 277 | 278 | 279 | true 280 | 281 | 282 | 283 | 284 | 285 | org.apache.maven.plugins 286 | maven-javadoc-plugin 287 | 288 | 289 | *.outdated_maven_plugin 290 | 291 | 292 | 293 | attach-javadocs 294 | 295 | jar 296 | 297 | 298 | true 299 | 300 | 301 | 302 | 303 | 304 | com.diffplug.spotless 305 | spotless-maven-plugin 306 | 307 | 308 | 309 | 310 | **/* 311 | 312 | 313 | **/target/** 314 | **/*.yml 315 | **/.idea/** 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | src/main/java/**/*.java 324 | src/test/java/**/*.java 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | spotless 333 | 334 | check 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | prepare-release 345 | 346 | local::file:./target/staging-deploy 347 | 348 | 349 | 350 | 351 | org.apache.maven.plugins 352 | maven-gpg-plugin 353 | 354 | 355 | sign-artifacts 356 | 357 | sign 358 | 359 | 360 | 361 | 362 | --pinentry-mode 363 | loopback 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | release 374 | 375 | 376 | 377 | org.jreleaser 378 | jreleaser-maven-plugin 379 | 380 | 381 | 382 | ALWAYS 383 | true 384 | 385 | 386 | 387 | Giovds 388 | ${project.artifactId} 389 | true 390 | 391 | 392 | 393 | 394 | 395 | 396 | ALWAYS 397 | https://central.sonatype.com/api/v1/publisher 398 | target/staging-deploy 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | --------------------------------------------------------------------------------