├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── maven-tests.yml │ └── release.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml └── src ├── it ├── basic-dependencyManagement │ └── pom.xml ├── basic-single-dependency-with-version-in-property │ ├── invoker.properties │ ├── pom.xml │ └── verify.groovy ├── basic-single-dependency │ ├── invoker.properties │ ├── pom.xml │ └── verify.groovy ├── dependency-defined-twice │ ├── invoker.properties │ ├── pom.xml │ └── verify.groovy └── no-dependencies │ ├── invoker.properties │ ├── pom.xml │ └── verify.groovy ├── main └── java │ └── io │ └── github │ └── mfoo │ └── libyear │ └── LibYearMojo.java ├── site └── site.xml └── test ├── java └── io │ └── github │ └── mfoo │ └── libyear │ ├── InMemoryTestLogger.java │ ├── LibYearMojoTest.java │ └── MavenProjectBuilder.java └── resources ├── pom-no-dependencies.xml └── pom-single-outdated-dependency.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '22 13 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v3 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/maven-tests.yml: -------------------------------------------------------------------------------- 1 | name: Java Tests 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-22.04] 9 | java: [11, 17, 18] 10 | fail-fast: false 11 | name: Test on JDK ${{ matrix.java }}, ${{ matrix.os }} 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up JDK 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: ${{ matrix.java }} 19 | distribution: adopt 20 | - name: Test with Maven 21 | run: mvn -B --file pom.xml test 22 | - name: Verify with Maven 23 | run: mvn -B -f pom.xml verify 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | packages: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-java@v4 14 | with: 15 | java-version: '11' 16 | distribution: 'adopt' 17 | - name: Publish package 18 | run: mvn --batch-mode deploy 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | .idea 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/mojohaus/versions-maven-plugin.svg?label=License)](http://www.apache.org/licenses/) 2 | ![GitHub Action build status badge](https://github.com/mfoo/libyear-maven-plugin/actions/workflows/maven-tests.yml/badge.svg) 3 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mfoo_libyear-maven-plugin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mfoo_libyear-maven-plugin) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/mfoo/libyear-maven-plugin/badge.svg)](https://snyk.io/test/github/mfoo/libyear-maven-plugin) 5 | 6 | # libyear-maven-plugin 7 | 8 | This Maven plugin implements the [libyear](https://libyear.com/) dependency 9 | freshness measure, showing you how many years behind the latest version each of 10 | your dependencies are. 11 | 12 | When executed, you'll see an output like this for each of your modules: 13 | ``` 14 | [INFO] The following dependencies in Dependencies have newer versions: 15 | [INFO] io.quarkus:quarkus-arc ................................. 0.27 libyears 16 | [INFO] io.quarkus:quarkus-junit5 .............................. 0.27 libyears 17 | [INFO] io.quarkus:quarkus-junit5-mockito ...................... 0.27 libyears 18 | [INFO] io.quarkus:quarkus-rest-client ......................... 0.27 libyears 19 | [INFO] io.quarkus:quarkus-resteasy ............................ 0.27 libyears 20 | [INFO] io.quarkus:quarkus-resteasy-jackson .................... 0.27 libyears 21 | [INFO] io.rest-assured:rest-assured ........................... 0.77 libyears 22 | [INFO] org.mockito:mockito-all ................................ 0.00 libyears 23 | [INFO] org.mockito:mockito-core ............................... 0.35 libyears 24 | [INFO] 25 | [INFO] This module is 2.73 libyears behind 26 | ``` 27 | 28 | The plugin requires JDK 11+. It is heavily based on the [MojoHaus Versions Maven Plugin](https://www.mojohaus.org/versions/versions-maven-plugin/index.html). 29 | 30 | ## Usage 31 | 32 | The plugin provides a single goal, `analyze`. This can be executed 33 | directly via the command-line with Maven, or it can be incorporated as part of 34 | a build inside `pom.xml`. 35 | 36 | ### Standalone 37 | 38 | ```shell 39 | mvn io.github.mfoo:libyear-maven-plugin:analyze 40 | ``` 41 | 42 | ### As part of a build 43 | 44 | ```xml 45 | 46 | 47 | 48 | io.github.mfoo 49 | libyear-maven-plugin 50 | 1.1.0 51 | 52 | 53 | libyear-analysis 54 | 55 | analyze 56 | 57 | 58 | 59 | 60 | 61 | 62 | ``` 63 | 64 | ## Configuration 65 | Configuration options can be specified on the command line such as 66 | `-DdependencyExcludes="io.github.mfoo:*"` or as part of the plugin 67 | configuration in `pom.xml`. 68 | 69 | | Option | Description | Format | 70 | |----------------------|-------------------------------------------------------------|---------------------------------| 71 | | `dependencyExcludes` | Ignore certain dependencies | `io.github.mfoo:*,org.apache:*` | 72 | | `maxLibYears` | Cause the build to fail if dependencies are older than this | `4` | 73 | 74 | 75 | A full list of options can be seen as part of the [plugin documentation 76 | site](https://mfoo.github.io/libyear-maven-plugin/analyze-mojo.html). -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.github.mfoo 7 | libyear-maven-plugin 8 | 1.1.1-SNAPSHOT 9 | libyear-maven-plugin Maven Mojo 10 | This plugin helps you see how outdated your Maven project 11 | dependencies are via the libyear dependency freshness measure. 12 | https://github.com/mfoo/libyear-maven-plugin 13 | maven-plugin 14 | 15 | 16 | GitHub 17 | https://github.com/mfoo/libyear-maven-plugin/issues 18 | 19 | 20 | 21 | 22 | ossrh 23 | https://s01.oss.sonatype.org/content/repositories/snapshots 24 | 25 | 26 | ossrh 27 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 28 | 29 | 30 | 31 | 2023 32 | 33 | 34 | 35 | Martin Foot 36 | https://github.com/mfoo 37 | 38 | 39 | 40 | 41 | 42 | Apache License, Version 2.0 43 | http://www.apache.org/licenses/LICENSE-2.0.txt 44 | repo 45 | 46 | 47 | 48 | 49 | scm:git:git://github.com/mfoo/libyear-maven-plugin.git 50 | scm:git:git@github.com:mfoo/libyear-maven-plugin.git 51 | https://github.com/mfoo/libyear-maven-plugin/tree/main 52 | 53 | 54 | 55 | 3.6.3 56 | 57 | 58 | 59 | UTF-8 60 | 11 61 | 11 62 | 3.9.10 63 | 2.16.2 64 | 5.13.1 65 | github 66 | 67 | 68 | mfoo 69 | https://sonarcloud.io 70 | 71 | 72 | 73 | 74 | org.apache.maven 75 | maven-plugin-api 76 | ${maven.dependencies.version} 77 | provided 78 | 79 | 80 | org.apache.maven 81 | maven-artifact 82 | ${maven.dependencies.version} 83 | provided 84 | 85 | 86 | org.apache.maven 87 | maven-core 88 | ${maven.dependencies.version} 89 | provided 90 | 91 | 92 | org.apache.maven 93 | maven-model 94 | ${maven.dependencies.version} 95 | provided 96 | 97 | 98 | org.apache.maven.plugin-tools 99 | maven-plugin-annotations 100 | 3.15.1 101 | provided 102 | 103 | 104 | org.apache.maven.resolver 105 | maven-resolver-api 106 | 2.0.9 107 | provided 108 | 109 | 110 | org.apache.maven 111 | maven-project 112 | 2.2.1 113 | provided 114 | 115 | 116 | org.apache.maven 117 | maven-settings 118 | ${maven.dependencies.version} 119 | provided 120 | 121 | 122 | javax.inject 123 | javax.inject 124 | 1 125 | provided 126 | 127 | 128 | 129 | org.eclipse.aether 130 | aether-api 131 | 1.1.0 132 | runtime 133 | 134 | 135 | 136 | org.apache.commons 137 | commons-lang3 138 | 3.17.0 139 | 140 | 141 | com.google.guava 142 | guava 143 | 33.4.8-jre 144 | 145 | 146 | org.apache.httpcomponents 147 | httpclient 148 | 4.5.14 149 | 150 | 151 | org.apache.httpcomponents 152 | httpcore 153 | 4.4.16 154 | 155 | 156 | org.codehaus.plexus 157 | plexus-utils 158 | 4.0.2 159 | 160 | 161 | 162 | org.codehaus.plexus 163 | plexus-xml 164 | 4.0.4 165 | provided 166 | 167 | 168 | org.json 169 | json 170 | 20250517 171 | 172 | 173 | org.codehaus.mojo.versions 174 | versions-common 175 | ${codehaus.versions.dependencies.version} 176 | 177 | 178 | 179 | org.codehaus.mojo.versions 180 | versions-test 181 | ${codehaus.versions.dependencies.version} 182 | test 183 | 184 | 185 | org.apache.maven.plugin-testing 186 | maven-plugin-testing-harness 187 | 3.3.0 188 | test 189 | 190 | 191 | org.junit.jupiter 192 | junit-jupiter-api 193 | ${junit.jupiter.version} 194 | test 195 | 196 | 197 | org.junit.jupiter 198 | junit-jupiter-engine 199 | ${junit.jupiter.version} 200 | test 201 | 202 | 203 | org.mockito 204 | mockito-junit-jupiter 205 | 5.18.0 206 | test 207 | 208 | 209 | org.mockito 210 | mockito-core 211 | 5.18.0 212 | test 213 | 214 | 215 | com.github.tomakehurst 216 | wiremock-jre8 217 | 2.35.2 218 | test 219 | 220 | 221 | 223 | org.slf4j 224 | slf4j-simple 225 | 2.0.17 226 | test 227 | 228 | 229 | 230 | 231 | 232 | 233 | 236 | commons-codec 237 | commons-codec 238 | 1.18.0 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | org.apache.maven.plugins 247 | maven-surefire-plugin 248 | 3.5.3 249 | 250 | 251 | org.apache.maven.plugins 252 | maven-plugin-plugin 253 | 3.15.1 254 | 255 | 256 | default-descriptor 257 | process-classes 258 | 259 | 260 | 261 | 262 | org.apache.maven.plugins 263 | maven-invoker-plugin 264 | 3.9.0 265 | 266 | verify 267 | 268 | 269 | 270 | integration-test 271 | 272 | install 273 | integration-test 274 | verify 275 | 276 | 277 | false 278 | false 279 | 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-dependency-plugin 286 | 3.8.1 287 | 288 | true 289 | true 290 | 291 | 292 | 293 | 294 | analyze-only 295 | 296 | 297 | 298 | 299 | 300 | org.apache.maven.plugins 301 | maven-javadoc-plugin 302 | 3.11.2 303 | 304 | 305 | 306 | jar 307 | 308 | 309 | 310 | 311 | 312 | org.apache.maven.plugins 313 | maven-source-plugin 314 | 3.3.1 315 | 316 | 317 | 318 | jar 319 | 320 | 321 | 322 | 323 | 324 | org.apache.maven.plugins 325 | maven-site-plugin 326 | 3.21.0 327 | 328 | 329 | com.diffplug.spotless 330 | spotless-maven-plugin 331 | 2.44.5 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | **/pom.xml 340 | 341 | 342 | XML 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | check 351 | 352 | compile 353 | 354 | 355 | 356 | 357 | 358 | org.jacoco 359 | jacoco-maven-plugin 360 | 0.8.13 361 | 362 | 363 | prepare-agent 364 | 365 | prepare-agent 366 | 367 | 368 | 369 | report 370 | prepare-package 371 | 372 | report 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | deploy 383 | 384 | 385 | 386 | 387 | org.apache.maven.plugins 388 | maven-gpg-plugin 389 | 3.2.7 390 | 391 | 392 | sign-artifacts 393 | verify 394 | 395 | sign 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | org.apache.maven.plugins 409 | maven-javadoc-plugin 410 | 3.11.2 411 | 412 | 413 | org.apache.maven.plugins 414 | maven-plugin-report-plugin 415 | 3.15.1 416 | 417 | 418 | org.apache.maven.plugins 419 | maven-project-info-reports-plugin 420 | 3.9.0 421 | 422 | 423 | 424 | index 425 | team 426 | dependency-info 427 | issue-management 428 | licenses 429 | scm 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | -------------------------------------------------------------------------------- /src/it/basic-dependencyManagement/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | libyear-report 10 | http://localhost/ 11 | 12 | 13 | Testing that versions from dependencyManagement are 14 | handled correctly 15 | 16 | 17 | 18 | 19 | commons-io 20 | commons-io 21 | 22 | 23 | 24 | 25 | 26 | 27 | commons-io 28 | commons-io 29 | 2.14.0 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/it/basic-single-dependency-with-version-in-property/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:analyze 2 | -------------------------------------------------------------------------------- /src/it/basic-single-dependency-with-version-in-property/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | libyear-report 10 | http://localhost/ 11 | 12 | 13 | Testing how the plugin interprets versions in properties 14 | 15 | 16 | 17 | 1.1 18 | 19 | 20 | 21 | localhost 22 | dummy-api 23 | ${dummyapi.version} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/it/basic-single-dependency-with-version-in-property/verify.groovy: -------------------------------------------------------------------------------- 1 | def buildLog = new File(basedir, "build.log") 2 | assert !buildLog.text.contains("have newer versions") -------------------------------------------------------------------------------- /src/it/basic-single-dependency/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:analyze 2 | -------------------------------------------------------------------------------- /src/it/basic-single-dependency/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | libyear-report 10 | http://localhost/ 11 | 12 | 13 | Very basic test to see that dependency updates are 14 | suggested correctly 15 | 16 | 17 | 18 | 19 | localhost 20 | dummy-api 21 | 1.1 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/it/basic-single-dependency/verify.groovy: -------------------------------------------------------------------------------- 1 | def buildLog = new File(basedir, "build.log") 2 | assert !buildLog.text.contains("have newer versions") -------------------------------------------------------------------------------- /src/it/dependency-defined-twice/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:analyze 2 | -------------------------------------------------------------------------------- /src/it/dependency-defined-twice/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | localhost 7 | basic-single-dependency 8 | 1.0 9 | pom 10 | libyear-analyze 11 | http://localhost/ 12 | 13 | 14 | Testing how the plugin sees dependencies that are defined 15 | twice 16 | 17 | 18 | 19 | 20 | commons-io 21 | commons-io 22 | 2.14.0 23 | 24 | 25 | commons-io 26 | commons-io 27 | 2.14.0 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/it/dependency-defined-twice/verify.groovy: -------------------------------------------------------------------------------- 1 | //def buildLog = new File(basedir, "build.log") 2 | // 3 | //assert buildLog.text.contains(""" 4 | //[INFO] The following dependencies in Dependencies have newer versions: 5 | //[INFO] localhost:dummy-api ....................................... 1.0 -> 3.0 6 | //[INFO] localhost:dummy-impl ...................................... 1.0 -> 2.2 7 | //[INFO] 8 | //""".replaceAll( "\n", System.lineSeparator() ) ) 9 | // 10 | //return true 11 | -------------------------------------------------------------------------------- /src/it/no-dependencies/invoker.properties: -------------------------------------------------------------------------------- 1 | invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:analyze 2 | -------------------------------------------------------------------------------- /src/it/no-dependencies/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | libyear-report 10 | http://localhost/ 11 | 12 | Testing how projects with no dependencies are handled 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/it/no-dependencies/verify.groovy: -------------------------------------------------------------------------------- 1 | def buildLog = new File(basedir, "build.log") 2 | assert !buildLog.text.contains("have newer versions") 3 | -------------------------------------------------------------------------------- /src/main/java/io/github/mfoo/libyear/LibYearMojo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Martin Foot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.mfoo.libyear; 18 | 19 | import static java.util.Collections.emptySet; 20 | import static org.codehaus.mojo.versions.filtering.DependencyFilter.filterDependencies; 21 | import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromDependencyManagement; 22 | import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromPlugins; 23 | import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractPluginDependenciesFromPluginsInPluginManagement; 24 | 25 | import com.google.common.collect.Maps; 26 | import java.io.IOException; 27 | import java.net.SocketTimeoutException; 28 | import java.net.URI; 29 | import java.nio.charset.StandardCharsets; 30 | import java.time.Instant; 31 | import java.time.LocalDate; 32 | import java.time.ZoneId; 33 | import java.time.temporal.ChronoUnit; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.Optional; 37 | import java.util.Set; 38 | import java.util.TreeSet; 39 | import java.util.concurrent.ConcurrentHashMap; 40 | import java.util.concurrent.atomic.AtomicLong; 41 | import java.util.stream.Collectors; 42 | import javax.inject.Inject; 43 | import org.apache.commons.lang3.tuple.Pair; 44 | import org.apache.http.HttpResponseInterceptor; 45 | import org.apache.http.client.config.RequestConfig; 46 | import org.apache.http.client.methods.CloseableHttpResponse; 47 | import org.apache.http.client.methods.HttpGet; 48 | import org.apache.http.conn.ConnectTimeoutException; 49 | import org.apache.http.impl.client.CloseableHttpClient; 50 | import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; 51 | import org.apache.http.impl.client.HttpClientBuilder; 52 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 53 | import org.apache.http.util.EntityUtils; 54 | import org.apache.maven.artifact.Artifact; 55 | import org.apache.maven.artifact.versioning.ArtifactVersion; 56 | import org.apache.maven.execution.MavenSession; 57 | import org.apache.maven.model.Dependency; 58 | import org.apache.maven.plugin.AbstractMojo; 59 | import org.apache.maven.plugin.MojoExecutionException; 60 | import org.apache.maven.plugins.annotations.LifecyclePhase; 61 | import org.apache.maven.plugins.annotations.Mojo; 62 | import org.apache.maven.plugins.annotations.Parameter; 63 | import org.apache.maven.project.MavenProject; 64 | import org.apache.maven.repository.RepositorySystem; 65 | import org.apache.maven.settings.Settings; 66 | import org.codehaus.mojo.versions.api.ArtifactVersions; 67 | import org.codehaus.mojo.versions.api.DefaultVersionsHelper; 68 | import org.codehaus.mojo.versions.api.VersionsHelper; 69 | import org.codehaus.mojo.versions.filtering.WildcardMatcher; 70 | import org.codehaus.mojo.versions.utils.DependencyComparator; 71 | import org.codehaus.plexus.util.StringUtils; 72 | import org.json.JSONObject; 73 | 74 | /** Analyze dependencies and calculate how old they are. */ 75 | @SuppressWarnings("unused") 76 | @Mojo(name = "analyze", defaultPhase = LifecyclePhase.VERIFY) 77 | public class LibYearMojo extends AbstractMojo { 78 | /** Screen width for formatting the output number of libyears */ 79 | private static final int INFO_PAD_SIZE = 72; 80 | 81 | /** 82 | * Cache to store the release dates of dependencies to reduce the number of API calls to {@link 83 | * #SEARCH_URI} 84 | */ 85 | static final Map> dependencyVersionReleaseDates = Maps.newHashMap(); 86 | 87 | /** 88 | * Track the running total of how many libweeks outdated we are. Used in multi-module builds. 89 | */ 90 | static final AtomicLong libWeeksOutDated = new AtomicLong(); 91 | 92 | private final CloseableHttpClient httpClient; 93 | 94 | /** 95 | * Track the age of each module in a multi-module project. 96 | */ 97 | static final Map projectAges = new ConcurrentHashMap<>(); 98 | 99 | /** 100 | * Track the age of the oldest version in use of each dependency 101 | */ 102 | static final Map dependencyAges = new ConcurrentHashMap<>(); 103 | 104 | /** 105 | * The Maven search URI quite often times out or returns HTTP 5xx. This variable controls how 106 | * many times we can retry on failure before skipping this dependency. 107 | */ 108 | private static int MAVEN_API_HTTP_RETRY_COUNT = 5; 109 | 110 | /** HTTP timeout for making calls to {@link #SEARCH_URI} */ 111 | private static int MAVEN_API_HTTP_TIMEOUT_SECONDS = 5; 112 | 113 | /** API endpoint to query dependency release dates for age calculations. */ 114 | // TODO: Consider users requiring HTTP proxies 115 | private static String SEARCH_URI = "https://search.maven.org"; 116 | 117 | private final RepositorySystem repositorySystem; 118 | private final org.eclipse.aether.RepositorySystem aetherRepositorySystem; 119 | private VersionsHelper helper; 120 | 121 | /** 122 | * The Maven Project that the plugin is being executed on. Used for accessing e.g. the list of 123 | * dependencies. 124 | */ 125 | @Parameter(defaultValue = "${project}", required = true, readonly = true) 126 | private MavenProject project; 127 | 128 | /** The Maven Settings that are being used, e.g. ~/.m2/settings.xml. */ 129 | @Parameter(defaultValue = "${settings}", readonly = true) 130 | private Settings settings; 131 | 132 | @Parameter(property = "maven.version.ignore", readonly = true) 133 | protected Set ignoredVersions; 134 | 135 | @Parameter(defaultValue = "${session}", required = true, readonly = true) 136 | private MavenSession session; 137 | 138 | /** 139 | * Whether to fail the build if the total number of libyears for the project exceeds this 140 | * target. 141 | * 142 | *

In a multi-module project, this value applies to individual modules, not the parent(s). 143 | * 144 | *

Note: If you are using this in a project's pom.xml then you may accidentally cause issues 145 | * when e.g. rebuilding older branches. Instead, it may make more sense to use this as a command 146 | * line plugin execution flag in CI or in a Maven profile used for building releases. 147 | * 148 | * @since 1.0. 149 | */ 150 | @Parameter(property = "maxLibYears", defaultValue = "0.0") 151 | private float maxLibYears; 152 | 153 | /** 154 | * Only take these artifacts into consideration. 155 | * 156 | *

Comma-separated list of extended GAV patterns. 157 | * 158 | *

Extended GAV: groupId:artifactId:version:type:classifier:scope 159 | * 160 | *

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

Example: {@code "mygroup:artifact:*,*:*:*:*:*:compile"} 164 | * 165 | * @since 1.0.0 166 | */ 167 | @Parameter(property = "pluginManagementDependencyIncludes", defaultValue = WildcardMatcher.WILDCARD) 168 | private List pluginManagementDependencyIncludes; 169 | 170 | /** 171 | * Exclude these artifacts into consideration:
172 | * Comma-separated list of {@code groupId:[artifactId[:version]]} patterns 173 | * 174 | *

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

Example: {@code "mygroup:artifact:*,othergroup:*,anothergroup"} 178 | * 179 | * @since 1.0.0 180 | */ 181 | @Parameter(property = "pluginManagementDependencyExcludes") 182 | private List pluginManagementDependencyExcludes; 183 | 184 | // TODO: Add test coverage for this before exposing it as an option 185 | // @Parameter(property = "processDependencyManagementTransitive", defaultValue = 186 | // "false") 187 | // private boolean processDependencyManagementTransitive; 188 | private final boolean processDependencyManagementTransitive = false; 189 | 190 | /** 191 | * Whether to consider the dependencyManagement pom section. If this is set to false, 192 | * dependencyManagement is ignored. 193 | * 194 | * @since 1.0. 195 | */ 196 | @Parameter(property = "processDependencyManagement", defaultValue = "true") 197 | private boolean processDependencyManagement = true; 198 | 199 | /** 200 | * Whether to consider the dependencies pom section. If this is set to false the plugin won't 201 | * analyze dependencies, but it might analyze e.g. plugins depending on configuration. 202 | * 203 | * @since 1.0. 204 | */ 205 | @Parameter(property = "processDependencies", defaultValue = "true") 206 | protected boolean processDependencies; 207 | 208 | // TODO: Add test coverage for this before exposing it as an option 209 | // @Parameter(property = "processPluginDependenciesInPluginManagement", 210 | // defaultValue = "true") 211 | // private boolean processPluginDependenciesInPluginManagement; 212 | private final boolean processPluginDependenciesInPluginManagement = true; 213 | 214 | // TODO: Add test coverage for this before exposing it as an option 215 | // @Parameter(property = "processPluginDependencies", defaultValue = "true") 216 | // protected boolean processPluginDependencies; 217 | private final boolean processPluginDependencies = true; 218 | 219 | /** 220 | * Only take these artifacts into consideration:
221 | * Comma-separated list of {@code groupId:[artifactId[:version]]} patterns 222 | * 223 | *

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

Example: {@code "mygroup:artifact:*,othergroup:*,anothergroup"} 227 | * 228 | * @since 1.0.0 229 | */ 230 | @Parameter(property = "pluginDependencyIncludes", defaultValue = WildcardMatcher.WILDCARD) 231 | private List pluginDependencyIncludes; 232 | 233 | /** 234 | * Exclude these artifacts into consideration:
235 | * Comma-separated list of {@code groupId:[artifactId[:version]]} patterns 236 | * 237 | *

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

Example: {@code "mygroup:artifact:*,othergroup:*,anothergroup"} 241 | * 242 | * @since 1.0.0 243 | */ 244 | @Parameter(property = "pluginDependencyExcludes") 245 | private List pluginDependencyExcludes; 246 | 247 | /** 248 | * Only take these artifacts into consideration. 249 | * 250 | *

Comma-separated list of extended GAV patterns. 251 | * 252 | *

Extended GAV: groupId:artifactId:version:type:classifier:scope 253 | * 254 | *

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

Example: {@code "mygroup:artifact:*,*:*:*:*:*:compile"} 258 | * 259 | * @since 1.0.0 260 | */ 261 | @Parameter(property = "dependencyIncludes", defaultValue = WildcardMatcher.WILDCARD) 262 | private List dependencyIncludes; 263 | 264 | /** 265 | * Exclude these artifacts from consideration. 266 | * 267 | *

Comma-separated list of extended GAV patterns. 268 | * 269 | *

Extended GAV: groupId:artifactId:version:type:classifier:scope 270 | * 271 | *

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

Example: {@code "mygroup:artifact:*,*:*:*:*:*:provided,*:*:*:*:*:system"} 275 | * 276 | * @since 1.0.0 277 | */ 278 | @Parameter(property = "dependencyExcludes") 279 | private List dependencyExcludes; 280 | 281 | /** 282 | * Only take these artifacts into consideration. 283 | * 284 | *

Comma-separated list of extended GAV patterns. 285 | * 286 | *

Extended GAV: groupId:artifactId:version:type:classifier:scope 287 | * 288 | *

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

Example: {@code "mygroup:artifact:*,*:*:*:*:*:compile"} 292 | * 293 | * @since 1.0.0 294 | */ 295 | @Parameter(property = "dependencyManagementIncludes", defaultValue = WildcardMatcher.WILDCARD) 296 | private List dependencyManagementIncludes; 297 | 298 | /** 299 | * Exclude these artifacts from consideration. 300 | * 301 | *

Comma-separated list of extended GAV patterns. 302 | * 303 | *

Extended GAV: groupId:artifactId:version:type:classifier:scope 304 | * 305 | *

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

Example: {@code "mygroup:artifact:*,*:*:*:*:*:provided,*:*:*:*:*:system"} 309 | * 310 | * @since 1.0.0 311 | */ 312 | @Parameter(property = "dependencyManagementExcludes") 313 | private List dependencyManagementExcludes; 314 | 315 | @Inject 316 | public LibYearMojo(RepositorySystem repositorySystem, org.eclipse.aether.RepositorySystem aetherRepositorySystem) { 317 | this.repositorySystem = repositorySystem; 318 | this.aetherRepositorySystem = aetherRepositorySystem; 319 | 320 | httpClient = setupHTTPClient(); 321 | } 322 | 323 | private CloseableHttpClient setupHTTPClient() { 324 | RequestConfig config = RequestConfig.custom() 325 | .setConnectTimeout(MAVEN_API_HTTP_TIMEOUT_SECONDS * 1000) 326 | .setConnectionRequestTimeout(MAVEN_API_HTTP_TIMEOUT_SECONDS * 1000) 327 | .setSocketTimeout(MAVEN_API_HTTP_TIMEOUT_SECONDS * 1000) 328 | .build(); 329 | 330 | return HttpClientBuilder.create() 331 | .setConnectionManager(new PoolingHttpClientConnectionManager()) 332 | .setMaxConnPerRoute(20) 333 | .setMaxConnTotal(20) 334 | .setDefaultRequestConfig(config) 335 | .addInterceptorLast((HttpResponseInterceptor) (response, context) -> { 336 | // By default Apache HTTP client doesn't retry on 5xx errors 337 | if (response.getStatusLine().getStatusCode() >= 500) { 338 | throw new IOException(response.getStatusLine().getReasonPhrase()); 339 | } 340 | }) 341 | .setRetryHandler(new DefaultHttpRequestRetryHandler(MAVEN_API_HTTP_RETRY_COUNT, true)) 342 | .build(); 343 | } 344 | 345 | /** 346 | * Setter for property 'project'. 347 | * 348 | * @param project Value to set for property 'project'. 349 | */ 350 | protected void setProject(MavenProject project) { 351 | this.project = project; 352 | } 353 | 354 | /** 355 | * Setter for property 'session'. 356 | * 357 | * @param session Value to set for property 'session'. 358 | */ 359 | protected void setSession(MavenSession session) { 360 | this.session = session; 361 | } 362 | 363 | /** Set the search URI */ 364 | protected void setSearchUri(String uri) { 365 | SEARCH_URI = uri; 366 | } 367 | 368 | /** Setter for the HTTP timeout for API calls */ 369 | protected void setHttpTimeout(int seconds) { 370 | MAVEN_API_HTTP_TIMEOUT_SECONDS = seconds; 371 | } 372 | 373 | /** 374 | * Setter for the HTTP API fetch retry count 375 | * 376 | * @param count the number of retries before giving up 377 | */ 378 | protected void setFetchRetryCount(int count) { 379 | MAVEN_API_HTTP_RETRY_COUNT = count; 380 | } 381 | 382 | private static boolean dependenciesMatch(Dependency dependency, Dependency managedDependency) { 383 | return managedDependency.getGroupId().equals(dependency.getGroupId()) 384 | && managedDependency.getArtifactId().equals(dependency.getArtifactId()); 385 | } 386 | 387 | /** 388 | * Main entry point for the plugin. 389 | * 390 | * @throws MojoExecutionException On failure, such as upstream HTTP issues 391 | */ 392 | public void execute() throws MojoExecutionException { 393 | Set dependencyManagement = emptySet(); 394 | 395 | float thisProjectLibYearsOutdated = 0; 396 | 397 | try { 398 | if (processDependencyManagement) { 399 | // Get all dependencies from 400 | Set dependenciesFromDependencyManagement = extractDependenciesFromDependencyManagement( 401 | project, processDependencyManagementTransitive, getLog()); 402 | 403 | // Handle user settings - filter out anything that's excluded 404 | dependencyManagement = filterDependencies( 405 | dependenciesFromDependencyManagement, 406 | dependencyManagementIncludes, 407 | dependencyManagementExcludes, 408 | "Dependency Management", 409 | getLog()); 410 | 411 | // Log anything that's left 412 | thisProjectLibYearsOutdated += processDependencyUpdates( 413 | getHelper().lookupDependenciesUpdates(dependencyManagement.stream(), false, false), 414 | "Dependency Management"); 415 | } 416 | 417 | if (processDependencies) { 418 | Set finalDependencyManagement = dependencyManagement; 419 | 420 | // Get a list of dependencies and versions, using the versions from dependency management if they exist 421 | Set dependenciesExcludingOverridden = project.getDependencies().parallelStream() 422 | .filter(dep -> finalDependencyManagement.parallelStream() 423 | .noneMatch(depMan -> dependenciesMatch(dep, depMan))) 424 | .collect(() -> new TreeSet<>(DependencyComparator.INSTANCE), Set::add, Set::addAll); 425 | 426 | // Handle user settings - filter out anything that's excluded 427 | Set dependencies = filterDependencies( 428 | dependenciesExcludingOverridden, 429 | dependencyIncludes, 430 | dependencyExcludes, 431 | "Dependencies", 432 | getLog()); 433 | 434 | // Log anything that's left 435 | thisProjectLibYearsOutdated += processDependencyUpdates( 436 | getHelper().lookupDependenciesUpdates(dependencies.stream(), false, false), "Dependencies"); 437 | } 438 | 439 | if (processPluginDependenciesInPluginManagement) { 440 | // Get all dependencies of plugins from dependencyManagement 441 | Set pluginDependenciesFromDepManagement = 442 | extractPluginDependenciesFromPluginsInPluginManagement(project); 443 | 444 | // Handle user settings - filter out anything that's excluded 445 | Set filteredPluginDependenciesFromDepManagement = filterDependencies( 446 | pluginDependenciesFromDepManagement, 447 | pluginManagementDependencyIncludes, 448 | pluginManagementDependencyExcludes, 449 | "Plugin Management Dependencies", 450 | getLog()); 451 | 452 | // Log anything that's left 453 | thisProjectLibYearsOutdated += processDependencyUpdates( 454 | getHelper() 455 | .lookupDependenciesUpdates( 456 | filteredPluginDependenciesFromDepManagement.stream(), false, false), 457 | "pluginManagement of plugins"); 458 | } 459 | 460 | if (processPluginDependencies) { 461 | // Get all dependencies of plugins 462 | Set pluginDependencies = extractDependenciesFromPlugins(project); 463 | 464 | // Handle user settings - filter out anything that's excluded 465 | Set filteredPluginDependencies = filterDependencies( 466 | pluginDependencies, 467 | pluginDependencyIncludes, 468 | pluginDependencyExcludes, 469 | "Plugin Dependencies", 470 | getLog()); 471 | 472 | // Log anything that's left 473 | thisProjectLibYearsOutdated += processDependencyUpdates( 474 | getHelper().lookupDependenciesUpdates(filteredPluginDependencies.stream(), false, false), 475 | "Plugin Dependencies"); 476 | } 477 | 478 | if (thisProjectLibYearsOutdated != 0) { 479 | getLog().info(String.format("This module is %.2f libyears behind", thisProjectLibYearsOutdated)); 480 | } 481 | 482 | if (maxLibYears != 0 && thisProjectLibYearsOutdated >= maxLibYears) { 483 | getLog().info(""); 484 | getLog().error("This module exceeds the maximum dependency age of " + maxLibYears + " libyears"); 485 | throw new MojoExecutionException("Dependencies exceed maximum specified age in libyears"); 486 | } 487 | 488 | projectAges.put(project.getName(), thisProjectLibYearsOutdated); 489 | } catch (Exception e) { 490 | throw new MojoExecutionException(e.getMessage(), e); 491 | } 492 | } 493 | 494 | private VersionsHelper getHelper() throws MojoExecutionException { 495 | if (helper == null) { 496 | helper = new DefaultVersionsHelper.Builder() 497 | .withRepositorySystem(repositorySystem) 498 | .withAetherRepositorySystem(aetherRepositorySystem) 499 | .withIgnoredVersions(ignoredVersions) 500 | .withLog(getLog()) 501 | .withMavenSession(session) 502 | .build(); 503 | } 504 | return helper; 505 | } 506 | 507 | /** 508 | * Iterates over the list of updates for the current pom section, logging how far behind the latest version they are. 509 | * 510 | * @param updates All of the available updates for this section 511 | * @param section The name of the section (e.g. "Plugin Management") 512 | */ 513 | private float processDependencyUpdates(Map updates, String section) { 514 | Map> dependencyVersionUpdates = Maps.newHashMap(); 515 | 516 | for (ArtifactVersions versions : updates.values()) { 517 | if (versions.getCurrentVersion() == null) { 518 | continue; 519 | } 520 | 521 | final String current = versions.getCurrentVersion().toString(); 522 | ArtifactVersion latest = versions.getNewestUpdateWithinSegment(Optional.empty(), false); 523 | 524 | if (latest == null) { 525 | continue; 526 | } 527 | 528 | if (current.equals(latest.toString())) { 529 | continue; 530 | } 531 | 532 | Artifact /* current */ artifact = versions.getArtifact(); 533 | Optional latestVersionReleaseDate = 534 | getReleaseDate(artifact.getGroupId(), artifact.getArtifactId(), latest.toString()); 535 | Optional currentVersionReleaseDate = 536 | getReleaseDate(artifact.getGroupId(), artifact.getArtifactId(), current); 537 | 538 | if (latestVersionReleaseDate.isEmpty() || currentVersionReleaseDate.isEmpty()) { 539 | // We couldn't find version details, skip 540 | continue; 541 | } 542 | 543 | String ga = String.format("%s:%s", artifact.getGroupId(), artifact.getArtifactId()); 544 | dependencyVersionUpdates.put(ga, Pair.of(currentVersionReleaseDate.get(), latestVersionReleaseDate.get())); 545 | } 546 | 547 | if (dependencyVersionUpdates.isEmpty()) { 548 | return 0; 549 | } 550 | 551 | return logDependencyUpdates(section, dependencyVersionUpdates); 552 | } 553 | 554 | /** 555 | * Given a set of outdated dependencies, print how many libyears behind they are to the 556 | * screen. 557 | * 558 | * @param pomSection The section of the pom we are analyzing 559 | * @param outdatedDependencies The outdated dependencies 560 | * @return A total libyear count for the provided dependencies 561 | */ 562 | private float logDependencyUpdates( 563 | String pomSection, Map> outdatedDependencies) { 564 | if (outdatedDependencies.isEmpty()) { 565 | return 0.0f; 566 | } 567 | 568 | Map> validOutdatedDependencies = outdatedDependencies.entrySet().stream() 569 | .filter((dep) -> { 570 | LocalDate currentReleaseDate = dep.getValue().getLeft(); 571 | LocalDate latestReleaseDate = dep.getValue().getRight(); 572 | 573 | // This is a bug in the underlying logic, where the 574 | // display-dependency-updates plugin will include 575 | // updates from e.g. commons-io:commons-io 2.11.0 -> 576 | // 20030203.000550, despite 2.11.0 being ~15 years 577 | // newer. We return here, so we don't count a negative 578 | // libyear count, even though the dependency may still be 579 | // outdated. Anybody experiencing this could use the 580 | // ignoredVersions setting instead 581 | return !currentReleaseDate.isAfter(latestReleaseDate); 582 | }) 583 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 584 | 585 | if (validOutdatedDependencies.isEmpty()) { 586 | return 0.0f; 587 | } 588 | 589 | float[] yearsOutdated = {0}; 590 | 591 | getLog().info(String.format("The following dependencies in %s have newer versions:", pomSection)); 592 | validOutdatedDependencies.entrySet().stream() 593 | .sorted(Map.Entry.comparingByKey()) 594 | .forEach((dep) -> { 595 | LocalDate currentReleaseDate = dep.getValue().getLeft(); 596 | LocalDate latestReleaseDate = dep.getValue().getRight(); 597 | 598 | long libWeeksOutdated = ChronoUnit.WEEKS.between(currentReleaseDate, latestReleaseDate); 599 | float libYearsOutdated = libWeeksOutdated / 52f; 600 | 601 | logDependencyAge(dep, libYearsOutdated); 602 | yearsOutdated[0] += libYearsOutdated; 603 | libWeeksOutDated.getAndAdd(libWeeksOutdated); 604 | 605 | if (!dependencyAges.containsKey(dep.getKey()) 606 | || dependencyAges.get(dep.getKey()) < libYearsOutdated) { 607 | dependencyAges.put(dep.getKey(), libYearsOutdated); 608 | } 609 | }); 610 | 611 | getLog().info(""); 612 | 613 | return yearsOutdated[0]; 614 | } 615 | 616 | /** 617 | * Display the output for how many libyears behind the specified dependency is. Wraps at {@link #INFO_PAD_SIZE}. 618 | *

619 | * Prints output in the form 620 | *

621 | * 622 | * mygroup:myartifact ................ 1.0 years 623 | * mygroup:myartifactwithlonglonglongname 624 | * ................................... 2.0 years 625 | * 626 | * 627 | * @param dep The dependency 628 | * @param libYearsOutdated How many libyears behind it is 629 | */ 630 | private void logDependencyAge(Map.Entry> dep, float libYearsOutdated) { 631 | String right = String.format(" %.2f libyears", libYearsOutdated); 632 | String left = " " + dep.getKey() + " "; 633 | 634 | if ((left.length() + right.length()) > INFO_PAD_SIZE) { 635 | getLog().info(left); 636 | String versionWithDots = StringUtils.rightPad(" ", INFO_PAD_SIZE - right.length(), "."); 637 | getLog().info(versionWithDots + right); 638 | } else { 639 | String versionWithDots = StringUtils.rightPad(left, INFO_PAD_SIZE - right.length(), "."); 640 | getLog().info(versionWithDots + right); 641 | } 642 | } 643 | 644 | /** 645 | * Make an API call to {@link #SEARCH_URI} to fetch the release date of the specified artifact. 646 | * Uses the cache in {@link #dependencyVersionReleaseDates} if possible. 647 | * 648 | * @param groupId The required artifact's groupId 649 | * @param artifactId The required artifact's artifactId 650 | * @param version The required artifact's version 651 | * @return The creation date of the artifact 652 | */ 653 | private Optional getReleaseDate(String groupId, String artifactId, String version) { 654 | String ga = String.format("%s:%s", groupId, artifactId); 655 | Map versionReleaseDates = dependencyVersionReleaseDates.getOrDefault(ga, Maps.newHashMap()); 656 | if (versionReleaseDates.containsKey(version)) { 657 | return Optional.of(versionReleaseDates.get(version)); 658 | } 659 | 660 | try { 661 | Optional response = fetchReleaseDate(groupId, artifactId, version); 662 | 663 | if (response.isEmpty()) { 664 | return Optional.empty(); 665 | } 666 | 667 | JSONObject json = new JSONObject(response.get()); 668 | JSONObject queryResponse = json.getJSONObject("response"); 669 | if (queryResponse.getLong("numFound") != 0) { 670 | long epochTime = 671 | queryResponse.getJSONArray("docs").getJSONObject(0).getLong("timestamp"); 672 | 673 | getLog().debug(String.format("Found release time %d for %s:%s", epochTime, ga, version)); 674 | 675 | LocalDate releaseDate = Instant.ofEpochMilli(epochTime) 676 | .atZone(ZoneId.systemDefault()) 677 | .toLocalDate(); 678 | 679 | versionReleaseDates.put(version, releaseDate); 680 | dependencyVersionReleaseDates.put(ga, versionReleaseDates); 681 | return Optional.of(releaseDate); 682 | } else { 683 | getLog().debug(String.format("Could not find artifact for %s %s", ga, version)); 684 | return Optional.empty(); 685 | } 686 | } catch (Exception e) { 687 | getLog().error(String.format("Failed to fetch release date for %s %s: %s", ga, version, e.getMessage())); 688 | return Optional.empty(); 689 | } 690 | } 691 | 692 | /** Make the API call to fetch the release date */ 693 | private Optional fetchReleaseDate(String groupId, String artifactId, String version) throws IOException { 694 | URI artifactUri = URI.create(String.format( 695 | "%s/solrsearch/select?q=g:%s+AND+a:%s+AND+v:%s&wt=json", SEARCH_URI, groupId, artifactId, version)); 696 | 697 | getLog().debug("Fetching " + artifactUri); 698 | 699 | final HttpGet httpGet = new HttpGet(artifactUri); 700 | 701 | try { 702 | try (CloseableHttpResponse response = httpClient.execute(httpGet)) { 703 | if (response.getStatusLine().getStatusCode() != 200) { 704 | getLog().error(String.format( 705 | "Failed to fetch release date for %s:%s %s (%s)", 706 | groupId, 707 | artifactId, 708 | version, 709 | response.getStatusLine().getReasonPhrase())); 710 | return Optional.empty(); 711 | } 712 | 713 | String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); 714 | return Optional.of(responseBody); 715 | } 716 | } catch (ConnectTimeoutException | SocketTimeoutException e) { 717 | getLog().error(String.format( 718 | "Failed to fetch release date for %s:%s %s (%s)", 719 | groupId, artifactId, version, "request timed out")); 720 | return Optional.empty(); 721 | } 722 | } 723 | } 724 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | org.apache.maven.skins 20 | maven-fluido-skin 21 | 1.11.1 22 | 23 | 24 | 25 |

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | mfoo/libyear-maven-plugin 41 | right 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/java/io/github/mfoo/libyear/InMemoryTestLogger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Martin Foot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.mfoo.libyear; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import org.apache.maven.plugin.logging.Log; 22 | 23 | class InMemoryTestLogger implements Log { 24 | 25 | public final List infoLogs = new ArrayList<>(); 26 | public final List errorLogs = new ArrayList<>(); 27 | public final List debugLogs = new ArrayList<>(); 28 | 29 | @Override 30 | public boolean isDebugEnabled() { 31 | return true; 32 | } 33 | 34 | @Override 35 | public void debug(CharSequence charSequence) { 36 | debugLogs.add(charSequence.toString()); 37 | } 38 | 39 | @Override 40 | public void debug(CharSequence charSequence, Throwable throwable) {} 41 | 42 | @Override 43 | public void debug(Throwable throwable) {} 44 | 45 | @Override 46 | public boolean isInfoEnabled() { 47 | return false; 48 | } 49 | 50 | @Override 51 | public void info(CharSequence charSequence) { 52 | infoLogs.add(charSequence.toString()); 53 | } 54 | 55 | @Override 56 | public void info(CharSequence charSequence, Throwable throwable) {} 57 | 58 | @Override 59 | public void info(Throwable throwable) {} 60 | 61 | @Override 62 | public boolean isWarnEnabled() { 63 | return false; 64 | } 65 | 66 | @Override 67 | public void warn(CharSequence charSequence) {} 68 | 69 | @Override 70 | public void warn(CharSequence charSequence, Throwable throwable) {} 71 | 72 | @Override 73 | public void warn(Throwable throwable) {} 74 | 75 | @Override 76 | public boolean isErrorEnabled() { 77 | return false; 78 | } 79 | 80 | @Override 81 | public void error(CharSequence charSequence) { 82 | errorLogs.add(charSequence.toString()); 83 | } 84 | 85 | @Override 86 | public void error(CharSequence charSequence, Throwable throwable) {} 87 | 88 | @Override 89 | public void error(Throwable throwable) {} 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/io/github/mfoo/libyear/LibYearMojoTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Martin Foot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.mfoo.libyear; 18 | 19 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 20 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 21 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.ok; 23 | import static com.github.tomakehurst.wiremock.client.WireMock.serverError; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 25 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 27 | import static java.util.Collections.emptyList; 28 | import static java.util.Collections.emptySet; 29 | import static java.util.Collections.singletonList; 30 | import static org.apache.maven.plugin.testing.ArtifactStubFactory.setVariableValueToObject; 31 | import static org.codehaus.mojo.versions.utils.MockUtils.mockAetherRepositorySystem; 32 | import static org.codehaus.mojo.versions.utils.MockUtils.mockRepositorySystem; 33 | import static org.junit.jupiter.api.Assertions.assertEquals; 34 | import static org.junit.jupiter.api.Assertions.assertFalse; 35 | import static org.junit.jupiter.api.Assertions.assertThrows; 36 | import static org.junit.jupiter.api.Assertions.assertTrue; 37 | 38 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 39 | import java.time.LocalDateTime; 40 | import java.time.ZoneOffset; 41 | import java.util.HashMap; 42 | import java.util.List; 43 | import java.util.Optional; 44 | import java.util.Set; 45 | import org.apache.maven.execution.MavenSession; 46 | import org.apache.maven.execution.ProjectDependencyGraph; 47 | import org.apache.maven.model.Dependency; 48 | import org.apache.maven.model.Plugin; 49 | import org.apache.maven.plugin.MojoExecutionException; 50 | import org.apache.maven.project.MavenProject; 51 | import org.codehaus.mojo.versions.filtering.WildcardMatcher; 52 | import org.codehaus.mojo.versions.utils.DependencyBuilder; 53 | import org.json.JSONArray; 54 | import org.json.JSONObject; 55 | import org.junit.jupiter.api.AfterEach; 56 | import org.junit.jupiter.api.Test; 57 | import org.junit.jupiter.api.extension.ExtendWith; 58 | import org.mockito.Mockito; 59 | import org.mockito.junit.jupiter.MockitoExtension; 60 | import org.mockito.junit.jupiter.MockitoSettings; 61 | import org.mockito.quality.Strictness; 62 | 63 | /** 64 | * Note: This class uses Strictness.LENIENT because some mocks are set up by the 65 | * versions-maven-plugin and these use an older version of Mockito. 66 | */ 67 | @ExtendWith(MockitoExtension.class) 68 | @MockitoSettings(strictness = Strictness.LENIENT) 69 | @WireMockTest(httpPort = 8080) 70 | public class LibYearMojoTest { 71 | // TODO: Tests with version numbers being referenced by variables 72 | // TODO: Test with version ranges 73 | 74 | /** 75 | * Test factory method. Generates a Plugin object representing the specified parameters. 76 | * 77 | * @param artifactId The Maven artifact ID 78 | * @param groupId The Maven group ID 79 | * @param version The Maven version 80 | * @return The Plugin object 81 | */ 82 | private static Plugin pluginOf(String artifactId, String groupId, String version) { 83 | Plugin plugin = new Plugin(); 84 | plugin.setGroupId(groupId); 85 | plugin.setArtifactId(artifactId); 86 | plugin.setVersion(version); 87 | return plugin; 88 | } 89 | 90 | @AfterEach 91 | public void reset() { 92 | // TODO: Make these private and add a reset method in the mojo 93 | LibYearMojo.libWeeksOutDated.set(0); 94 | LibYearMojo.dependencyVersionReleaseDates.clear(); 95 | } 96 | 97 | /** 98 | * This is a basic test to ensure that a project with a single dependency correctly shows 99 | * nothing when no updates are available. 100 | */ 101 | @Test 102 | public void dependencyIsAlreadyOnTheLatestVersion() throws Exception { 103 | LibYearMojo mojo = 104 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 105 | { 106 | put("default-dependency", new String[] {"1.0.0"}); 107 | } 108 | })) { 109 | { 110 | MavenProject project = new MavenProjectBuilder() 111 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 112 | .withGroupId("default-group") 113 | .withArtifactId("default-dependency") 114 | .withVersion("1.0.0") 115 | .build())) 116 | .build(); 117 | 118 | setProject(project); 119 | allowProcessingAllDependencies(this); 120 | setPluginContext(new HashMap<>()); 121 | 122 | setSession(mockMavenSession(project)); 123 | setSearchUri("http://localhost:8080"); 124 | 125 | setLog(new InMemoryTestLogger()); 126 | } 127 | }; 128 | 129 | LocalDateTime now = LocalDateTime.now(); 130 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 131 | 132 | mojo.execute(); 133 | 134 | assertTrue(((InMemoryTestLogger) mojo.getLog()).infoLogs.isEmpty()); 135 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 136 | } 137 | 138 | /** 139 | * This is a basic test to ensure that a project with a single dependency correctly shows 140 | * available updates. 141 | */ 142 | @Test 143 | public void dependencyUpdateAvailable() throws Exception { 144 | LibYearMojo mojo = 145 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 146 | { 147 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 148 | } 149 | })) { 150 | { 151 | MavenProject project = new MavenProjectBuilder() 152 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 153 | .withGroupId("default-group") 154 | .withArtifactId("default-dependency") 155 | .withVersion("1.0.0") 156 | .build())) 157 | .build(); 158 | 159 | setProject(project); 160 | allowProcessingAllDependencies(this); 161 | setPluginContext(new HashMap<>()); 162 | 163 | setSession(mockMavenSession(project)); 164 | setSearchUri("http://localhost:8080"); 165 | 166 | setLog(new InMemoryTestLogger()); 167 | } 168 | }; 169 | 170 | LocalDateTime now = LocalDateTime.now(); 171 | 172 | // Mark version 2.0.0 as a year newer 173 | // Don't stub 1.1.0, there's no need to check its version 174 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 175 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 176 | 177 | mojo.execute(); 178 | 179 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 180 | .infoLogs.stream() 181 | .anyMatch( 182 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 183 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 184 | } 185 | 186 | /** This test checks that the output for dependencies with long names is formatted correctly. */ 187 | @Test 188 | public void dependencyUpdateWithLongNameAvailable() throws Exception { 189 | LibYearMojo mojo = 190 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 191 | { 192 | put("default-dependency-with-very-very-long-name", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 193 | } 194 | })) { 195 | { 196 | MavenProject project = new MavenProjectBuilder() 197 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 198 | .withGroupId("default-group") 199 | .withArtifactId("default-dependency-with-very-very-long-name") 200 | .withVersion("1.0.0") 201 | .build())) 202 | .build(); 203 | setProject(project); 204 | allowProcessingAllDependencies(this); 205 | setPluginContext(new HashMap<>()); 206 | 207 | setSession(mockMavenSession(project)); 208 | setSearchUri("http://localhost:8080"); 209 | 210 | setLog(new InMemoryTestLogger()); 211 | } 212 | }; 213 | 214 | LocalDateTime now = LocalDateTime.now(); 215 | 216 | // Mark version 2.0.0 as a year newer 217 | stubResponseFor("default-group", "default-dependency-with-very-very-long-name", "1.0.0", now.minusYears(1)); 218 | stubResponseFor("default-group", "default-dependency-with-very-very-long-name", "2.0.0", now); 219 | 220 | mojo.execute(); 221 | 222 | List logs = ((InMemoryTestLogger) mojo.getLog()).infoLogs; 223 | Optional logLine = logs.stream() 224 | .filter((l) -> l.contains("default-group:default-dependency-with-very-very-long-name")) 225 | .findFirst(); 226 | assertTrue(logLine.isPresent()); 227 | 228 | int dependencyFirstLineIndex = logs.indexOf(logLine.get()); 229 | 230 | assertFalse(logs.get(dependencyFirstLineIndex).contains(".")); 231 | assertTrue(logs.get(dependencyFirstLineIndex + 1).startsWith(" ...") 232 | && logs.get(dependencyFirstLineIndex + 1).endsWith(("1.00 libyears"))); 233 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 234 | } 235 | 236 | @Test 237 | public void singleProjectWithDependencyUpdateAvailableDoesntShowEntireProjectLogLine() throws Exception { 238 | LibYearMojo mojo = 239 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 240 | { 241 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 242 | } 243 | })) { 244 | { 245 | MavenProject project = new MavenProjectBuilder() 246 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 247 | .withGroupId("default-group") 248 | .withArtifactId("default-dependency") 249 | .withVersion("1.0.0") 250 | .build())) 251 | .build(); 252 | 253 | setProject(project); 254 | allowProcessingAllDependencies(this); 255 | setPluginContext(new HashMap<>()); 256 | 257 | setSession(mockMavenSession(project)); 258 | setSearchUri("http://localhost:8080"); 259 | 260 | setLog(new InMemoryTestLogger()); 261 | } 262 | }; 263 | 264 | LocalDateTime now = LocalDateTime.now(); 265 | 266 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 267 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 268 | 269 | mojo.execute(); 270 | 271 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 272 | .infoLogs.stream().noneMatch((l) -> l.contains("for the entire project"))); 273 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 274 | } 275 | 276 | @Test 277 | public void dependencyUpdateAvailableButDependencyIsExcluded() throws Exception { 278 | LibYearMojo mojo = 279 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 280 | { 281 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 282 | } 283 | })) { 284 | { 285 | MavenProject project = new MavenProjectBuilder() 286 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 287 | .withGroupId("default-group") 288 | .withArtifactId("default-dependency") 289 | .withVersion("1.0.0") 290 | .build())) 291 | .build(); 292 | setProject(project); 293 | allowProcessingAllDependencies(this); 294 | 295 | setVariableValueToObject( 296 | this, "dependencyExcludes", List.of("default-group:default-dependency")); 297 | 298 | setPluginContext(new HashMap<>()); 299 | 300 | setSession(mockMavenSession(project)); 301 | setSearchUri("http://localhost:8080"); 302 | 303 | setLog(new InMemoryTestLogger()); 304 | } 305 | }; 306 | 307 | // No need to stub output as no API calls should be made 308 | 309 | mojo.execute(); 310 | 311 | assertFalse(((InMemoryTestLogger) mojo.getLog()) 312 | .infoLogs.stream().anyMatch((l) -> l.contains("default-group:default-dependency"))); 313 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 314 | } 315 | 316 | @Test 317 | public void dependencyUpdateAvailableButVersionIsIgnored() throws Exception { 318 | LibYearMojo mojo = 319 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 320 | { 321 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 322 | } 323 | })) { 324 | { 325 | MavenProject project = new MavenProjectBuilder() 326 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 327 | .withGroupId("default-group") 328 | .withArtifactId("default-dependency") 329 | .withVersion("1.0.0") 330 | .build())) 331 | .build(); 332 | setProject(project); 333 | allowProcessingAllDependencies(this); 334 | 335 | setVariableValueToObject(this, "ignoredVersions", Set.of("2.0.0")); 336 | 337 | setPluginContext(new HashMap<>()); 338 | 339 | setSession(mockMavenSession(project)); 340 | setSearchUri("http://localhost:8080"); 341 | 342 | setLog(new InMemoryTestLogger()); 343 | } 344 | }; 345 | 346 | // No need to stub output as no API calls should be made 347 | 348 | mojo.execute(); 349 | 350 | assertFalse(((InMemoryTestLogger) mojo.getLog()) 351 | .infoLogs.stream().anyMatch((l) -> l.contains("default-group:default-dependency"))); 352 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 353 | } 354 | 355 | @Test 356 | public void dependencyUpdateAvailableButProcessingDependenciesIsDisabled() throws Exception { 357 | LibYearMojo mojo = 358 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 359 | { 360 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 361 | } 362 | })) { 363 | { 364 | MavenProject project = new MavenProjectBuilder() 365 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 366 | .withGroupId("default-group") 367 | .withArtifactId("default-dependency") 368 | .withVersion("1.0.0") 369 | .build())) 370 | .build(); 371 | setProject(project); 372 | allowProcessingAllDependencies(this); 373 | setVariableValueToObject(this, "processDependencies", false); 374 | 375 | setPluginContext(new HashMap<>()); 376 | 377 | setSession(mockMavenSession(project)); 378 | setSearchUri("http://localhost:8080"); 379 | 380 | setLog(new InMemoryTestLogger()); 381 | } 382 | }; 383 | 384 | LocalDateTime now = LocalDateTime.now(); 385 | 386 | // Mark version 2.0.0 as a year newer 387 | // Don't stub 1.1.0, there's no need to check its version 388 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 389 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 390 | 391 | mojo.execute(); 392 | 393 | assertFalse(((InMemoryTestLogger) mojo.getLog()) 394 | .infoLogs.stream().anyMatch((l) -> l.contains("default-group:default-dependency"))); 395 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 396 | } 397 | 398 | /** 399 | * The plugin should cache the result of API calls to find the release date of every dependency 400 | * that it makes. This test ensures that despite running the plugin twice, we only make one API 401 | * call for each dependency version. 402 | */ 403 | @Test 404 | public void cacheIsUsedForDependencyVersions() throws Exception { 405 | // Run one of the tests twice, which should only trigger one fetch per 406 | // dependency version 407 | dependencyUpdateAvailable(); 408 | dependencyUpdateAvailable(); 409 | 410 | // One call for v1.0.0 411 | verify( 412 | 1, 413 | getRequestedFor(urlPathEqualTo("/solrsearch/select")) 414 | .withQueryParam("q", equalTo("g:default-group AND a:default-dependency AND v:1.0.0"))); 415 | 416 | // One call for v2.0.0 417 | verify( 418 | 1, 419 | getRequestedFor(urlPathEqualTo("/solrsearch/select")) 420 | .withQueryParam("q", equalTo("g:default-group AND a:default-dependency AND v:2.0.0"))); 421 | } 422 | 423 | @Test 424 | public void dependencyUpdateAvailableWhenVersionSpecifiedInDependencyManagement() throws Exception { 425 | LibYearMojo mojo = 426 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 427 | { 428 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 429 | } 430 | })) { 431 | { 432 | MavenProject project = new MavenProjectBuilder() 433 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 434 | .withGroupId("default-group") 435 | .withArtifactId("default-dependency") 436 | .withVersion("1.0.0") // This is 437 | // overridden by 438 | // dependencyManagement 439 | .build())) 440 | .withDependencyManagementDependencyList(singletonList(DependencyBuilder.newBuilder() 441 | .withGroupId("default-group") 442 | .withArtifactId("default-dependency") 443 | .withVersion("1.1.0") 444 | .build())) 445 | .build(); 446 | setProject(project); 447 | allowProcessingAllDependencies(this); 448 | setPluginContext(new HashMap<>()); 449 | 450 | setSession(mockMavenSession(project)); 451 | setSearchUri("http://localhost:8080"); 452 | 453 | setLog(new InMemoryTestLogger()); 454 | } 455 | }; 456 | 457 | LocalDateTime now = LocalDateTime.now(); 458 | 459 | // Mark version 2.0.0 as a year newer than the version specified by 460 | // dependencyManagement 461 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(2)); 462 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 463 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 464 | 465 | mojo.execute(); 466 | 467 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 468 | .infoLogs.stream() 469 | .anyMatch( 470 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 471 | 472 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 473 | } 474 | 475 | /** 476 | * This test has a project with a dependency with version 1.0.0. Dependency management pins it 477 | * at 1.1.0, and 2.0.0 is available. We exclude the plugin from considering dependency 478 | * management, meaning we should show the age between 1.0.0 and 2.0.0. 479 | */ 480 | @Test 481 | public void 482 | dependencyUpdateAvailableWhenVersionSpecifiedInDependencyManagementButDependencyExcludedFromDependencyManagement() 483 | throws Exception { 484 | LibYearMojo mojo = 485 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 486 | { 487 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 488 | } 489 | })) { 490 | { 491 | MavenProject project = new MavenProjectBuilder() 492 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 493 | .withGroupId("default-group") 494 | .withArtifactId("default-dependency") 495 | .withVersion("1.0.0") // This is 496 | // overridden by 497 | // dependencyManagement 498 | .build())) 499 | .withDependencyManagementDependencyList(singletonList(DependencyBuilder.newBuilder() 500 | .withGroupId("default-group") 501 | .withArtifactId("default-dependency") 502 | .withVersion("1.1.0") 503 | .build())) 504 | .build(); 505 | 506 | setProject(project); 507 | allowProcessingAllDependencies(this); 508 | 509 | setVariableValueToObject( 510 | this, "dependencyManagementExcludes", List.of("default-group:default-dependency")); 511 | 512 | setPluginContext(new HashMap<>()); 513 | 514 | setSession(mockMavenSession(project)); 515 | setSearchUri("http://localhost:8080"); 516 | 517 | setLog(new InMemoryTestLogger()); 518 | } 519 | }; 520 | 521 | LocalDateTime now = LocalDateTime.now(); 522 | 523 | // Mark version 2.0.0 as a year newer than the version specified by 524 | // dependencyManagement 525 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(2)); 526 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 527 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 528 | 529 | mojo.execute(); 530 | 531 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 532 | .infoLogs.stream() 533 | .anyMatch( 534 | (l) -> l.contains("default-group:default-dependency") && l.contains("2.00 libyears"))); 535 | 536 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 537 | } 538 | 539 | /** 540 | * This test has a project with a two-year-old dependency where the dependency version is 541 | * overridden by the dependencyManagement section to use a one-year-old version, but the plugin 542 | * setting enableDependencyManagement is set to false. We should ignore the one-year-old version 543 | * and suggest that the dependency is two years out-of-date. 544 | */ 545 | @Test 546 | public void 547 | dependencyUpdateAvailableWhenVersionSpecifiedInDependencyManagementWhereProcessDependencyManagementIsDisabled() 548 | throws Exception { 549 | LibYearMojo mojo = 550 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 551 | { 552 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 553 | } 554 | })) { 555 | { 556 | MavenProject project = new MavenProjectBuilder() 557 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 558 | .withGroupId("default-group") 559 | .withArtifactId("default-dependency") 560 | .withVersion("1.0.0") // This is 561 | // overridden by 562 | // dependencyManagement 563 | .build())) 564 | .withDependencyManagementDependencyList(singletonList(DependencyBuilder.newBuilder() 565 | .withGroupId("default-group") 566 | .withArtifactId("default-dependency") 567 | .withVersion("1.1.0") 568 | .build())) 569 | .build(); 570 | 571 | setProject(project); 572 | allowProcessingAllDependencies(this); 573 | setVariableValueToObject(this, "processDependencyManagement", false); 574 | setPluginContext(new HashMap<>()); 575 | 576 | setSession(mockMavenSession(project)); 577 | setSearchUri("http://localhost:8080"); 578 | 579 | setLog(new InMemoryTestLogger()); 580 | } 581 | }; 582 | 583 | LocalDateTime now = LocalDateTime.now(); 584 | 585 | // Mark version 2.0.0 as a year newer than the version specified by 586 | // dependencyManagement 587 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(2)); 588 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 589 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 590 | 591 | mojo.execute(); 592 | 593 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 594 | .infoLogs.stream() 595 | .anyMatch( 596 | (l) -> l.contains("default-group:default-dependency") && l.contains("2.00 libyears"))); 597 | 598 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 599 | } 600 | 601 | /** 602 | * This test has a dependency with version 1.0.0, which is overridden by dependencyManagement to 603 | * 1.1.0. Version 2.0.0 is available. 604 | * 605 | *

We ensure that the proposed change is the libyears between 1.1.0 and 2.0.0. 606 | */ 607 | @Test 608 | public void dependencyUpdateAvailableDependencyDefinedInDependencyManagement() throws Exception { 609 | LibYearMojo mojo = 610 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 611 | { 612 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 613 | } 614 | })) { 615 | { 616 | MavenProject project = new MavenProjectBuilder() 617 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 618 | .withGroupId("default-group") 619 | .withArtifactId("default-dependency") 620 | .withVersion("1.0.0") 621 | .build())) 622 | .withDependencyManagementDependencyList(singletonList(DependencyBuilder.newBuilder() 623 | .withGroupId("default-group") 624 | .withArtifactId("default-dependency") 625 | .withVersion("1.1.0") 626 | .build())) 627 | .build(); 628 | 629 | setProject(project); 630 | allowProcessingAllDependencies(this); 631 | setPluginContext(new HashMap<>()); 632 | 633 | setSession(mockMavenSession(project)); 634 | setSearchUri("http://localhost:8080"); 635 | 636 | setLog(new InMemoryTestLogger()); 637 | } 638 | }; 639 | 640 | LocalDateTime now = LocalDateTime.now(); 641 | 642 | // Mark version 2.0.0 as a year newer. The current dependency version is 1.1.0, 643 | // overridden from 644 | // 1.0.0 by 645 | // dependencyManagement 646 | 647 | // TODO: This first stub probably shouldn't be needed as we shouldn't need to 648 | // fetch the release 649 | // year of this version 650 | // but without it, the test fails. 651 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(10)); 652 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 653 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 654 | 655 | mojo.execute(); 656 | 657 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 658 | .infoLogs.stream() 659 | .anyMatch( 660 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 661 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 662 | } 663 | 664 | @Test 665 | public void pluginVersionUpdateAvailable() throws Exception { 666 | Plugin plugin = pluginOf("default-plugin", "default-group", "1.0.0"); 667 | plugin.setDependencies(singletonList(dependencyFor("default-group", "default-dependency", "1.0.0"))); 668 | 669 | LibYearMojo mojo = 670 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 671 | { 672 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 673 | } 674 | })) { 675 | { 676 | MavenProject project = new MavenProjectBuilder() 677 | .withPlugins(singletonList(plugin)) 678 | .build(); 679 | 680 | setProject(project); 681 | 682 | allowProcessingAllDependencies(this); 683 | setPluginContext(new HashMap<>()); 684 | 685 | setSession(mockMavenSession(project)); 686 | setSearchUri("http://localhost:8080"); 687 | 688 | setLog(new InMemoryTestLogger()); 689 | } 690 | }; 691 | 692 | LocalDateTime now = LocalDateTime.now(); 693 | 694 | // Mark version 2.0.0 as a year newer 695 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 696 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 697 | 698 | mojo.execute(); 699 | 700 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 701 | .infoLogs.stream() 702 | .anyMatch( 703 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 704 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 705 | } 706 | 707 | /** 708 | * This test has a plugin version 1.0.0 which has a dependency on another artifact version 709 | * 1.0.1. That version is overridden in dependency management to 1.1.0. Version 2.0.0 is 710 | * available. 711 | * 712 | *

This test ensures we suggest the libyears between 1.1.0 and 2.0.0. 713 | */ 714 | @Test 715 | public void pluginVersionUpdateAvailableDependencyDefinedInDependencyManagement() throws Exception { 716 | Plugin plugin = pluginOf("default-plugin", "default-group", "1.0.0"); 717 | plugin.setDependencies(singletonList(dependencyFor("default-group", "default-dependency", "1.0.1"))); 718 | 719 | LibYearMojo mojo = 720 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 721 | { 722 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 723 | } 724 | })) { 725 | { 726 | MavenProject project = new MavenProjectBuilder() 727 | .withPlugins(singletonList(plugin)) 728 | .withDependencyManagementDependencyList(singletonList(DependencyBuilder.newBuilder() 729 | .withGroupId("default-group") 730 | .withArtifactId("default-dependency") 731 | .withVersion("1.1.0") 732 | .build())) 733 | .build(); 734 | 735 | setProject(project); 736 | 737 | allowProcessingAllDependencies(this); 738 | setPluginContext(new HashMap<>()); 739 | 740 | setSession(mockMavenSession(project)); 741 | setSearchUri("http://localhost:8080"); 742 | 743 | setLog(new InMemoryTestLogger()); 744 | } 745 | }; 746 | 747 | LocalDateTime now = LocalDateTime.now(); 748 | 749 | // Mark version 2.0.0 as a year newer 750 | stubResponseFor("default-group", "default-dependency", "1.0.1", now.minusYears(2)); 751 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 752 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 753 | 754 | mojo.execute(); 755 | 756 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 757 | .infoLogs.stream() 758 | .anyMatch( 759 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 760 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 761 | } 762 | 763 | /** 764 | * This test has a plugin version 1.0.0. That version is overridden in plugin management to 765 | * 1.1.0. Version 2.0.0 is available. 766 | * 767 | *

This test ensures we suggest the libyears between 1.1.0 and 2.0.0. 768 | */ 769 | @Test 770 | public void pluginVersionUpdateAvailableDependencyDefinedInPluginManagement() throws Exception { 771 | Plugin plugin = pluginOf("default-plugin", "default-group", "1.0.0"); 772 | plugin.setDependencies(singletonList(dependencyFor("default-group", "default-dependency", "1.0.1"))); 773 | 774 | Plugin managedPlugin = pluginOf("default-plugin", "default-group", "1.0.0"); 775 | plugin.setDependencies(singletonList(dependencyFor("default-group", "default-dependency", "1.1.0"))); 776 | 777 | LibYearMojo mojo = 778 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 779 | { 780 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 781 | } 782 | })) { 783 | { 784 | MavenProject project = new MavenProjectBuilder() 785 | .withPlugins(singletonList(plugin)) 786 | .withPluginManagementPluginList(singletonList(managedPlugin)) 787 | .build(); 788 | 789 | setProject(project); 790 | 791 | allowProcessingAllDependencies(this); 792 | 793 | setPluginContext(new HashMap<>()); 794 | 795 | setSession(mockMavenSession(project)); 796 | setSearchUri("http://localhost:8080"); 797 | 798 | setLog(new InMemoryTestLogger()); 799 | } 800 | }; 801 | 802 | LocalDateTime now = LocalDateTime.now(); 803 | 804 | // Mark version 2.0.0 as a year newer 805 | stubResponseFor("default-group", "default-dependency", "1.1.0", now.minusYears(1)); 806 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 807 | 808 | mojo.execute(); 809 | 810 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 811 | .infoLogs.stream() 812 | .anyMatch( 813 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 814 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 815 | } 816 | 817 | private Dependency dependencyFor(String groupID, String artifactId, String version) { 818 | Dependency dep = new Dependency(); 819 | dep.setGroupId(groupID); 820 | dep.setArtifactId(artifactId); 821 | dep.setVersion(version); 822 | return dep; 823 | } 824 | 825 | /** 826 | * Stub the Maven search API response that looks for release information for a particular 827 | * version of a particular artifact 828 | */ 829 | private void stubResponseFor(String groupId, String artifactId, String version, LocalDateTime time) { 830 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 831 | .withQueryParam("q", equalTo(String.format("g:%s AND a:%s AND v:%s", groupId, artifactId, version))) 832 | .withQueryParam("wt", equalTo("json")) 833 | .willReturn(ok(getJSONResponseForVersion(groupId, artifactId, version, time)))); 834 | } 835 | 836 | /** 837 | * Simulate the JSON response from the Maven Central repository search API. Docs available at here. 839 | */ 840 | private String getJSONResponseForVersion( 841 | String groupId, String artifactId, String version, LocalDateTime releaseDate) { 842 | JSONObject root = new JSONObject(); 843 | JSONObject response = new JSONObject(); 844 | JSONArray versions = new JSONArray(); 845 | JSONObject newVersion = new JSONObject(); 846 | 847 | newVersion.put("id", String.format("%s:%s:%s", groupId, artifactId, version)); 848 | newVersion.put("g", groupId); 849 | newVersion.put("a", artifactId); 850 | newVersion.put("v", version); 851 | newVersion.put("p", "jar"); 852 | newVersion.put("timestamp", releaseDate.toEpochSecond(ZoneOffset.UTC) * 1000); // API response is in millis 853 | 854 | versions.put(newVersion); 855 | response.put("docs", versions); 856 | response.put("numFound", 1); 857 | root.put("response", response); 858 | 859 | return root.toString(); 860 | } 861 | 862 | @Test 863 | public void apiCallToMavenFails() throws Exception { 864 | LibYearMojo mojo = 865 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 866 | { 867 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 868 | } 869 | })) { 870 | { 871 | MavenProject project = new MavenProjectBuilder() 872 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 873 | .withGroupId("default-group") 874 | .withArtifactId("default-dependency") 875 | .withVersion("1.0.0") 876 | .build())) 877 | .build(); 878 | 879 | setProject(project); 880 | allowProcessingAllDependencies(this); 881 | setPluginContext(new HashMap<>()); 882 | 883 | setSession(mockMavenSession(project)); 884 | setSearchUri("http://localhost:8080"); 885 | 886 | setLog(new InMemoryTestLogger()); 887 | } 888 | }; 889 | 890 | stubFor(get(urlPathEqualTo("/solrsearch/select")).willReturn(serverError())); 891 | 892 | mojo.execute(); 893 | 894 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 895 | .errorLogs.stream() 896 | .anyMatch((l) -> l.contains( 897 | "Failed to fetch release date for" + " default-group:default-dependency" + " 1.0.0"))); 898 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 899 | .errorLogs.stream() 900 | .anyMatch((l) -> l.contains( 901 | "Failed to fetch release date for" + " default-group:default-dependency" + " 2.0.0"))); 902 | } 903 | 904 | @Test 905 | public void apiCallToMavenTimesOut() throws Exception { 906 | LibYearMojo mojo = 907 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 908 | { 909 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 910 | } 911 | })) { 912 | { 913 | MavenProject project = new MavenProjectBuilder() 914 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 915 | .withGroupId("default-group") 916 | .withArtifactId("default-dependency") 917 | .withVersion("1.0.0") 918 | .build())) 919 | .build(); 920 | 921 | setProject(project); 922 | allowProcessingAllDependencies(this); 923 | setPluginContext(new HashMap<>()); 924 | 925 | setSession(mockMavenSession(project)); 926 | setSearchUri("http://localhost:8080"); 927 | setHttpTimeout(1); 928 | setFetchRetryCount(0); 929 | 930 | setLog(new InMemoryTestLogger()); 931 | } 932 | }; 933 | 934 | stubFor(get(urlPathEqualTo("/solrsearch/select")).willReturn(ok().withFixedDelay(10_000 /* ms */))); 935 | 936 | mojo.execute(); 937 | 938 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 939 | .errorLogs.stream() 940 | .anyMatch((l) -> l.contains("Failed to fetch release date for" 941 | + " default-group:default-dependency" 942 | + " 1.0.0 (request timed out)"))); 943 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 944 | .errorLogs.stream() 945 | .anyMatch((l) -> l.contains("Failed to fetch release date for" 946 | + " default-group:default-dependency" 947 | + " 2.0.0 (request timed out)"))); 948 | assertEquals(2, ((InMemoryTestLogger) mojo.getLog()).errorLogs.size()); 949 | } 950 | 951 | @Test 952 | public void apiCallToMavenRetriesOnFailure() throws Exception { 953 | LibYearMojo mojo = 954 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 955 | { 956 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 957 | } 958 | })) { 959 | { 960 | MavenProject project = new MavenProjectBuilder() 961 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 962 | .withGroupId("default-group") 963 | .withArtifactId("default-dependency") 964 | .withVersion("1.0.0") 965 | .build())) 966 | .build(); 967 | 968 | setProject(project); 969 | allowProcessingAllDependencies(this); 970 | setPluginContext(new HashMap<>()); 971 | 972 | setSession(mockMavenSession(project)); 973 | setSearchUri("http://localhost:8080"); 974 | 975 | setFetchRetryCount(2); 976 | 977 | setLog(new InMemoryTestLogger()); 978 | } 979 | }; 980 | 981 | LocalDateTime now = LocalDateTime.now(); 982 | 983 | // The first dependency should fail twice and return OK on the second retry 984 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 985 | .withQueryParam( 986 | "q", 987 | equalTo(String.format( 988 | "g:%s AND a:%s AND v:%s", "default-group", "default-dependency", "1.0.0"))) 989 | .inScenario("Failure chain") 990 | .whenScenarioStateIs("Started") 991 | .willReturn(serverError()) 992 | .willSetStateTo("First failure")); 993 | 994 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 995 | .inScenario("Failure chain") 996 | .whenScenarioStateIs("First failure") 997 | .willReturn(serverError()) 998 | .willSetStateTo("Second failure")); 999 | 1000 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 1001 | .inScenario("Failure chain") 1002 | .whenScenarioStateIs("Second failure") 1003 | .willReturn(ok( 1004 | getJSONResponseForVersion("default-group", "default-dependency", "1.0.0", now.minusYears(1))))); 1005 | 1006 | // The second request will succeed first time 1007 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 1008 | 1009 | mojo.execute(); 1010 | 1011 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 1012 | .infoLogs.stream() 1013 | .anyMatch( 1014 | (l) -> l.contains("default-group:default-dependency") && l.contains("1.00 libyears"))); 1015 | } 1016 | 1017 | @Test 1018 | public void dependencyUpdateAvailableButFetchingReleaseDateOfNewerVersionFails() throws Exception { 1019 | LibYearMojo mojo = 1020 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 1021 | { 1022 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 1023 | } 1024 | })) { 1025 | { 1026 | MavenProject project = new MavenProjectBuilder() 1027 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 1028 | .withGroupId("default-group") 1029 | .withArtifactId("default-dependency") 1030 | .withVersion("1.0.0") 1031 | .build())) 1032 | .build(); 1033 | 1034 | setProject(project); 1035 | 1036 | allowProcessingAllDependencies(this); 1037 | 1038 | setPluginContext(new HashMap<>()); 1039 | 1040 | setSession(mockMavenSession(project)); 1041 | setSearchUri("http://localhost:8080"); 1042 | 1043 | setLog(new InMemoryTestLogger()); 1044 | } 1045 | }; 1046 | 1047 | LocalDateTime now = LocalDateTime.now(); 1048 | 1049 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 1050 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 1051 | .withQueryParam( 1052 | "q", 1053 | equalTo(String.format( 1054 | "g:%s AND a:%s AND v:%s", "default-group", "default-dependency", "2.0.0"))) 1055 | .withQueryParam("wt", equalTo("json")) 1056 | .willReturn(serverError())); 1057 | 1058 | mojo.execute(); 1059 | 1060 | assertFalse(((InMemoryTestLogger) mojo.getLog()) 1061 | .infoLogs.stream().anyMatch((l) -> l.contains("default-group:default-dependency"))); 1062 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 1063 | .errorLogs.contains( 1064 | "Failed to fetch release date for default-group:default-dependency" + " 2.0.0: Server Error")); 1065 | } 1066 | 1067 | @Test 1068 | public void dependencyUpdateAvailableButAPISearchDoesNotContainLatestVersion() throws Exception { 1069 | LibYearMojo mojo = 1070 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 1071 | { 1072 | put("default-dependency", new String[] {"1.0.0", "1.1.0", "2.0.0"}); 1073 | } 1074 | })) { 1075 | { 1076 | MavenProject project = new MavenProjectBuilder() 1077 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 1078 | .withGroupId("default-group") 1079 | .withArtifactId("default-dependency") 1080 | .withVersion("1.0.0") 1081 | .build())) 1082 | .build(); 1083 | 1084 | setProject(project); 1085 | allowProcessingAllDependencies(this); 1086 | setPluginContext(new HashMap<>()); 1087 | 1088 | setSession(mockMavenSession(project)); 1089 | setSearchUri("http://localhost:8080"); 1090 | 1091 | setLog(new InMemoryTestLogger()); 1092 | } 1093 | }; 1094 | 1095 | LocalDateTime now = LocalDateTime.now(); 1096 | 1097 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 1098 | stubFor(get(urlPathEqualTo("/solrsearch/select")) 1099 | .withQueryParam( 1100 | "q", 1101 | equalTo(String.format( 1102 | "g:%s AND a:%s AND v:%s", "default-group", "default-dependency", "2.0.0"))) 1103 | .withQueryParam("wt", equalTo("json")) 1104 | .willReturn(ok("{\"response\":{\"docs\":[],\"numFound\":0}}"))); 1105 | 1106 | mojo.execute(); 1107 | 1108 | assertFalse(((InMemoryTestLogger) mojo.getLog()) 1109 | .infoLogs.stream().anyMatch((l) -> l.contains("default-group:default-dependency"))); 1110 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 1111 | .debugLogs.contains("Could not find artifact for default-group:default-dependency" + " 2.0.0")); 1112 | } 1113 | 1114 | @Test 1115 | public void multipleDependenciesWithUpdatesAreSortedAlphabetically() throws Exception { 1116 | LibYearMojo mojo = 1117 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 1118 | { 1119 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 1120 | put("second-dependency", new String[] {"5.0.0", "6.0.0"}); 1121 | } 1122 | })) { 1123 | { 1124 | MavenProject project = new MavenProjectBuilder() 1125 | .withDependencies(List.of( 1126 | DependencyBuilder.newBuilder() 1127 | .withGroupId("default-group") 1128 | .withArtifactId("second-dependency") 1129 | .withVersion("5.0.0") 1130 | .build(), 1131 | DependencyBuilder.newBuilder() 1132 | .withGroupId("default-group") 1133 | .withArtifactId("default-dependency") 1134 | .withVersion("1.0.0") 1135 | .build())) 1136 | .build(); 1137 | 1138 | setProject(project); 1139 | allowProcessingAllDependencies(this); 1140 | setPluginContext(new HashMap<>()); 1141 | 1142 | setSession(mockMavenSession(project)); 1143 | setSearchUri("http://localhost:8080"); 1144 | 1145 | setLog(new InMemoryTestLogger()); 1146 | } 1147 | }; 1148 | 1149 | LocalDateTime now = LocalDateTime.now(); 1150 | 1151 | // Mark version 2.0.0 as a year newer 1152 | // Don't stub 1.1.0, there's no need to check its version 1153 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 1154 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 1155 | stubResponseFor("default-group", "second-dependency", "5.0.0", now.minusYears(5)); 1156 | stubResponseFor("default-group", "second-dependency", "6.0.0", now.minusYears(3)); 1157 | 1158 | mojo.execute(); 1159 | 1160 | List infoLogs = ((InMemoryTestLogger) mojo.getLog()).infoLogs; 1161 | String firstLogInvolvingDefaultGroup = infoLogs.stream() 1162 | .filter(l -> l.contains("default-group")) 1163 | .findFirst() 1164 | .get(); 1165 | int indexOfFirstLog = infoLogs.indexOf(firstLogInvolvingDefaultGroup); 1166 | assertTrue(infoLogs.get(indexOfFirstLog + 1).contains("second-dependency")); 1167 | assertTrue(((InMemoryTestLogger) mojo.getLog()).errorLogs.isEmpty()); 1168 | } 1169 | 1170 | @Test 1171 | public void projectExceedsMaxLibYearsAndShouldFailTheBuild() throws Exception { 1172 | LibYearMojo mojo = 1173 | new LibYearMojo(mockRepositorySystem(), mockAetherRepositorySystem(new HashMap<>() { 1174 | { 1175 | put("default-dependency", new String[] {"1.0.0", "2.0.0"}); 1176 | } 1177 | })) { 1178 | { 1179 | MavenProject project = new MavenProjectBuilder() 1180 | .withDependencies(singletonList(DependencyBuilder.newBuilder() 1181 | .withGroupId("default-group") 1182 | .withArtifactId("default-dependency") 1183 | .withVersion("1.0.0") 1184 | .build())) 1185 | .build(); 1186 | 1187 | setProject(project); 1188 | allowProcessingAllDependencies(this); 1189 | 1190 | setVariableValueToObject(this, "maxLibYears", 0.1f); 1191 | 1192 | setPluginContext(new HashMap<>()); 1193 | 1194 | setSession(mockMavenSession(project)); 1195 | setSearchUri("http://localhost:8080"); 1196 | 1197 | setLog(new InMemoryTestLogger()); 1198 | } 1199 | }; 1200 | 1201 | LocalDateTime now = LocalDateTime.now(); 1202 | 1203 | stubResponseFor("default-group", "default-dependency", "1.0.0", now.minusYears(1)); 1204 | stubResponseFor("default-group", "default-dependency", "2.0.0", now); 1205 | 1206 | MojoExecutionException ex = assertThrows(MojoExecutionException.class, mojo::execute); 1207 | assertEquals("Dependencies exceed maximum specified age in libyears", ex.getMessage()); 1208 | 1209 | assertTrue(((InMemoryTestLogger) mojo.getLog()) 1210 | .errorLogs.stream() 1211 | .anyMatch(l -> 1212 | l.contains("This module exceeds the maximum" + " dependency age of 0.1 libyears"))); 1213 | } 1214 | 1215 | private void allowProcessingAllDependencies(LibYearMojo mojo) throws IllegalAccessException { 1216 | setVariableValueToObject(mojo, "ignoredVersions", emptySet()); 1217 | setVariableValueToObject(mojo, "processDependencies", true); 1218 | setVariableValueToObject(mojo, "processPluginDependencies", true); 1219 | setVariableValueToObject(mojo, "pluginDependencyIncludes", singletonList(WildcardMatcher.WILDCARD)); 1220 | setVariableValueToObject(mojo, "pluginDependencyExcludes", emptyList()); 1221 | setVariableValueToObject(mojo, "processPluginDependenciesInPluginManagement", true); 1222 | setVariableValueToObject(mojo, "pluginManagementDependencyIncludes", singletonList(WildcardMatcher.WILDCARD)); 1223 | setVariableValueToObject(mojo, "pluginManagementDependencyExcludes", emptyList()); 1224 | setVariableValueToObject(mojo, "processDependencyManagement", true); 1225 | setVariableValueToObject(mojo, "dependencyManagementIncludes", singletonList(WildcardMatcher.WILDCARD)); 1226 | setVariableValueToObject(mojo, "dependencyManagementExcludes", emptyList()); 1227 | setVariableValueToObject(mojo, "dependencyIncludes", singletonList(WildcardMatcher.WILDCARD)); 1228 | setVariableValueToObject(mojo, "dependencyExcludes", emptyList()); 1229 | } 1230 | 1231 | private MavenSession mockMavenSession(MavenProject project) { 1232 | MavenSession session = org.codehaus.mojo.versions.utils.MockUtils.mockMavenSession(); 1233 | Mockito.when(session.getProjectDependencyGraph()).thenReturn(new ProjectDependencyGraph() { 1234 | @Override 1235 | public List getAllProjects() { 1236 | throw new UnsupportedOperationException(); 1237 | } 1238 | 1239 | @Override 1240 | public List getSortedProjects() { 1241 | return List.of(project); 1242 | } 1243 | 1244 | @Override 1245 | public List getDownstreamProjects(MavenProject mavenProject, boolean b) { 1246 | throw new UnsupportedOperationException(); 1247 | } 1248 | 1249 | @Override 1250 | public List getUpstreamProjects(MavenProject mavenProject, boolean b) { 1251 | throw new UnsupportedOperationException(); 1252 | } 1253 | }); 1254 | return session; 1255 | } 1256 | } 1257 | -------------------------------------------------------------------------------- /src/test/java/io/github/mfoo/libyear/MavenProjectBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Martin Foot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.mfoo.libyear; 18 | 19 | import java.util.Collections; 20 | import java.util.List; 21 | import org.apache.maven.model.Build; 22 | import org.apache.maven.model.Dependency; 23 | import org.apache.maven.model.DependencyManagement; 24 | import org.apache.maven.model.Model; 25 | import org.apache.maven.model.Plugin; 26 | import org.apache.maven.model.PluginManagement; 27 | import org.apache.maven.project.MavenProject; 28 | 29 | class MavenProjectBuilder { 30 | 31 | private List dependencyList = Collections.emptyList(); 32 | 33 | private List pluginList = Collections.emptyList(); 34 | 35 | private List dependencyManagementDependencyList = Collections.emptyList(); 36 | 37 | private List pluginManagementPluginList = Collections.emptyList(); 38 | 39 | public MavenProjectBuilder withDependencies(List dependencyList) { 40 | this.dependencyList = dependencyList; 41 | return this; 42 | } 43 | 44 | public MavenProjectBuilder withPlugins(List pluginList) { 45 | this.pluginList = pluginList; 46 | return this; 47 | } 48 | 49 | public MavenProjectBuilder withDependencyManagementDependencyList(List dependencyList) { 50 | this.dependencyManagementDependencyList = dependencyList; 51 | return this; 52 | } 53 | 54 | public MavenProjectBuilder withPluginManagementPluginList(List pluginList) { 55 | this.pluginManagementPluginList = pluginList; 56 | return this; 57 | } 58 | 59 | public MavenProject build() { 60 | Model model = new Model() { 61 | { 62 | setGroupId("default-group"); 63 | setArtifactId("default-artifact"); 64 | setVersion("1.0.0-SNAPSHOT"); 65 | 66 | setDependencies(dependencyList); 67 | 68 | Build build = new Build(); 69 | setBuild(build); 70 | 71 | build.setPlugins(pluginList); 72 | 73 | PluginManagement pluginManagement = new PluginManagement(); 74 | build.setPluginManagement(pluginManagement); 75 | pluginManagement.setPlugins(pluginManagementPluginList); 76 | 77 | DependencyManagement dependencyManagement = new DependencyManagement(); 78 | setDependencyManagement(dependencyManagement); 79 | dependencyManagement.setDependencies(dependencyManagementDependencyList); 80 | } 81 | }; 82 | 83 | MavenProject project = new MavenProject(); 84 | project.setModel(model); 85 | project.setOriginalModel(model); 86 | 87 | return project; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/resources/pom-no-dependencies.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | basic-libyear-report 10 | http://localhost/ 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/pom-single-outdated-dependency.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | localhost 6 | basic-single-dependency 7 | 1.0 8 | pom 9 | abstract-versions-report 10 | http://localhost/ 11 | 12 | 13 | Testing command line invocation of dependency- and plugin-updates-report 14 | 15 | 16 | 17 | 18 | commons-io 19 | commons-io 20 | 2.8.0 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------