├── .gitignore ├── versiondebt-shared ├── src │ ├── test │ │ ├── java │ │ │ └── com │ │ │ │ └── portofrotterdam │ │ │ │ └── versiondebt │ │ │ │ ├── VersiondebtTest.java │ │ │ │ └── PrettyFormatterTest.java │ │ └── resources │ │ │ └── versiondebt.xml │ └── main │ │ ├── resources │ │ └── versiondebt.xsd │ │ └── java │ │ └── com │ │ └── portofrotterdam │ │ └── versiondebt │ │ ├── VersiondebtsFactory.java │ │ ├── PrettyFormatter.java │ │ └── Versiondebts.java ├── pom.xml └── versiondebt-shared.iml ├── versiondebt-sonar-plugin ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── portofrotterdam │ │ │ └── versiondebt │ │ │ ├── VersiondebtDashboardWidget.java │ │ │ ├── VersiondebtPlugin.java │ │ │ ├── VersiondebtMetrics.java │ │ │ └── VersiondebtSensor.java │ │ └── resources │ │ └── versiondebt_dashboard_widget.html.erb └── pom.xml ├── versiondebt-maven-plugin ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── portofrotterdam │ └── versiondebt │ └── VersiondebtMojo.java ├── README.md ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | *.iml 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | 15 | .idea/ 16 | /target/ 17 | versiondebt-maven-plugin/target/ 18 | versiondebt-shared/target/ 19 | versiondebt-sonar-plugin/target/ 20 | -------------------------------------------------------------------------------- /versiondebt-shared/src/test/java/com/portofrotterdam/versiondebt/VersiondebtTest.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | public class VersiondebtTest { 9 | 10 | private VersiondebtsFactory versiondebtsFactory; 11 | 12 | @Before 13 | public void setUp() throws Exception { 14 | versiondebtsFactory = VersiondebtsFactory.newInstance(); 15 | } 16 | 17 | @Test 18 | public void create() throws Exception { 19 | final Versiondebts versiondebts = versiondebtsFactory.fromXML(getClass().getResourceAsStream("/versiondebt.xml")); 20 | assertNotNull(versiondebts); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/src/main/java/com/portofrotterdam/versiondebt/VersiondebtDashboardWidget.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import org.sonar.api.web.AbstractRubyTemplate; 4 | import org.sonar.api.web.Description; 5 | import org.sonar.api.web.RubyRailsWidget; 6 | import org.sonar.api.web.UserRole; 7 | 8 | 9 | @UserRole(UserRole.USER) 10 | @Description("Reports on outdated versions") 11 | public class VersiondebtDashboardWidget extends AbstractRubyTemplate implements RubyRailsWidget { 12 | 13 | @Override 14 | public String getId() { 15 | return "versiondebt"; 16 | } 17 | 18 | @Override 19 | public String getTitle() { 20 | return "Version Debt"; 21 | } 22 | 23 | @Override 24 | protected String getTemplatePath() { 25 | return "/versiondebt_dashboard_widget.html.erb"; 26 | } 27 | } -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/src/main/java/com/portofrotterdam/versiondebt/VersiondebtPlugin.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import org.sonar.api.CoreProperties; 4 | import org.sonar.api.Plugin; 5 | import org.sonar.api.config.PropertyDefinition; 6 | import org.sonar.api.resources.Qualifiers; 7 | 8 | public class VersiondebtPlugin implements Plugin { 9 | 10 | public static final String VERSIONDEBT_REPORT_PATH = "versiondebt.reportPath"; 11 | 12 | @Override 13 | public void define(Context context) { 14 | context.addExtensions( 15 | VersiondebtSensor.class, 16 | VersiondebtMetrics.class, 17 | VersiondebtDashboardWidget.class, 18 | PropertyDefinition.builder(VERSIONDEBT_REPORT_PATH) 19 | .category(CoreProperties.CATEGORY_JAVA) 20 | .subCategory("Versiondebt") 21 | .name("Report path") 22 | .description("Path (absolute or relative) to versiondebt xml file.") 23 | .defaultValue("target/versiondebt.xml") 24 | .onQualifiers(Qualifiers.PROJECT) 25 | .build() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /versiondebt-shared/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | com.portofrotterdam 7 | versiondebt 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 4.0.0 12 | versiondebt-shared 13 | jar 14 | 1.0.0-SNAPSHOT 15 | 16 | versiondebt-shared 17 | Shared code between Maven and Sonar plugin. 18 | 19 | 20 | 21 | com.thoughtworks.xstream 22 | xstream 23 | 24 | 25 | junit 26 | junit 27 | test 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/src/main/resources/versiondebt_dashboard_widget.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | <%= metric('total_dependency_debt_string').description -%> 8 | 9 | 10 | <%= format_measure('total_dependency_debt_string', :suffix => '', :url => url_for_drilldown('total_dependency_debt'), :default => '-') %> 11 | <%= dashboard_configuration.selected_period? ? format_variation('total_dependency_debt') : trend_icon('total_dependency_debt') -%> 12 | 13 | 14 |
15 | 16 |
17 | <%= metric('dependency_amount').description -%> 18 | 19 | <%= format_measure('dependency_amount', :suffix => '', :url => url_for_drilldown('dependency_amount'), :default => '-') %> 20 | <%= dashboard_configuration.selected_period? ? format_variation('dependency_amount') : trend_icon('dependency_amount') -%> 21 | 22 |
23 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /versiondebt-shared/src/main/resources/versiondebt.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /versiondebt-shared/src/main/java/com/portofrotterdam/versiondebt/VersiondebtsFactory.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import com.thoughtworks.xstream.XStream; 4 | 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.io.Writer; 8 | 9 | public class VersiondebtsFactory { 10 | 11 | private XStream stream; 12 | 13 | private VersiondebtsFactory(final ClassLoader classLoader) { 14 | stream = new XStream(); 15 | stream.setClassLoader(classLoader); 16 | stream.processAnnotations(new Class[]{ 17 | Versiondebts.class, 18 | Versiondebts.VersiondebtItem.class, 19 | Versiondebts.VersiondebtItem.Version.class}); 20 | } 21 | 22 | public static VersiondebtsFactory newInstance() { 23 | return new VersiondebtsFactory(Versiondebts.class.getClassLoader()); 24 | } 25 | 26 | public Versiondebts fromXML(final String xml) { 27 | return (Versiondebts) stream.fromXML(xml); 28 | } 29 | 30 | public Versiondebts fromXML(final InputStream inputStream) { 31 | return (Versiondebts) stream.fromXML(inputStream); 32 | } 33 | 34 | public void toXML(final Object object, final OutputStream out) { 35 | stream.toXML(object, out); 36 | } 37 | 38 | public void toXML(final Object object, final Writer out) { 39 | stream.toXML(object, out); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /versiondebt-shared/src/test/resources/versiondebt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7.0 5 | 2013-05-20 16:16:24.0 UTC 6 | 7 | 8 | 7.0 9 | 2013-05-20 16:16:24.0 UTC 10 | 11 | 12 | 13 | 14 | 2.2.16 15 | 2016-11-14 11:12:47.0 UTC 16 | 17 | 18 | 2.2.22 19 | 2016-11-21 11:17:11.0 UTC 20 | 21 | 22 | 23 | 24 | 4.11 25 | 2012-11-14 19:21:47.0 UTC 26 | 27 | 28 | 4.12 29 | 2014-12-04 16:17:43.0 UTC 30 | 31 | 32 | 33 | 34 | 1.7.6 35 | 2014-02-05 22:37:14.0 UTC 36 | 37 | 38 | 1.7.21 39 | 2016-04-04 18:36:40.0 UTC 40 | 41 | 42 | -------------------------------------------------------------------------------- /versiondebt-shared/versiondebt-shared.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /versiondebt-shared/src/test/java/com/portofrotterdam/versiondebt/PrettyFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class PrettyFormatterTest { 9 | 10 | @Test 11 | public void testMoreThanADay() { 12 | 13 | long millis = TimeUnit.DAYS.toMillis(365); 14 | Assert.assertEquals("365 days", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 15 | 16 | millis = TimeUnit.DAYS.toMillis(366); 17 | Assert.assertEquals("1 year, 1 day", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 18 | 19 | millis = TimeUnit.DAYS.toMillis(366) + TimeUnit.HOURS.toMillis(5); 20 | Assert.assertEquals("1 year, 1 day, 5 hours", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 21 | } 22 | 23 | @Test 24 | public void testLessThanAnHour() { 25 | 26 | long millis = TimeUnit.MINUTES.toMillis(1); 27 | Assert.assertEquals("1 minute", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 28 | 29 | millis += TimeUnit.MINUTES.toMillis(1); 30 | Assert.assertEquals("2 minutes", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 31 | 32 | millis += TimeUnit.SECONDS.toMillis(1); 33 | Assert.assertEquals("2 minutes and 1 second", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 34 | 35 | millis += TimeUnit.SECONDS.toMillis(1); 36 | Assert.assertEquals("2 minutes and 2 seconds", PrettyFormatter.formatMillisToYearsDaysHours(millis)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/src/main/java/com/portofrotterdam/versiondebt/VersiondebtMetrics.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import org.sonar.api.measures.CoreMetrics; 4 | import org.sonar.api.measures.Metric; 5 | import org.sonar.api.measures.Metrics; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import static java.util.Arrays.asList; 11 | 12 | public class VersiondebtMetrics implements Metrics { 13 | 14 | public static final Metric DEPENDENCY_AMOUNT = new Metric.Builder("dependency_amount", "Amount of outdated dependencies", Metric.ValueType.INT) 15 | .setDescription("Amount of outdated dependencies") 16 | .setBestValue(0.0) 17 | .setDomain(CoreMetrics.DOMAIN_MAINTAINABILITY) 18 | .setDirection(Metric.DIRECTION_WORST) 19 | .create(); 20 | 21 | public static final Metric TOTAL_DEPENDENCY_DEBT_DAYS = new Metric.Builder("total_dependency_debt", "Total dependency debt (in days)", Metric.ValueType 22 | .INT) 23 | .setBestValue(0.0) 24 | .setDescription("Total dependency debt (in days)") 25 | .setDomain(CoreMetrics.DOMAIN_MAINTAINABILITY) 26 | .setDirection(Metric.DIRECTION_WORST) 27 | .create(); 28 | 29 | public static final Metric TOTAL_DEPENDENCY_DEBT_STRING = new Metric.Builder("total_dependency_debt_string", "Total dependency debt", Metric 30 | .ValueType.STRING) 31 | .setHidden(true) 32 | .setDescription("Total dependency debt") 33 | .setDomain(CoreMetrics.DOMAIN_MAINTAINABILITY) 34 | .create(); 35 | 36 | @Override 37 | public List getMetrics() { 38 | return new ArrayList(asList(DEPENDENCY_AMOUNT, TOTAL_DEPENDENCY_DEBT_DAYS, TOTAL_DEPENDENCY_DEBT_STRING)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /versiondebt-shared/src/main/java/com/portofrotterdam/versiondebt/PrettyFormatter.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.stream.Collectors; 7 | 8 | 9 | public class PrettyFormatter { 10 | 11 | /** 12 | * Don't ask... there is probably a better way to do this, but this works (for now). 13 | * 14 | * @param millis 15 | * @return 16 | */ 17 | public static String formatMillisToYearsDaysHours(long millis) { 18 | long days = TimeUnit.MILLISECONDS.toDays(millis); 19 | millis -= TimeUnit.DAYS.toMillis(days); 20 | 21 | final long hours = TimeUnit.MILLISECONDS.toHours(millis); 22 | millis -= TimeUnit.HOURS.toMillis(hours); 23 | 24 | long years = 0; 25 | while(days>365) { 26 | days -= 365; 27 | years++; 28 | } 29 | 30 | List parts = new ArrayList<>(); 31 | parts.add((years>0)?(years>1)?years+" years":years+" year":null); 32 | parts.add((days>0)?(days>1)?days+" days":days+" day":null); 33 | parts.add((hours>0)?(hours>1)?hours+" hours":hours+" hour":null); 34 | 35 | final String formatted = parts.stream().filter(s -> s!=null).collect(Collectors.joining(", ")); 36 | 37 | if(formatted.length() == 0) { 38 | //Less than a day? 39 | if(millis > 0) { 40 | 41 | final long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); 42 | millis -= TimeUnit.MINUTES.toMillis(minutes); 43 | 44 | final long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); 45 | 46 | final String minutesPart = (minutes>0)?(minutes>1)?minutes+" minutes":minutes+" minute":""; 47 | final String secondsPart = (seconds>0)?(seconds>1)?" and "+seconds+" seconds":" and "+seconds+" second":""; 48 | return minutesPart + secondsPart; 49 | } 50 | return "-"; 51 | } 52 | return formatted; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | versiondebt-sonar-plugin 6 | sonar-plugin 7 | 1.0.0-SNAPSHOT 8 | versiondebt Sonar Plugin 9 | 10 | 11 | com.portofrotterdam 12 | versiondebt 13 | 1.0.0-SNAPSHOT 14 | 15 | 16 | 17 | 18 | 19 | org.sonarsource.sonar-packaging-maven-plugin 20 | sonar-packaging-maven-plugin 21 | true 22 | 23 | com.portofrotterdam.versiondebt.VersiondebtPlugin 24 | Plugin to display the measured dependency version debt. 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-compiler-plugin 30 | 31 | 32 | 33 | 34 | 35 | 36 | com.portofrotterdam 37 | versiondebt-shared 38 | 39 | 40 | org.sonarsource.sonarqube 41 | sonar-plugin-api 42 | 43 | provided 44 | 45 | 46 | joda-time 47 | joda-time 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /versiondebt-maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.apache.maven.plugins 5 | versiondebt-maven-plugin 6 | maven-plugin 7 | versiondebt-maven-plugin Maven Mojo 8 | http://maven.apache.org 9 | 10 | 11 | com.portofrotterdam 12 | versiondebt 13 | 1.0.0-SNAPSHOT 14 | ../pom.xml 15 | 16 | 17 | 18 | 19 | 20 | maven-plugin-plugin 21 | 22 | true 23 | versiondebt 24 | 25 | 26 | 27 | mojo-descriptor 28 | process-classes 29 | 30 | descriptor 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | com.portofrotterdam 40 | versiondebt-shared 41 | 42 | 43 | org.apache.maven 44 | maven-plugin-api 45 | 46 | 47 | org.apache.maven 48 | maven-model 49 | 50 | 51 | org.apache.maven 52 | maven-artifact 53 | 54 | 55 | 56 | 57 | org.apache.httpcomponents 58 | httpclient 59 | 60 | 61 | org.apache.maven.plugin-tools 62 | maven-plugin-annotations 63 | compile 64 | 65 | 66 | org.apache.maven 67 | maven-artifact 68 | 69 | 70 | 71 | 72 | org.apache.maven 73 | maven-project 74 | 75 | 76 | joda-time 77 | joda-time 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # versiondebt-plugin 2 | 3 | This project contains a simple Maven and SonarQube plugin to measure how much debt your dependency versions have. 4 | 5 | Example output of the Maven plugin: 6 | 7 | ``` 8 | [INFO] --- Dependency:org.apache.maven.plugin-tools : maven-plugin-annotations --- 9 | [INFO] 10 | [INFO] Current version: 3.2 Wed Nov 07 15:41:57 CET 2012 11 | [INFO] Latest version: 3.5 Fri Aug 26 23:20:30 CEST 2016 12 | [INFO] 13 | [INFO] Artifact debt: 3 years, 293 days, 6 hours 14 | ``` 15 | 16 | As you can see, we are using maven plugin tools 3.2 and this dependency has a whopping 3 years of new development we aren't using at the moment. 17 | 18 | ## Usage 19 | 20 | ### Maven plugin 21 | 22 | To use the Maven plugin you'll need to do the following. 23 | 24 | This plugin hasn't been published to Maven Central (yet?) so in order to use it you'll have to build it yourself. 25 | 26 | After building this project you'll be able to do: 27 | 28 | ``` 29 | mvn clean package versiondebt:generate 30 | ``` 31 | 32 | This outputs the version information in the Maven log, but also generates a file called *versiondebt.xml* in the target folder. This XML file contains all 33 | the information needed for the SonarQube plugin. 34 | 35 | ### SonarQube plugin 36 | 37 | If you have a SonarQube build in your CI environment you can use the SonarQube plugin as well. First you'll have to make sure the Maven plugin is running in your 38 | build. You can add it in the *pom.xml* file as follows: 39 | 40 | ```xml 41 | 42 | org.apache.maven.plugins 43 | versiondebt-maven-plugin 44 | 1.0.6 45 | 46 | 47 | versiondebt 48 | prepare-package 49 | 50 | generate 51 | 52 | 53 | 54 | 55 | ``` 56 | 57 | The plugin isn't very fast because it needs to do a couple of HTTP calls, if you decide to add it to the build, make a specific SonarQube profile in Maven. 58 | 59 | Next step is to put the plugin in SonarQube, this can be done by taking the compiled *versiondebt-sonar-plugin-1.0.0-SNAPSHOT.jar* and to put this in the plugins 60 | folder of your SonarQube installation. 61 | 62 | ## Contribution 63 | 64 | These plugins have been developed during a Port of Rotterdam hackathon and aren't maintained on a regular basis. If you have any contribution, feel free to 65 | file a pull request, or develop it further in your own fork. -------------------------------------------------------------------------------- /versiondebt-shared/src/main/java/com/portofrotterdam/versiondebt/Versiondebts.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamAsAttribute; 5 | import com.thoughtworks.xstream.annotations.XStreamImplicit; 6 | 7 | import java.io.Serializable; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Date; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | /** 16 | * Represents the following XML fragment: 17 | *

18 | *

 19 |  * 		
 20 |  * 			
 21 |  * 			
 22 |  * 				3.5.4-Final
 23 |  * 				21-Jul-2010 22:44
 24 |  * 			
 25 |  * 			
 26 |  * 				4.3.5-Final
 27 |  * 		 		02-Apr-2014 14:52
 28 |  * 			
 29 |  * 		
 30 |  * 
31 | */ 32 | @XStreamAlias("versiondebtItems") 33 | public class Versiondebts implements Serializable { 34 | 35 | @XStreamImplicit 36 | private final Set versiondebtItems = new HashSet<>(); 37 | 38 | public void addVersiondebtItem(final VersiondebtItem item) { 39 | versiondebtItems.add(item); 40 | } 41 | 42 | public void addAll(List items) { 43 | versiondebtItems.addAll(items); 44 | } 45 | 46 | public List getVersiondebtItems() { 47 | if (versiondebtItems != null) { 48 | return Collections.unmodifiableList(new ArrayList<>(versiondebtItems)); 49 | } else { 50 | return Collections.emptyList(); 51 | } 52 | } 53 | 54 | @XStreamAlias("versiondebtItem") 55 | public static class VersiondebtItem { 56 | 57 | @XStreamAsAttribute 58 | private String groupId; 59 | 60 | @XStreamAsAttribute 61 | private String artifactId; 62 | 63 | public VersiondebtItem() { 64 | } 65 | 66 | public VersiondebtItem(final String groupId, final String artifactId) { 67 | this.groupId = groupId; 68 | this.artifactId = artifactId; 69 | } 70 | 71 | private Version usedVersion; 72 | 73 | private Version latestVersion; 74 | 75 | public String getGroupId() { 76 | return groupId; 77 | } 78 | 79 | public String getArtifactId() { 80 | return artifactId; 81 | } 82 | 83 | public void setUsedVersion(final Version usedVersion) { 84 | this.usedVersion = usedVersion; 85 | } 86 | 87 | public Version getUsedVersion() { 88 | return usedVersion; 89 | } 90 | 91 | public void setLatestVersion(final Version latestVersion) { 92 | this.latestVersion = latestVersion; 93 | } 94 | 95 | public Version getLatestVersion() { 96 | return latestVersion; 97 | } 98 | 99 | @XStreamAlias("version") 100 | public static class Version { 101 | 102 | private String name; 103 | 104 | private Date timestamp; 105 | 106 | public Version() { 107 | } 108 | 109 | public Version(final String name, final Date timestamp) { 110 | this.name = name; 111 | this.timestamp = timestamp; 112 | } 113 | 114 | public String getName() { 115 | return name; 116 | } 117 | 118 | public Date getTimestamp() { 119 | return timestamp; 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | com.portofrotterdam 6 | versiondebt 7 | pom 8 | 1.0.0-SNAPSHOT 9 | versiondebt plugin 10 | 11 | https://github.com/PortOfRotterdam/versiondebt-plugin 12 | 13 | 14 | Port of Rotterdam 15 | 16 | 17 | 18 | versiondebt-maven-plugin 19 | versiondebt-sonar-plugin 20 | versiondebt-shared 21 | 22 | 23 | 24 | 25 | 26 | 27 | maven-plugin-plugin 28 | 3.4 29 | 30 | versiondebt 31 | 32 | 33 | 34 | org.sonarsource.sonar-packaging-maven-plugin 35 | sonar-packaging-maven-plugin 36 | 1.17 37 | true 38 | 39 | com.portofrotterdam.versiondebt.VersiondebtPlugin 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-compiler-plugin 45 | 2.0.2 46 | 47 | 1.8 48 | 1.8 49 | UTF-8 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | com.portofrotterdam 60 | versiondebt-shared 61 | ${project.version} 62 | 63 | 64 | org.apache.maven 65 | maven-project 66 | 2.2.1 67 | 68 | 69 | org.apache.maven 70 | maven-plugin-api 71 | 3.0 72 | 73 | 74 | org.apache.maven.plugin-tools 75 | maven-plugin-annotations 76 | 3.2 77 | 78 | 79 | org.apache.httpcomponents 80 | httpclient 81 | 4.3.3 82 | 83 | 84 | org.sonarsource.sonarqube 85 | sonar-plugin-api 86 | 5.6 87 | 88 | 89 | com.thoughtworks.xstream 90 | xstream 91 | 1.4.7 92 | 93 | 94 | joda-time 95 | joda-time 96 | 1.6.2 97 | 98 | 99 | junit 100 | junit 101 | 4.11 102 | test 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /versiondebt-sonar-plugin/src/main/java/com/portofrotterdam/versiondebt/VersiondebtSensor.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import org.sonar.api.batch.fs.FileSystem; 4 | import org.sonar.api.batch.sensor.Sensor; 5 | import org.sonar.api.batch.sensor.SensorContext; 6 | import org.sonar.api.batch.sensor.SensorDescriptor; 7 | import org.sonar.api.config.Settings; 8 | import org.sonar.api.scan.filesystem.PathResolver; 9 | import org.sonar.api.utils.log.Logger; 10 | import org.sonar.api.utils.log.Loggers; 11 | 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.util.Date; 16 | 17 | public class VersiondebtSensor implements Sensor { 18 | 19 | private static final Logger LOGGER = Loggers.get(VersiondebtSensor.class); 20 | private Settings settings; 21 | private PathResolver pathResolver; 22 | private FileSystem fileSystem; 23 | 24 | public VersiondebtSensor(final Settings settings, final PathResolver pathResolver, final FileSystem fileSystem) { 25 | this.settings = settings; 26 | this.pathResolver = pathResolver; 27 | this.fileSystem = fileSystem; 28 | } 29 | 30 | @Override 31 | public void describe(SensorDescriptor descriptor) { 32 | descriptor.name("versiondebt sensor"); 33 | } 34 | 35 | @Override 36 | public void execute(SensorContext context) { 37 | String path = settings.getString(VersiondebtPlugin.VERSIONDEBT_REPORT_PATH); 38 | File report = pathResolver.relativeFile(fileSystem.baseDir(), path); 39 | if (report.exists() && report.isFile()) { 40 | LOGGER.info("Versiondebt report found {}", report); 41 | parseVersiondebtXML(report, context); 42 | } else { 43 | LOGGER.warn("Versiondebt report not found at {}", report); 44 | } 45 | } 46 | 47 | private void parseVersiondebtXML(final File file, final SensorContext context) { 48 | try { 49 | final Versiondebts versiondebts = VersiondebtsFactory.newInstance().fromXML(new FileInputStream(file)); 50 | LOGGER.info("Amount of non-up to date versions '{}'", versiondebts.getVersiondebtItems().size()); 51 | context.newMeasure() 52 | .on(context.module()) 53 | .forMetric(VersiondebtMetrics.DEPENDENCY_AMOUNT) 54 | .withValue(getOutdatedVersionCount(versiondebts)) 55 | .save(); 56 | 57 | final long duration = calculateDuration(versiondebts); 58 | final long days = (duration / (1000*60*60*24)); 59 | final String durationDateString = PrettyFormatter.formatMillisToYearsDaysHours(duration); 60 | LOGGER.info("Duration '{}'", durationDateString); 61 | context.newMeasure() 62 | .on(context.module()) 63 | .forMetric(VersiondebtMetrics.TOTAL_DEPENDENCY_DEBT_DAYS) 64 | .withValue((int) days) 65 | .save(); 66 | 67 | context.newMeasure() 68 | .on(context.module()) 69 | .forMetric(VersiondebtMetrics.TOTAL_DEPENDENCY_DEBT_STRING) 70 | .withValue(durationDateString) 71 | .save(); 72 | } 73 | catch (FileNotFoundException e) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | 78 | private long calculateDuration(Versiondebts versiondebts) { 79 | long totalDuration = 0; 80 | for (Versiondebts.VersiondebtItem versiondebtItem : versiondebts.getVersiondebtItems()) { 81 | totalDuration += getOutdatedDuration(versiondebtItem); 82 | } 83 | return totalDuration; 84 | } 85 | 86 | private int getOutdatedVersionCount(final Versiondebts versiondebts) { 87 | int count = 0; 88 | for (Versiondebts.VersiondebtItem versiondebtItem : versiondebts.getVersiondebtItems()) { 89 | if (getOutdatedDuration(versiondebtItem) > 0) { 90 | ++count; 91 | } 92 | } 93 | return count; 94 | } 95 | 96 | private long getOutdatedDuration(final Versiondebts.VersiondebtItem versiondebtItem) { 97 | Date usedVersionTimestamp = versiondebtItem.getUsedVersion().getTimestamp(); 98 | Date latestVersionTimestamp = versiondebtItem.getLatestVersion().getTimestamp(); 99 | if (usedVersionTimestamp == null || latestVersionTimestamp == null) { 100 | return 0; 101 | } 102 | long difference = latestVersionTimestamp.getTime() - usedVersionTimestamp.getTime(); 103 | return difference <= 0 ? 0 : difference; 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return getClass().getSimpleName(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /versiondebt-maven-plugin/src/main/java/com/portofrotterdam/versiondebt/VersiondebtMojo.java: -------------------------------------------------------------------------------- 1 | package com.portofrotterdam.versiondebt; 2 | 3 | import com.portofrotterdam.versiondebt.Versiondebts.VersiondebtItem; 4 | import com.portofrotterdam.versiondebt.Versiondebts.VersiondebtItem.Version; 5 | import org.apache.http.client.utils.DateUtils; 6 | import org.apache.maven.artifact.Artifact; 7 | import org.apache.maven.artifact.repository.ArtifactRepository; 8 | import org.apache.maven.artifact.repository.metadata.ArtifactRepositoryMetadata; 9 | import org.apache.maven.artifact.repository.metadata.Metadata; 10 | import org.apache.maven.artifact.repository.metadata.RepositoryMetadata; 11 | import org.apache.maven.artifact.repository.metadata.RepositoryMetadataManager; 12 | import org.apache.maven.artifact.repository.metadata.RepositoryMetadataResolutionException; 13 | import org.apache.maven.plugin.AbstractMojo; 14 | import org.apache.maven.plugin.MojoExecutionException; 15 | import org.apache.maven.plugin.logging.Log; 16 | import org.apache.maven.plugins.annotations.Component; 17 | import org.apache.maven.plugins.annotations.LifecyclePhase; 18 | import org.apache.maven.plugins.annotations.Mojo; 19 | import org.apache.maven.plugins.annotations.Parameter; 20 | import org.apache.maven.project.MavenProject; 21 | 22 | import java.io.File; 23 | import java.io.FileWriter; 24 | import java.io.IOException; 25 | import java.net.URI; 26 | import java.net.URL; 27 | import java.net.URLConnection; 28 | import java.nio.file.Files; 29 | import java.util.ArrayList; 30 | import java.util.Collections; 31 | import java.util.Date; 32 | import java.util.HashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.Set; 36 | 37 | /** 38 | * 39 | * This is the Maven plugin. 40 | * 41 | * It creates a versiondebt.xml file for each submodule which contains all the version information. 42 | * 43 | * Part of this plugin is heavily based on: 44 | * https://github.com/mojohaus/cobertura-maven-plugin/blob/master/src/main/java/org/codehaus/mojo/cobertura/CoberturaReportMojo.java 45 | * This is used to create an aggregate file containing all the version information (which is used by the Sonar plugin). 46 | * 47 | * Possible improvements: 48 | * - Add caching? Checking the latest version is a bit slow and it might be possible to cache this for a day for example? 49 | * 50 | */ 51 | @Mojo(name = "generate", defaultPhase = LifecyclePhase.COMPILE, requiresProject = true, threadSafe = true) 52 | public class VersiondebtMojo extends AbstractMojo { 53 | 54 | /** 55 | *

56 | * The Datafile Location. 57 | *

58 | */ 59 | @Parameter(defaultValue= "${project.build.directory}/versiondebt.xml") 60 | private File dataFile; 61 | 62 | /** 63 | * The Maven project. 64 | */ 65 | @Parameter(defaultValue = "${project}") 66 | private MavenProject currentProject; 67 | 68 | /** 69 | * List of maven project of the current build 70 | * 71 | * @parameter expression="${reactorProjects}" 72 | * @required 73 | * @readonly 74 | */ 75 | @Parameter(defaultValue = "${reactorProjects}") 76 | private List reactorProjects; 77 | 78 | private Map> projectChildren; 79 | 80 | private String relDataFileName; 81 | 82 | /** 83 | * Misc Maven components: 84 | */ 85 | @Component 86 | protected RepositoryMetadataManager repositoryMetadataManager; 87 | 88 | @Parameter(defaultValue = "${localRepository}") 89 | protected ArtifactRepository localRepository; 90 | 91 | @Override 92 | public void execute() throws MojoExecutionException { 93 | 94 | // attempt to determine where data files and output dir are 95 | relDataFileName = relativize( currentProject.getBasedir(), dataFile); 96 | if ( relDataFileName == null ) 97 | { 98 | getLog().warn( "Could not determine relative data file name, defaulting to 'versiondebt.xml'" ); 99 | relDataFileName = "versiondebt.xml"; 100 | } 101 | 102 | try { 103 | executeDependencyReport(); 104 | if (canGenerateAggregateReports()) { 105 | // If we are executing the final project, write the aggregate: 106 | executeAggregateReport(); 107 | } 108 | 109 | } catch (IOException ioe) { 110 | throw new MojoExecutionException("Error creating report ", ioe); 111 | } 112 | 113 | } 114 | 115 | private void writeVersiondebtReport(Versiondebts versiondebts, MavenProject project) throws IOException { 116 | 117 | final File outputFile = new File(project.getBasedir(), relDataFileName); 118 | verifyDirectory(outputFile); 119 | 120 | FileWriter fileWriter = null; 121 | try { 122 | fileWriter = new FileWriter(outputFile); 123 | VersiondebtsFactory.newInstance().toXML(versiondebts, fileWriter); 124 | } finally { 125 | if (fileWriter != null) { 126 | try { 127 | fileWriter.close(); 128 | } catch (final IOException e) { 129 | // ignore 130 | } 131 | } 132 | } 133 | } 134 | 135 | private void executeDependencyReport() throws IOException { 136 | 137 | final Versiondebts versiondebts = new Versiondebts(); 138 | 139 | final List repositoryUrls = getRepositoryUrls(); 140 | 141 | final Log log = getLog(); 142 | log.info(""); 143 | log.info("------------------------------------------------------------------------"); 144 | log.info("Analyzing dependency version debt:"); 145 | log.info("------------------------------------------------------------------------"); 146 | log.info(""); 147 | 148 | long totalDebtTime = 0; 149 | long amountOfOutdatedDependencies = 0; 150 | 151 | @SuppressWarnings("unchecked") 152 | final Set artifacts = currentProject.getDependencyArtifacts(); 153 | for (final Artifact artifact : artifacts) { 154 | 155 | final String currentVersion = artifact.getVersion(); 156 | final Date currentVersionDate = extractLastModified(artifact, currentVersion, repositoryUrls); 157 | 158 | final String latestVersion = retrieveLatestReleasedVersion(artifact); 159 | final Date latestVersionDate = extractLastModified(artifact, latestVersion, repositoryUrls); 160 | 161 | log.info("--- Dependency:" + artifact.getGroupId() + " : " + artifact.getArtifactId()+" ---"); 162 | log.info(""); 163 | log.info("\tCurrent version: " + currentVersion+ "\t" + currentVersionDate); 164 | log.info("\tLatest version: " + latestVersion+ "\t" + latestVersionDate); 165 | log.info(""); 166 | 167 | if (latestVersionDate != null && currentVersionDate != null) { 168 | final long artifactDebtTime = latestVersionDate.getTime() - currentVersionDate.getTime(); 169 | if (artifactDebtTime > 0) { 170 | amountOfOutdatedDependencies++; 171 | totalDebtTime += artifactDebtTime; 172 | log.info("\tArtifact debt: " + PrettyFormatter.formatMillisToYearsDaysHours(artifactDebtTime)); 173 | log.info(""); 174 | } else { 175 | log.info("\tArtifact debt: Congratulations! You are up to date!"); 176 | log.info(""); 177 | } 178 | } 179 | 180 | versiondebts.addVersiondebtItem(generateVersiondebtItem(artifact, currentVersion, currentVersionDate, latestVersion, latestVersionDate)); 181 | } 182 | 183 | long averageDebtTime = amountOfOutdatedDependencies > 0 ? totalDebtTime / amountOfOutdatedDependencies : 0; 184 | log.info("------------------------------------------------------------------------"); 185 | log.info("AVERAGE DEBT PER DEP: " + PrettyFormatter.formatMillisToYearsDaysHours(averageDebtTime)); 186 | log.info("TOTAL DEBT: " + PrettyFormatter.formatMillisToYearsDaysHours(totalDebtTime)); 187 | log.info("------------------------------------------------------------------------"); 188 | 189 | log.info(""); 190 | 191 | writeVersiondebtReport(versiondebts, currentProject); 192 | } 193 | 194 | private VersiondebtItem generateVersiondebtItem(final Artifact artifact, final String currentVersionName, final Date currentVersionDate, 195 | final String latestVersionName, final Date latestVersionDate) { 196 | 197 | final VersiondebtItem versiondebt = new VersiondebtItem(artifact.getGroupId(), artifact.getArtifactId()); 198 | versiondebt.setUsedVersion(new Version(currentVersionName, currentVersionDate)); 199 | versiondebt.setLatestVersion(new Version(latestVersionName, latestVersionDate)); 200 | 201 | return versiondebt; 202 | } 203 | 204 | /** 205 | * Method for combining all know URLs to get the best match/latest version 206 | */ 207 | private List getRepositoryUrls() { 208 | final List repositoryUrls = new ArrayList<>(); 209 | // Combine all possible repository urls: 210 | @SuppressWarnings("unchecked") 211 | final List artifactRepositories = currentProject.getRemoteArtifactRepositories(); 212 | for (final ArtifactRepository repository : artifactRepositories) { 213 | repositoryUrls.add(repository.getUrl()); 214 | } 215 | return repositoryUrls; 216 | } 217 | 218 | private Date extractLastModified(final Artifact artifact, final String version, final List repositoryUrls) throws IOException { 219 | for (final String repositoryUrl : repositoryUrls) { 220 | final URL url = new URL(generateUrl(artifact, version, repositoryUrl)); 221 | final URLConnection connection = url.openConnection(); 222 | String lastModified = connection.getHeaderField("Last-Modified"); 223 | if (lastModified != null) { 224 | // Once found, return. 225 | return DateUtils.parseDate(lastModified); 226 | } 227 | } 228 | return null; 229 | } 230 | 231 | private String generateUrl(final Artifact artifact, final String version, final String repositoryUrl) { 232 | 233 | // Build the repository URL to check the Last-Modified header: 234 | String groupIdPart = artifact.getGroupId().replace(".", "/"); 235 | String artifactIdPart = artifact.getArtifactId().replace(".", "/"); 236 | 237 | return repositoryUrl + "/" + groupIdPart + "/" + artifactIdPart + "/" + version 238 | + "/" + artifact.getArtifactId() + "-" + version + ".pom"; 239 | } 240 | 241 | private String retrieveLatestReleasedVersion(final Artifact artifact) { 242 | final RepositoryMetadata metadata = new ArtifactRepositoryMetadata(artifact); 243 | try { 244 | repositoryMetadataManager.resolve(metadata, currentProject.getRemoteArtifactRepositories(), localRepository); 245 | } catch (final RepositoryMetadataResolutionException e1) { 246 | e1.printStackTrace(); 247 | } 248 | final Metadata repoMetadata = metadata.getMetadata(); 249 | if (repoMetadata.getVersioning() != null) { 250 | final String releasedVersion = repoMetadata.getVersioning().getRelease(); 251 | if (releasedVersion != null) { 252 | return releasedVersion; 253 | } 254 | final String latestVersion = repoMetadata.getVersioning().getLatest(); 255 | if (latestVersion != null) { 256 | return latestVersion; 257 | } 258 | } 259 | 260 | return repoMetadata.getVersion(); 261 | } 262 | 263 | /** 264 | * ------------------------------------------------------------------------------------------- 265 | * Stuff below is heavily based on: 266 | * https://github.com/mojohaus/cobertura-maven-plugin/blob/master/src/main/java/org/codehaus/mojo/cobertura/CoberturaReportMojo.java 267 | * 268 | * Needed to generate an aggregate report. 269 | * ------------------------------------------------------------------------------------------- 270 | */ 271 | 272 | /** 273 | * Generates an aggregate report for the given project. 274 | */ 275 | private void executeAggregateReport(MavenProject topProject) throws IOException { 276 | Versiondebts aggregateVersiondebts = new Versiondebts(); 277 | 278 | List children = getAllChildren( topProject ); 279 | 280 | if ( children.isEmpty() ) 281 | { 282 | return; 283 | } 284 | 285 | List partialReportFiles = getOutputFiles(children); 286 | if(partialReportFiles.isEmpty()) { 287 | getLog().info("No reports found"); 288 | return; 289 | } 290 | 291 | getLog().info( "Executing aggregate reports for " + topProject.getName()); 292 | 293 | for ( File partialReportFile : partialReportFiles ) { 294 | try { 295 | // Merge all the versiondebts: 296 | getLog().info("Collecting report: "+partialReportFile.toString()); 297 | 298 | Versiondebts partialDebts = VersiondebtsFactory.newInstance().fromXML(Files.newInputStream(partialReportFile.toPath())); 299 | aggregateVersiondebts.addAll(partialDebts.getVersiondebtItems()); 300 | } catch (IOException ioe) { 301 | getLog().error(ioe); 302 | } 303 | } 304 | 305 | writeVersiondebtReport(aggregateVersiondebts, topProject); 306 | } 307 | 308 | 309 | /** 310 | * Returns a list containing all the recursive, non-pom children of the given project, never null. 311 | */ 312 | private List getAllChildren( MavenProject parentProject ) 313 | { 314 | List children = projectChildren.get( parentProject ); 315 | if(children == null) { 316 | return Collections.emptyList(); 317 | } 318 | 319 | List result = new ArrayList<>(); 320 | for(MavenProject child : children) { 321 | if(isMultiModule(child)) { 322 | result.addAll( getAllChildren( child ) ); 323 | } else { 324 | result.add( child ); 325 | } 326 | } 327 | return result; 328 | } 329 | 330 | /** 331 | * Generates aggregate reports for all multi-module projects. 332 | */ 333 | private void executeAggregateReport() throws IOException { 334 | // Find the top project and create a report for that project: 335 | for ( MavenProject proj : reactorProjects) { 336 | if(!isMultiModule(proj)) { 337 | continue; 338 | } 339 | executeAggregateReport(proj); 340 | } 341 | } 342 | 343 | /** 344 | * Returns whether or not we can generate any aggregate reports at this time. 345 | */ 346 | private boolean canGenerateAggregateReports() 347 | { 348 | // we only generate aggregate reports after the last project runs 349 | if(isLastProject(currentProject, reactorProjects)) { 350 | buildAggregateInfo(); 351 | if (!getOutputFiles(reactorProjects).isEmpty()) { 352 | return true; 353 | } 354 | } 355 | return false; 356 | } 357 | 358 | /** 359 | * Check whether the element is the last element of the list 360 | * 361 | * @param project element to check 362 | * @param mavenProjectList list of maven project 363 | * @return true if project is the last element of mavenProjectList list 364 | */ 365 | private boolean isLastProject( MavenProject project, List mavenProjectList ) 366 | { 367 | return project.equals( mavenProjectList.get( mavenProjectList.size() - 1 ) ); 368 | } 369 | 370 | /** 371 | * Generates various information needed for building aggregate reports. 372 | */ 373 | private void buildAggregateInfo() 374 | { 375 | if(projectChildren != null) { 376 | return; // already did this work 377 | } 378 | 379 | // build parent-child map 380 | projectChildren = new HashMap<>(); 381 | for(MavenProject proj : reactorProjects) { 382 | List depList = projectChildren.get(proj.getParent()); 383 | if(depList == null) { 384 | depList = new ArrayList<>(); 385 | projectChildren.put( proj.getParent(), depList); 386 | } 387 | depList.add(proj); 388 | } 389 | } 390 | 391 | /** 392 | * Returns any existing partial reports from the given list of projects. 393 | */ 394 | private List getOutputFiles( List projects ) 395 | { 396 | List files = new ArrayList<>(); 397 | for ( MavenProject proj : projects ) 398 | { 399 | if ( isMultiModule( proj ) ) 400 | { 401 | continue; 402 | } 403 | File outputFile = new File(proj.getBasedir(), relDataFileName); 404 | if ( outputFile.exists() ) 405 | { 406 | files.add( outputFile ); 407 | } 408 | } 409 | return files; 410 | } 411 | 412 | /** 413 | * Test if the project has pom packaging 414 | * 415 | * @param mavenProject Project to test 416 | * @return True if it has a pom packaging 417 | */ 418 | private boolean isMultiModule( MavenProject mavenProject ) 419 | { 420 | return "pom".equals( mavenProject.getPackaging() ); 421 | } 422 | 423 | 424 | /** 425 | * Attempts to make the given childFile relative to the given parentFile. 426 | */ 427 | private String relativize( File parentFile, File childFile ) 428 | { 429 | try { 430 | URI parentURI = parentFile.getCanonicalFile().toURI().normalize(); 431 | URI childURI = childFile.getCanonicalFile().toURI().normalize(); 432 | 433 | URI relativeURI = parentURI.relativize( childURI ); 434 | if ( relativeURI.isAbsolute() ) { 435 | // child is not relative to parent 436 | return null; 437 | } 438 | String relativePath = relativeURI.getPath(); 439 | if ( File.separatorChar != '/' ) { 440 | relativePath = relativePath.replace( '/', File.separatorChar ); 441 | } 442 | return relativePath; 443 | } 444 | catch ( Exception e ) { 445 | getLog().warn( "Failed relativizing " + childFile + " to " + parentFile, e ); 446 | } 447 | return null; 448 | } 449 | 450 | private void verifyDirectory(final File directory) { 451 | 452 | File currentTarget = directory; 453 | // Move to top directory, not file: 454 | currentTarget = currentTarget.getParentFile(); 455 | 456 | if(!currentTarget.exists()) { 457 | currentTarget.mkdirs(); 458 | } 459 | } 460 | } 461 | --------------------------------------------------------------------------------