From 003a107baff135ea426e74723cb46277510799c1 Mon Sep 17 00:00:00 2001 From: Peter Palaga Date: Wed, 20 Feb 2019 14:34:36 +0100 Subject: [PATCH] Fix #265 Let AbstractDownloadLicensesMojo.licensesConfigFile match by regular expressions --- .../invoker.properties | 1 - .../licenses.xml | 14 - .../pom.xml | 40 --- .../postbuild.groovy | 48 --- .../java/org/codehaus/mojo/HelloWorld.java | 15 - .../invoker.properties | 2 +- .../licenses-pre-1.18.expected.xml | 39 ++ .../licenses-since-1.18.expected.xml | 39 ++ src/it/download-licenses-configured/pom.xml | 31 ++ .../postbuild.groovy | 66 ++-- .../src/license/licenses-config-pre-1.18.xml | 29 ++ .../license/licenses-config-since-1.18.xml | 41 +++ .../src/license/licenses.xml | 14 - .../licenses-errors.expected.xml | 30 +- src/it/download-licenses-force/pom.xml | 5 + .../download-licenses-force/postbuild.groovy | 2 +- .../license/AbstractDownloadLicensesMojo.java | 276 ++++++++------ .../AggregateDownloadLicensesMojo.java | 13 + .../mojo/license/DownloadLicensesMojo.java | 13 + .../license/download/LicenseMatchers.java | 339 ++++++++++++++++++ .../download/LicenseSummaryReader.java | 57 ++- .../download/LicenseSummaryWriter.java | 96 ++++- .../mojo/license/download/ProjectLicense.java | 72 +++- .../license/download/ProjectLicenseInfo.java | 81 +++++ .../examples/example-download-licenses.apt.vm | 6 +- .../license/download/LicenseMatchersTest.java | 160 +++++++++ .../license/download/LicenseSummaryTest.java | 10 + 27 files changed, 1240 insertions(+), 299 deletions(-) delete mode 100644 src/it/download-licenses-configured-alt-location/invoker.properties delete mode 100644 src/it/download-licenses-configured-alt-location/licenses.xml delete mode 100644 src/it/download-licenses-configured-alt-location/pom.xml delete mode 100644 src/it/download-licenses-configured-alt-location/postbuild.groovy delete mode 100644 src/it/download-licenses-configured-alt-location/src/main/java/org/codehaus/mojo/HelloWorld.java create mode 100644 src/it/download-licenses-configured/licenses-pre-1.18.expected.xml create mode 100644 src/it/download-licenses-configured/licenses-since-1.18.expected.xml create mode 100644 src/it/download-licenses-configured/src/license/licenses-config-pre-1.18.xml create mode 100644 src/it/download-licenses-configured/src/license/licenses-config-since-1.18.xml delete mode 100644 src/it/download-licenses-configured/src/license/licenses.xml create mode 100644 src/main/java/org/codehaus/mojo/license/download/LicenseMatchers.java create mode 100644 src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java diff --git a/src/it/download-licenses-configured-alt-location/invoker.properties b/src/it/download-licenses-configured-alt-location/invoker.properties deleted file mode 100644 index be80c31ba..000000000 --- a/src/it/download-licenses-configured-alt-location/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals = -DlicensesConfigFile=./licenses.xml -DlicensesOutputDirectory=./licenseout -DlicensesOutputFile=licensesout.xml clean license:download-licenses diff --git a/src/it/download-licenses-configured-alt-location/licenses.xml b/src/it/download-licenses-configured-alt-location/licenses.xml deleted file mode 100644 index 0c3d63fa2..000000000 --- a/src/it/download-licenses-configured-alt-location/licenses.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - commons-logging - commons-logging - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - \ No newline at end of file diff --git a/src/it/download-licenses-configured-alt-location/pom.xml b/src/it/download-licenses-configured-alt-location/pom.xml deleted file mode 100644 index 3eecf1a10..000000000 --- a/src/it/download-licenses-configured-alt-location/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - 4.0.0 - - org.codehaus.mojo.license - it-test - 1.0-SNAPSHOT - Integration Test - http://maven.apache.org - - Check default execution. - - - - - commons-logging - commons-logging - 1.0 - - - - - - - org.codehaus.mojo - license-maven-plugin - @pom.version@ - - - - download-licenses - - - - - - - diff --git a/src/it/download-licenses-configured-alt-location/postbuild.groovy b/src/it/download-licenses-configured-alt-location/postbuild.groovy deleted file mode 100644 index f5da10fab..000000000 --- a/src/it/download-licenses-configured-alt-location/postbuild.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * #%L - * License Maven Plugin - * %% - * Copyright (C) 2008 - 2011 CodeLutin, Codehaus, Tony Chemit, Tony chemit - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Lesser Public License for more details. - * - * You should have received a copy of the GNU General Lesser Public - * License along with this program. If not, see - * . - * #L% - */ -def assertExistsFile(file) -{ - if ( !file.exists() || file.isDirectory() ) - { - println(file.getAbsolutePath() + " file is missing or a directory.") - assert false - } - assert true -} - -def assertContains(file, content, expected) -{ - if ( !content.contains(expected) ) - { - println(expected + " was not found in file [" + file + "]\n :" + content) - assert false - } - assert true -} - -file = new File(basedir, 'licenseout/apache license 2.0 - license-2.0.txt'); -assertExistsFile(file); - -file = new File(basedir, 'licensesout.xml'); -assertExistsFile(file); - -return true; diff --git a/src/it/download-licenses-configured-alt-location/src/main/java/org/codehaus/mojo/HelloWorld.java b/src/it/download-licenses-configured-alt-location/src/main/java/org/codehaus/mojo/HelloWorld.java deleted file mode 100644 index 16aaa7fed..000000000 --- a/src/it/download-licenses-configured-alt-location/src/main/java/org/codehaus/mojo/HelloWorld.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.codehaus.mojo; - -public class HelloWorld -{ - - /** - * @param args - */ - public static void main( String[] args ) - { - // TODO Auto-generated method stub - - } - -} diff --git a/src/it/download-licenses-configured/invoker.properties b/src/it/download-licenses-configured/invoker.properties index 9131f8e3b..2bdc012db 100644 --- a/src/it/download-licenses-configured/invoker.properties +++ b/src/it/download-licenses-configured/invoker.properties @@ -1 +1 @@ -invoker.goals = clean license:download-licenses +invoker.goals = clean validate diff --git a/src/it/download-licenses-configured/licenses-pre-1.18.expected.xml b/src/it/download-licenses-configured/licenses-pre-1.18.expected.xml new file mode 100644 index 000000000..cbf34f831 --- /dev/null +++ b/src/it/download-licenses-configured/licenses-pre-1.18.expected.xml @@ -0,0 +1,39 @@ + + + + + aopalliance + aopalliance + 1.0 + + + Public Domain + + + + + asm + asm + 3.1 + + + BSD 3-Clause ASM + https://gitlab.ow2.org/asm/asm/raw/ASM_3_1_MVN/LICENSE.txt + bsd 3-clause asm - license.txt + + + + + commons-logging + commons-logging + 1.0 + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + apache license 2.0 - license-2.0.txt + + + + + diff --git a/src/it/download-licenses-configured/licenses-since-1.18.expected.xml b/src/it/download-licenses-configured/licenses-since-1.18.expected.xml new file mode 100644 index 000000000..cbf34f831 --- /dev/null +++ b/src/it/download-licenses-configured/licenses-since-1.18.expected.xml @@ -0,0 +1,39 @@ + + + + + aopalliance + aopalliance + 1.0 + + + Public Domain + + + + + asm + asm + 3.1 + + + BSD 3-Clause ASM + https://gitlab.ow2.org/asm/asm/raw/ASM_3_1_MVN/LICENSE.txt + bsd 3-clause asm - license.txt + + + + + commons-logging + commons-logging + 1.0 + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + apache license 2.0 - license-2.0.txt + + + + + diff --git a/src/it/download-licenses-configured/pom.xml b/src/it/download-licenses-configured/pom.xml index 3eecf1a10..2b55cf741 100644 --- a/src/it/download-licenses-configured/pom.xml +++ b/src/it/download-licenses-configured/pom.xml @@ -19,6 +19,16 @@ commons-logging 1.0 + + aopalliance + aopalliance + 1.0 + + + asm + asm + 3.1 + @@ -29,9 +39,30 @@ @pom.version@ + pre-1.18 + validate + + download-licenses + + + ${basedir}/src/license/licenses-config-pre-1.18.xml + ${project.build.directory}/pre-1.18/licenses + ${project.build.directory}/pre-1.18/licenses.xml + true + + + + since-1.18 + validate download-licenses + + ${basedir}/src/license/licenses-config-since-1.18.xml + ${project.build.directory}/since-1.18/licenses + ${project.build.directory}/since-1.18/licenses.xml + true + diff --git a/src/it/download-licenses-configured/postbuild.groovy b/src/it/download-licenses-configured/postbuild.groovy index bd268ce7e..8ee453093 100644 --- a/src/it/download-licenses-configured/postbuild.groovy +++ b/src/it/download-licenses-configured/postbuild.groovy @@ -19,30 +19,42 @@ * . * #L% */ -def assertExistsFile(file) -{ - if ( !file.exists() || file.isDirectory() ) - { - println(file.getAbsolutePath() + " file is missing or a directory.") - assert false - } - assert true -} - -def assertContains(file, content, expected) -{ - if ( !content.contains(expected) ) - { - println(expected + " was not found in file [" + file + "]\n :" + content) - assert false - } - assert true -} - -file = new File(basedir, 'target/generated-resources/licenses/apache license 2.0 - license-2.0.txt'); -assertExistsFile(file); - -file = new File(basedir, 'target/generated-resources/licenses.xml'); -assertExistsFile(file); - -return true; + +import java.nio.file.Path; +import java.nio.file.Files; + +final Path basePath = basedir.toPath() + +return { + final String id = 'pre-1.18' + final Path outputBase = basePath.resolve('target/' + id) + + final Path asl2 = outputBase.resolve('licenses/apache license 2.0 - license-2.0.txt') + assert Files.exists(asl2) + assert asl2.text.contains('Version 2.0, January 2004') + + final Path bsdAsm = outputBase.resolve('licenses/bsd 3-clause asm - license.txt') + assert Files.exists(bsdAsm) + assert bsdAsm.text.contains('ASM: a very small and fast Java bytecode manipulation framework') + + final Path expectedLicensesXml = basePath.resolve('licenses-'+ id +'.expected.xml') + final Path licensesXml = outputBase.resolve('licenses.xml') + assert expectedLicensesXml.text.equals(licensesXml.text) + return true +}() && { + final String id = 'since-1.18' + final Path outputBase = basePath.resolve('target/' + id) + + final Path asl2 = outputBase.resolve('licenses/apache license 2.0 - license-2.0.txt') + assert Files.exists(asl2) + assert asl2.text.contains('Version 2.0, January 2004') + + final Path bsdAsm = outputBase.resolve('licenses/bsd 3-clause asm - license.txt') + assert Files.exists(bsdAsm) + assert bsdAsm.text.contains('ASM: a very small and fast Java bytecode manipulation framework') + + final Path expectedLicensesXml = basePath.resolve('licenses-'+ id +'.expected.xml') + final Path licensesXml = outputBase.resolve('licenses.xml') + assert expectedLicensesXml.text.equals(licensesXml.text) + return true +}() \ No newline at end of file diff --git a/src/it/download-licenses-configured/src/license/licenses-config-pre-1.18.xml b/src/it/download-licenses-configured/src/license/licenses-config-pre-1.18.xml new file mode 100644 index 000000000..17e4178df --- /dev/null +++ b/src/it/download-licenses-configured/src/license/licenses-config-pre-1.18.xml @@ -0,0 +1,29 @@ + + + + commons-logging + commons-logging + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + aopalliance + aopalliance + + + + asm + asm + + + BSD 3-Clause ASM + https://gitlab.ow2.org/asm/asm/raw/ASM_3_1_MVN/LICENSE.txt + + + + + \ No newline at end of file diff --git a/src/it/download-licenses-configured/src/license/licenses-config-since-1.18.xml b/src/it/download-licenses-configured/src/license/licenses-config-since-1.18.xml new file mode 100644 index 000000000..ba5e70464 --- /dev/null +++ b/src/it/download-licenses-configured/src/license/licenses-config-since-1.18.xml @@ -0,0 +1,41 @@ + + + + + \Qcommons-logging + \Qcommons-logging + + + + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + \Qaopalliance\E + \Qaopalliance\E + + + \QPublic Domain\E + + + + + + \Qasm\E + \Qasm\E + + + + + + BSD 3-Clause ASM + https://gitlab.ow2.org/asm/asm/raw/ASM_3_1_MVN/LICENSE.txt + + + + + diff --git a/src/it/download-licenses-configured/src/license/licenses.xml b/src/it/download-licenses-configured/src/license/licenses.xml deleted file mode 100644 index 0c3d63fa2..000000000 --- a/src/it/download-licenses-configured/src/license/licenses.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - commons-logging - commons-logging - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - \ No newline at end of file diff --git a/src/it/download-licenses-force/licenses-errors.expected.xml b/src/it/download-licenses-force/licenses-errors.expected.xml index eeae85b50..10ccf4a95 100644 --- a/src/it/download-licenses-force/licenses-errors.expected.xml +++ b/src/it/download-licenses-force/licenses-errors.expected.xml @@ -2,11 +2,33 @@ - groovy - groovy-all - 1.0-jsr-04 + \Qaopalliance\E + \Qaopalliance\E + \Q1.0\E + + + \QPublic Domain\E + + - + + + Public Domain + + + + No URL for license at index 0 in dependency aopalliance:aopalliance + + + + \Qgroovy\E + \Qgroovy-all\E + \Q1.0-jsr-04\E + + + + + No license information available for: groovy:groovy-all diff --git a/src/it/download-licenses-force/pom.xml b/src/it/download-licenses-force/pom.xml index 386466ce8..facf2ebbf 100644 --- a/src/it/download-licenses-force/pom.xml +++ b/src/it/download-licenses-force/pom.xml @@ -47,6 +47,11 @@ + + aopalliance + aopalliance + 1.0 + diff --git a/src/it/download-licenses-force/postbuild.groovy b/src/it/download-licenses-force/postbuild.groovy index db2917e85..7294695c5 100644 --- a/src/it/download-licenses-force/postbuild.groovy +++ b/src/it/download-licenses-force/postbuild.groovy @@ -44,4 +44,4 @@ assert expectedLicensesErrorsXml.text.equals(licensesErrorsXml.text) final Path log = basePath.resolve('build.log') assert Files.exists(log) -assert log.text.contains('There were 1 download errors - check ') +assert log.text.contains('There were 2 download errors - check ') diff --git a/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java index 1db78e16b..7f1904e10 100644 --- a/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java @@ -46,13 +46,14 @@ import org.codehaus.mojo.license.download.ProjectLicense; import org.codehaus.mojo.license.download.ProjectLicenseInfo; import org.codehaus.mojo.license.download.LicenseDownloader.LicenseDownloadResult; +import org.codehaus.mojo.license.download.LicenseMatchers; import org.codehaus.mojo.license.utils.FileUtil; import org.codehaus.mojo.license.utils.MojoHelper; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -61,8 +62,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -100,13 +101,95 @@ public abstract class AbstractDownloadLicensesMojo @Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true ) private List remoteRepositories; + // CHECKSTYLE_OFF: LineLength /** - * Input file containing a mapping between each dependency and it's license information. + * A file containing the license data (most notably license names and license URLs) missing in + * {@code pom.xml} files of the dependencies. + *

+ * Note that since 1.18, if you set {@link #errorRemedy} to {@code xmlOutput} the format of + * {@link #licensesErrorsFile} is the same as the one of {@link #licensesConfigFile}. So you can use + * {@link #licensesErrorsFile} as a base for {@link #licensesConfigFile}. + *

+ * Since 1.18, the format of the file is as follows: + *

+     * {@code
+     * 
+     *   
+     *     
+     *       \Qaopalliance\E
+     *       \Qaopalliance\E
+     *       
+     *         
+     *           \QPublic Domain\E
+     *         
+     *       
+     *       
+     *                                   
+     *                                   
+     *     
+     *     
+     *         \Qasm\E
+     *       \Qasm\E
+     *       
+     *         
+     *       
+     *       
+     *       
+     *         
+     *           BSD 3-Clause ASM
+     *           https://gitlab.ow2.org/asm/asm/raw/ASM_3_1_MVN/LICENSE.txt
+     *         
+     *       
+     *     
+     *     
+     *       \Qca.uhn.hapi\E
+     *       .*
+     *       
+     *         
+     *         
+     *           \QHAPI is dual licensed (MPL, GPL)\E
+     *           \QHAPI is dual licensed under both the Mozilla Public License and the GNU General Public License.\E\s+\QWhat this means is that you may choose to use HAPI under the terms of either license.\E\s+\QYou are both permitted and encouraged to use HAPI, royalty-free, within your applications,\E\s+\Qwhether they are free/open-source or commercial/closed-source, provided you abide by the\E\s+\Qterms of one of the licenses below.\E\s+\QYou are under no obligations to inform the HAPI project about what you are doing with\E\s+\QHAPI, but we would love to hear about it anyway!\E
+     *         
+     *         
+     *           \QMozilla Public License 1.1\E
+     *           \Qhttp://www.mozilla.org/MPL/MPL-1.1.txt\E
+     *           \Qmozilla public license 1.1 - index.0c5913925d40.txt\E
+     *         
+     *         
+     *           \QGNU General Public License\E
+     *           \Qhttp://www.gnu.org/licenses/gpl.txt\E
+     *           \Qgnu general public license - gpl.txt\E
+     *         
+     *       
+     *       
+     *     
+     *   
+     * 
+     * }
+     * 
+ *

+ * Before 1.18 the format was the same as the one of {@link #licensesOutputFile} and the groupIds and artifactIds + * were matched as plain text rather than regular expressions. No other elements (incl. versions) were matched at + * all. Since 1.18 the backwards compatibility is achieved by falling back to plain text matching of groupIds + * and artifactIds if the given {@code } does not contain the {@code } element. + *

+ * Relationship to other parameters: + *

    + *
  • License names and license URLs {@link #licensesConfigFile} is applied before + * {@link #licenseUrlReplacements}
  • + *
  • {@link #licenseUrlReplacements} are applied before {@link #licenseUrlFileNames}
  • + *
  • {@link #licenseUrlFileNames} have higher precedence than {@code } elements in + * {@link #licensesConfigFile}
  • + *
  • {@link #licenseUrlFileNames} are ignored when {@link #organizeLicensesByDependencies} is {@code true}
  • + *
* * @since 1.0 */ @Parameter( property = "licensesConfigFile", defaultValue = "${project.basedir}/src/license/licenses.xml" ) - private File licensesConfigFile; + protected File licensesConfigFile; + // CHECKSTYLE_ON: LineLength /** * The directory to which the dependency licenses should be written. @@ -115,7 +198,7 @@ public abstract class AbstractDownloadLicensesMojo */ @Parameter( property = "licensesOutputDirectory", defaultValue = "${project.build.directory}/generated-resources/licenses" ) - private File licensesOutputDirectory; + protected File licensesOutputDirectory; /** * If {@code true}, the mojo will delete all files from {@link #licensesOutputDirectory} and then download them all @@ -247,7 +330,7 @@ public abstract class AbstractDownloadLicensesMojo * @since 1.18 */ @Parameter( property = "license.errorRemedy", defaultValue = "warn" ) - private ErrorRemedy errorRemedy; + protected ErrorRemedy errorRemedy; /** * If {@code true}, all encountered dependency license URLs are downloaded, no matter what is there in @@ -373,11 +456,21 @@ public abstract class AbstractDownloadLicensesMojo * * } * + *

+ * Relationship to other parameters: + *

    + *
  • License names and license URLs {@link #licensesConfigFile} is applied before + * {@link #licenseUrlReplacements}
  • + *
  • {@link #licenseUrlReplacements} are applied before {@link #licenseUrlFileNames}
  • + *
  • {@link #licenseUrlFileNames} have higher precedence than {@code } elements in + * {@link #licensesConfigFile}
  • + *
  • {@link #licenseUrlFileNames} are ignored when {@link #organizeLicensesByDependencies} is {@code true}
  • + *
* * @since 1.17 */ @Parameter - private List licenseUrlReplacements; + protected List licenseUrlReplacements; /** * A map that helps to select local files names for the content downloaded from license URLs. @@ -420,6 +513,8 @@ public abstract class AbstractDownloadLicensesMojo *

* Relationship to other parameters: *

    + *
  • License names and license URLs {@link #licensesConfigFile} is applied before + * {@link #licenseUrlReplacements}
  • *
  • {@link #licenseUrlReplacements} are applied before {@link #licenseUrlFileNames}
  • *
  • {@link #licenseUrlFileNames} have higher precedence than {@code } elements in * {@link #licensesConfigFile}
  • @@ -429,7 +524,7 @@ public abstract class AbstractDownloadLicensesMojo * @since 1.18 */ @Parameter - private Map licenseUrlFileNames; + protected Map licenseUrlFileNames; /** * If {@code true}, {@link #licensesOutputFile} and {@link #licensesErrorsFile} will contain {@code } @@ -509,24 +604,12 @@ public void execute() initDirectories(); - Map configuredDepLicensesMap = new HashMap<>(); + final LicenseMatchers matchers = LicenseMatchers.load( licensesConfigFile ); - // License info from previous build - if ( !forceDownload && licensesOutputFile.exists() ) - { - loadLicenseInfo( configuredDepLicensesMap, licensesOutputFile, true ); - } - - // Manually configured license info, loaded second to override previously loaded info - if ( licensesConfigFile.exists() ) - { - loadLicenseInfo( configuredDepLicensesMap, licensesConfigFile, false ); - } - - Set dependencies = getDependencies(); + final Set dependencies = getDependencies(); // The resulting list of licenses after dependency resolution - List depProjectLicenses = new ArrayList<>(); + final List depProjectLicenses = new ArrayList<>(); try ( LicenseDownloader licenseDownloader = new LicenseDownloader( findActiveProxy() ) ) { @@ -534,17 +617,8 @@ public void execute() { Artifact artifact = project.getArtifact(); getLog().debug( "Checking licenses for project " + artifact ); - String artifactProjectId = getArtifactProjectId( artifact ); - ProjectLicenseInfo depProject; - if ( configuredDepLicensesMap.containsKey( artifactProjectId ) ) - { - depProject = configuredDepLicensesMap.get( artifactProjectId ); - depProject.setVersion( artifact.getVersion() ); - } - else - { - depProject = createDependencyProject( project ); - } + final ProjectLicenseInfo depProject = createDependencyProject( project ); + matchers.replaceMatches( depProject ); depProjectLicenses.add( depProject ); } if ( !offline ) @@ -574,7 +648,7 @@ public void execute() { if ( sortByGroupIdAndArtifactId ) { - depProjectLicenses = sortByGroupIdAndArtifactId( depProjectLicenses ); + sortByGroupIdAndArtifactId( depProjectLicenses ); } if ( licensesOutputFileEncoding == null ) @@ -664,9 +738,8 @@ private static ErrorRemedy getEffectiveErrorRemedy( boolean quiet, ErrorRemedy e } } - private List sortByGroupIdAndArtifactId( List depProjectLicenses ) + private void sortByGroupIdAndArtifactId( List depProjectLicenses ) { - List sorted = new ArrayList<>( depProjectLicenses ); Comparator comparator = new Comparator() { public int compare( ProjectLicenseInfo info1, ProjectLicenseInfo info2 ) @@ -676,8 +749,7 @@ public int compare( ProjectLicenseInfo info1, ProjectLicenseInfo info2 ) + "+" + info2.getArtifactId() ); } }; - Collections.sort( sorted, comparator ); - return sorted; + Collections.sort( depProjectLicenses, comparator ); } // ---------------------------------------------------------------------- @@ -840,71 +912,61 @@ private Proxy findActiveProxy() * @param previouslyDownloaded Whether these licenses were already downloaded * @throws MojoExecutionException if could not load license infos */ - private void loadLicenseInfo( Map configuredDepLicensesMap, File licenseConfigFile, + private Map loadLicenseInfo( File licenseConfigFile, boolean previouslyDownloaded ) throws MojoExecutionException { - FileInputStream fis = null; - try + final Map configuredDepLicensesMap = new LinkedHashMap<>(); + if ( licenseConfigFile.exists() ) { - fis = new FileInputStream( licenseConfigFile ); - List licensesList = LicenseSummaryReader.parseLicenseSummary( fis ); - for ( ProjectLicenseInfo dep : licensesList ) + try ( InputStream fis = Files.newInputStream( licenseConfigFile.toPath() ) ) { - configuredDepLicensesMap.put( dep.getId(), dep ); - if ( previouslyDownloaded ) + final List licensesList = LicenseSummaryReader.parseLicenseSummary( fis ); + for ( ProjectLicenseInfo dep : licensesList ) { - for ( ProjectLicense license : dep.getLicenses() ) + configuredDepLicensesMap.put( dep.getId(), dep ); + if ( previouslyDownloaded ) { - final String url = license.getUrl(); - if ( url != null ) + for ( ProjectLicense license : dep.getLicenses() ) { - final String licenseUrl = rewriteLicenseUrlIfNecessary( url ); - final FileNameEntry fileNameEntry = - getLicenseFileName( dep, licenseUrl, license.getName(), license.getFile() ); - final File licenseFile = fileNameEntry.getFile(); - if ( !forceDownload && licenseFile.exists() ) + final String url = license.getUrl(); + if ( url != null ) { - final String actualSha1 = FileUtil.sha1( licenseFile.toPath() ); - if ( fileNameEntry.getSha1() != null && !actualSha1.equals( fileNameEntry.getSha1() ) ) + final String licenseUrl = rewriteLicenseUrlIfNecessary( url ); + final FileNameEntry fileNameEntry = + getLicenseFileName( dep, licenseUrl, license.getName(), license.getFile() ); + final File licenseFile = fileNameEntry.getFile(); + if ( !forceDownload && licenseFile.exists() ) { - throw new MojoFailureException( "Unexpected sha1 checksum for file '" + final String actualSha1 = FileUtil.sha1( licenseFile.toPath() ); + if ( fileNameEntry.getSha1() != null + && !actualSha1.equals( fileNameEntry.getSha1() ) ) + { + throw new MojoFailureException( "Unexpected sha1 checksum for file '" + licenseFile.getAbsolutePath() + "': '" + actualSha1 + "'; expected '" + fileNameEntry.getSha1() + "'. You may want to (a) re-run the current mojo" + " with -Dlicense.forceDownload=true or (b) change the expected sha1 in" - + " the licenseUrlFileNames entry '" - + fileNameEntry.getFile().getName() + "' or (c) split the entry so that" + + " the licenseUrlFileNames entry '" + fileNameEntry.getFile().getName() + + "' or (c) split the entry so that" + " its URLs return content with different sha1 sums." ); + } + // Save the URL so we don't download it again + cache.put( license.getUrl(), LicenseDownloadResult.success( licenseFile, + actualSha1, + fileNameEntry.isPreferred() ) ); } - // Save the URL so we don't download it again - cache.put( license.getUrl(), LicenseDownloadResult.success( licenseFile, - actualSha1, - fileNameEntry.isPreferred() ) ); } } } } } + catch ( Exception e ) + { + throw new MojoExecutionException( "Unable to parse license summary output file: " + licenseConfigFile, + e ); + } } - catch ( Exception e ) - { - throw new MojoExecutionException( "Unable to parse license summary output file: " + licenseConfigFile, e ); - } - finally - { - FileUtil.tryClose( fis ); - } - } - - /** - * Returns the project ID for the artifact - * - * @param artifact the artifact - * @return groupId:artifactId - */ - private String getArtifactProjectId( Artifact artifact ) - { - return artifact.getGroupId() + ":" + artifact.getArtifactId(); + return configuredDepLicensesMap; } /** @@ -918,10 +980,11 @@ private ProjectLicenseInfo createDependencyProject( MavenProject depMavenProject ProjectLicenseInfo dependencyProject = new ProjectLicenseInfo( depMavenProject.getGroupId(), depMavenProject.getArtifactId(), depMavenProject.getVersion() ); - List licenses = depMavenProject.getLicenses(); - for ( Object license : licenses ) + @SuppressWarnings( "unchecked" ) + List licenses = depMavenProject.getLicenses(); + for ( License license : licenses ) { - dependencyProject.addLicense( new ProjectLicense( (License) license ) ); + dependencyProject.addLicense( new ProjectLicense( license ) ); } return dependencyProject; } @@ -1148,25 +1211,32 @@ private void handleResult( String licenseUrl, LicenseDownloadResult result, Proj private void handleError( ProjectLicenseInfo depProject, String msg ) throws MojoFailureException { - switch ( errorRemedy ) + if ( depProject.isApproved() ) { - case ignore: - /* do nothing */ - break; - case warn: - getLog().warn( msg ); - break; - case failFast: - throw new MojoFailureException( msg ); - case xmlOutput: - getLog().debug( msg ); - depProject.addDownloaderMessage( msg ); - break; - default: - throw new IllegalStateException( "Unexpected value of " + ErrorRemedy.class.getName() + ": " - + errorRemedy ); + getLog().debug( "Supressing manually approved license issue: " + msg ); + } + else + { + switch ( errorRemedy ) + { + case ignore: + /* do nothing */ + break; + case warn: + getLog().warn( msg ); + break; + case failFast: + throw new MojoFailureException( msg ); + case xmlOutput: + getLog().debug( msg ); + depProject.addDownloaderMessage( msg ); + break; + default: + throw new IllegalStateException( "Unexpected value of " + ErrorRemedy.class.getName() + ": " + + errorRemedy ); + } + downloadErrorCount++; } - downloadErrorCount++; } private String rewriteLicenseUrlIfNecessary( final String originalLicenseUrl ) diff --git a/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java index 7df49e607..351eb408b 100644 --- a/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java @@ -36,6 +36,19 @@ /** * Download the license files of all aggregated dependencies of the current project, and generate a summary file * containing a list of all dependencies and their licenses. + *

    + * The license files will be downloaded to {@link AbstractDownloadLicensesMojo#licensesOutputDirectory} to be included + * in the final packaging of the project if desired. The licenses are downloaded from the url field of the dependency + * POM. + *

    + * If the license information (license name and license URL) is missing or otherwise broken in a dependency POM, this + * mojo offers several fallback options: + *

      + *
    • {@link AbstractDownloadLicensesMojo#licensesConfigFile}
    • + *
    • {@link AbstractDownloadLicensesMojo#errorRemedy}
    • + *
    • {@link AbstractDownloadLicensesMojo#licenseUrlReplacements}
    • + *
    • {@link AbstractDownloadLicensesMojo#licenseUrlFileNames}
    • + *
    * * Created on 23/05/16. * diff --git a/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java index b5ad8c859..4edc11b21 100644 --- a/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java @@ -35,6 +35,19 @@ /** * Download the license files of all the current project's dependencies, and generate a summary file containing a list * of all dependencies and their licenses. + *

    + * The license files will be downloaded to {@link AbstractDownloadLicensesMojo#licensesOutputDirectory} to be included + * in the final packaging of the project if desired. The licenses are downloaded from the url field of the dependency + * POM. + *

    + * If the license information (license name and license URL) is missing or otherwise broken in a dependency POM, this + * mojo offers several fallback options: + *

      + *
    • {@link AbstractDownloadLicensesMojo#licensesConfigFile}
    • + *
    • {@link AbstractDownloadLicensesMojo#errorRemedy}
    • + *
    • {@link AbstractDownloadLicensesMojo#licenseUrlReplacements}
    • + *
    • {@link AbstractDownloadLicensesMojo#licenseUrlFileNames}
    • + *
    * * @author Paul Gier * @since 1.0 diff --git a/src/main/java/org/codehaus/mojo/license/download/LicenseMatchers.java b/src/main/java/org/codehaus/mojo/license/download/LicenseMatchers.java new file mode 100644 index 000000000..df003f47c --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/download/LicenseMatchers.java @@ -0,0 +1,339 @@ +package org.codehaus.mojo.license.download; + +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2018 Codehaus + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.maven.plugin.MojoExecutionException; + +/** + * A collection of {@link DependencyMatcher}s to match and replace licenses in {@link ProjectLicenseInfo} instances. + * + * @author Peter Palaga + * @since 1.18 + */ +public class LicenseMatchers +{ + + private static final Pattern MATCH_EMPTY_PATTERN = Pattern.compile( "" ); + private static final Pattern MATCH_ALL_PATTERN = Pattern.compile( ".*" ); + + /** + * @return a new {@link Builder} + */ + public static Builder builder() + { + return new Builder(); + } + + /** + * @param licenseMatchersFile + * @return new {@link LicenseMatchers} configured from the given {@code licenseMatchersFile} + * @throws MojoExecutionException + */ + public static LicenseMatchers load( File licenseMatchersFile ) + throws MojoExecutionException + { + final List matchers = new ArrayList<>(); + try + { + if ( licenseMatchersFile != null && licenseMatchersFile.exists() ) + { + final List replacements = + LicenseSummaryReader.parseLicenseSummary( licenseMatchersFile ); + + for ( ProjectLicenseInfo dependency : replacements ) + { + matchers.add( DependencyMatcher.of( dependency ) ); + } + } + } + catch ( Exception e ) + { + throw new MojoExecutionException( "Could not parse licensesReplacementsFile " + licenseMatchersFile, e ); + } + return new LicenseMatchers( matchers ); + } + + private static boolean match( Pattern pattern, String string ) + { + return string == null ? pattern.matcher( "" ).matches() : pattern.matcher( string ).matches(); + } + + private static Pattern pattern( String string, boolean isPre118Match ) + { + return string == null || string.isEmpty() ? MATCH_EMPTY_PATTERN + : isPre118Match ? Pattern.compile( Pattern.quote( string ) ) + : Pattern.compile( string, Pattern.CASE_INSENSITIVE ); + } + + private final List matchers; + + private LicenseMatchers( List matchers ) + { + super(); + this.matchers = matchers; + } + + /** + * Replace matching licenses in the given {@code dependency} + * + * @param dependency + */ + public void replaceMatches( ProjectLicenseInfo dependency ) + { + for ( DependencyMatcher matcher : matchers ) + { + if ( matcher.matches( dependency ) ) + { + if ( matcher.isApproved() ) + { + /* do nothing */ + } + else + { + dependency.setLicenses( matcher.cloneLicenses() ); + } + dependency.setApproved( true ); + } + } + } + + /** + * A {@link LicenseMatchers} builder + */ + public static class Builder + { + private List matchers = new ArrayList<>(); + + public Builder matcher( DependencyMatcher matcher ) + { + matchers.add( matcher ); + return this; + } + + public LicenseMatchers build() + { + final List ms = matchers; + matchers = null; + return new LicenseMatchers( ms ); + } + } + + /** + * A matcher for dependency nodes in a licenses.xml file + * + * @since 1.18 + */ + static class DependencyMatcher + { + + public static DependencyMatcher of( ProjectLicenseInfo dependency ) + { + final String version = dependency.getVersion(); + final boolean isPre118Match = !dependency.hasMatchLicenses(); + return new DependencyMatcher( pattern( dependency.getGroupId(), isPre118Match ), + pattern( dependency.getArtifactId(), isPre118Match ), + isPre118Match || version == null || version.isEmpty() ? MATCH_ALL_PATTERN + : Pattern.compile( version, Pattern.CASE_INSENSITIVE ), + LicenseListMatcher.of( dependency ), dependency.cloneLicenses(), + dependency.isApproved() ); + } + + private final Pattern artifactId; + + private final Pattern groupId; + + private final LicenseListMatcher licenseListMatcher; + + private final boolean approved; + private final List licenses; + + private final Pattern version; + + DependencyMatcher( Pattern groupId, Pattern artifactId, Pattern version, LicenseListMatcher licenseListMatcher, + List licenses, boolean approved ) + { + super(); + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.licenseListMatcher = licenseListMatcher; + this.licenses = licenses; + this.approved = approved; + } + + public List getLicenseMatchers() + { + return null; + } + + /** + * @return a deep clone of {@link #licenses} + */ + public List cloneLicenses() + { + try + { + final ArrayList result = new ArrayList<>( licenses != null ? licenses.size() : 0 ); + if ( licenses != null ) + { + for ( ProjectLicense license : licenses ) + { + result.add( license.clone() ); + } + } + return result; + } + catch ( CloneNotSupportedException e ) + { + throw new RuntimeException( e ); + } + } + + public boolean matches( ProjectLicenseInfo dependency ) + { + return match( groupId, dependency.getGroupId() ) && match( artifactId, dependency.getArtifactId() ) + && match( version, dependency.getVersion() ) && licenseListMatcher.matches( dependency.getLicenses() ); + } + + public boolean isApproved() + { + return approved; + } + + } + + /** + * A matcher for lists of {@link ProjectLicense}s. + * + * @since 1.18 + */ + static class LicenseListMatcher + { + private final List licenseMatchers; + + private static final LicenseListMatcher MATCHES_ALL_LICENSE_LIST_MATCHER = new LicenseListMatcher( null ) + { + @Override + public boolean matches( List licenses ) + { + return true; + } + }; + + public LicenseListMatcher( List licenseMatchers ) + { + this.licenseMatchers = licenseMatchers; + } + + public boolean matches( List licenses ) + { + final int licsSize = licenses == null ? 0 : licenses.size(); + if ( licenseMatchers.size() != licsSize ) + { + return false; + } + final Iterator matchersIt = licenseMatchers.iterator(); + final Iterator licsIt = licenses.iterator(); + while ( matchersIt.hasNext() ) + { + if ( !matchersIt.next().matches( licsIt.next() ) ) + { + return false; + } + } + return true; + } + + public static LicenseListMatcher of( ProjectLicenseInfo dependency ) + { + if ( !dependency.hasMatchLicenses() ) + { + return MATCHES_ALL_LICENSE_LIST_MATCHER; + } + + final List licenseMatchers; + final List rawMatchers = dependency.getMatchLicenses(); + if ( rawMatchers == null || rawMatchers.isEmpty() ) + { + licenseMatchers = Collections.emptyList(); + } + else + { + licenseMatchers = new ArrayList<>(); + for ( ProjectLicense lic : rawMatchers ) + { + licenseMatchers.add( new LicenseMatcher( lic.getName(), lic.getUrl(), lic.getDistribution(), + lic.getComments() ) ); + } + } + return new LicenseListMatcher( licenseMatchers ); + } + } + + /** + * A matcher for single {@link ProjectLicense}s. + * + * @since 1.18 + */ + static class LicenseMatcher + { + + private final Pattern comments; + private final Pattern distribution; + private final Pattern name; + private final Pattern url; + + LicenseMatcher( Pattern name, Pattern url, Pattern distribution, Pattern comments ) + { + super(); + this.name = name; + this.url = url; + this.distribution = distribution; + this.comments = comments; + } + + LicenseMatcher( String name, String url, String distribution, String comments ) + { + super(); + this.name = pattern( name, false ); + this.url = pattern( url, false ); + this.distribution = pattern( distribution, false ); + this.comments = pattern( comments, false ); + } + + public boolean matches( ProjectLicense license ) + { + return match( name, license.getName() ) && match( url, license.getUrl() ) + && match( distribution, license.getDistribution() ) && match( comments, license.getComments() ); + } + + } + +} diff --git a/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryReader.java b/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryReader.java index b1548e642..e057f3891 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryReader.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryReader.java @@ -31,10 +31,16 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; + +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; /** * A LicenseSummaryReader. @@ -46,6 +52,19 @@ public class LicenseSummaryReader { + public static List parseLicenseSummary( File licSummaryFile ) + throws IOException, ParserConfigurationException, SAXException + { + if ( licSummaryFile.exists() ) + { + try ( InputStream in = Files.newInputStream( licSummaryFile.toPath() ) ) + { + return parseLicenseSummary( in ); + } + } + return Collections.emptyList(); + } + /** * Read a component-info.xml from an input stream into a ComponentInfo object. * @@ -106,19 +125,39 @@ else if ( node.getNodeName().equals( "version" ) ) } else if ( node.getNodeName().equals( "licenses" ) ) { - NodeList licensesChildNodes = node.getChildNodes(); - for ( int j = 0; j < licensesChildNodes.getLength(); ++j ) + Map.Entry> entry = parseLicenses( node ); + dependency.setLicenses( entry.getValue() ); + dependency.setApproved( entry.getKey() ); + } + else if ( node.getNodeName().equals( "matchLicenses" ) ) + { + dependency.setHasMatchLicenses( true ); + dependency.setMatchLicenses( parseLicenses( node ).getValue() ); + } + } + return dependency; + } + + private static Map.Entry> parseLicenses( Node node ) + { + final List result = new ArrayList(); + final NodeList licensesChildNodes = node.getChildNodes(); + final Node approvedNode = node.getAttributes().getNamedItem( "approved" ); + boolean approved = Boolean.parseBoolean( approvedNode != null ? approvedNode.getNodeValue() : "false" ); + for ( int j = 0; j < licensesChildNodes.getLength(); ++j ) + { + final Node licensesChildNode = licensesChildNodes.item( j ); + final String nodeName = licensesChildNode.getNodeName(); + if ( nodeName.equals( "license" ) ) + { + if ( approved ) { - Node licensesChildNode = licensesChildNodes.item( j ); - if ( licensesChildNode.getNodeName().equals( "license" ) ) - { - ProjectLicense license = parseLicense( licensesChildNode ); - dependency.addLicense( license ); - } + throw new IllegalStateException( "Cannot combine approved=\"true\" with elements" ); } + result.add( parseLicense( licensesChildNode ) ); } } - return dependency; + return new AbstractMap.SimpleImmutableEntry>( approved, result ); } private static ProjectLicense parseLicense( Node licenseNode ) diff --git a/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java b/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java index 8462ccdd3..6238ab3f9 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java @@ -41,6 +41,8 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A LicenseSummaryWriter. @@ -87,39 +89,69 @@ public static void writeLicenseSummary( List dependencies, F public static Node createDependencyNode( Document doc, ProjectLicenseInfo dep, boolean writeVersions ) { - Node depNode = doc.createElement( "dependency" ); + final List messages = dep.getDownloaderMessages(); + final boolean hasDownloaderMessages = messages != null && !messages.isEmpty(); + + final Node depNode = doc.createElement( "dependency" ); - Node groupIdNode = doc.createElement( "groupId" ); - groupIdNode.appendChild( doc.createTextNode( dep.getGroupId() ) ); + final Node groupIdNode = doc.createElement( "groupId" ); + groupIdNode.appendChild( doc.createTextNode( patternOrText( dep.getGroupId(), hasDownloaderMessages ) ) ); depNode.appendChild( groupIdNode ); - Node artifactIdNode = doc.createElement( "artifactId" ); - artifactIdNode.appendChild( doc.createTextNode( dep.getArtifactId() ) ); + final Node artifactIdNode = doc.createElement( "artifactId" ); + artifactIdNode.appendChild( doc.createTextNode( patternOrText( dep.getArtifactId(), hasDownloaderMessages ) ) ); depNode.appendChild( artifactIdNode ); if ( writeVersions ) { - Node versionNode = doc.createElement( "version" ); - versionNode.appendChild( doc.createTextNode( dep.getVersion() ) ); + final Node versionNode = doc.createElement( "version" ); + versionNode.appendChild( doc.createTextNode( patternOrText( dep.getVersion(), hasDownloaderMessages ) ) ); depNode.appendChild( versionNode ); } + else if ( hasDownloaderMessages ) + { + depNode.appendChild( doc.createComment( " " + dep.getVersion() + " " ) ); + } + + if ( hasDownloaderMessages ) + { + Node matchLicensesNode = doc.createElement( "matchLicenses" ); + if ( dep.getLicenses() == null || dep.getLicenses().size() == 0 ) + { + matchLicensesNode.appendChild( doc.createComment( " Match dependency with no licenses " ) ); + } + else + { + for ( ProjectLicense lic : dep.getLicenses() ) + { + matchLicensesNode.appendChild( createLicenseNode( doc, lic, true ) ); + } + } + depNode.appendChild( matchLicensesNode ); + } + Node licensesNode = doc.createElement( "licenses" ); if ( dep.getLicenses() == null || dep.getLicenses().size() == 0 ) { - licensesNode.appendChild( doc.createComment( "No license information available. " ) ); + final String comment = + hasDownloaderMessages ? " Manually add license elements here: " : " No license information available. "; + licensesNode.appendChild( doc.createComment( comment ) ); } else { + if ( hasDownloaderMessages ) + { + licensesNode.appendChild( doc.createComment( " Manually fix the existing license nodes: " ) ); + } for ( ProjectLicense lic : dep.getLicenses() ) { - licensesNode.appendChild( createLicenseNode( doc, lic ) ); + licensesNode.appendChild( createLicenseNode( doc, lic, false ) ); } } depNode.appendChild( licensesNode ); - final List messages = dep.getDownloaderMessages(); - if ( messages != null && !messages.isEmpty() ) + if ( hasDownloaderMessages ) { final Node downloaderMessagesNode = doc.createElement( "downloaderMessages" ); for ( String msg : messages ) @@ -135,46 +167,76 @@ public static Node createDependencyNode( Document doc, ProjectLicenseInfo dep, b } - public static Node createLicenseNode( Document doc, ProjectLicense lic ) + public static Node createLicenseNode( Document doc, ProjectLicense lic, boolean isMatcher ) { Node licenseNode = doc.createElement( "license" ); if ( lic.getName() != null ) { Node licNameNode = doc.createElement( "name" ); - licNameNode.appendChild( doc.createTextNode( lic.getName() ) ); + licNameNode.appendChild( doc.createTextNode( patternOrText( lic.getName(), isMatcher ) ) ); licenseNode.appendChild( licNameNode ); } if ( lic.getUrl() != null ) { Node licUrlNode = doc.createElement( "url" ); - licUrlNode.appendChild( doc.createTextNode( lic.getUrl() ) ); + licUrlNode.appendChild( doc.createTextNode( patternOrText( lic.getUrl(), isMatcher ) ) ); licenseNode.appendChild( licUrlNode ); } if ( lic.getDistribution() != null ) { Node licDistNode = doc.createElement( "distribution" ); - licDistNode.appendChild( doc.createTextNode( lic.getDistribution() ) ); + licDistNode.appendChild( doc.createTextNode( patternOrText( lic.getDistribution(), isMatcher ) ) ); licenseNode.appendChild( licDistNode ); } if ( lic.getFile() != null ) { Node licFileNode = doc.createElement( "file" ); - licFileNode.appendChild( doc.createTextNode( lic.getFile() ) ); + licFileNode.appendChild( doc.createTextNode( patternOrText( lic.getFile(), isMatcher ) ) ); licenseNode.appendChild( licFileNode ); } if ( lic.getComments() != null ) { Node licCommentsNode = doc.createElement( "comments" ); - licCommentsNode.appendChild( doc.createTextNode( lic.getComments() ) ); + licCommentsNode.appendChild( doc.createTextNode( patternOrText( lic.getComments(), isMatcher ) ) ); licenseNode.appendChild( licCommentsNode ); } return licenseNode; } + private static final Pattern WHITESPACE_PATTERN = Pattern.compile( "\\s{2,}" ); + + static String patternOrText( String value, boolean isMatcher ) + { + if ( value != null && !value.isEmpty() && isMatcher ) + { + final StringBuilder result = new StringBuilder(); + final Matcher m = WHITESPACE_PATTERN.matcher( value ); + int offset = 0; + while ( m.find() ) + { + if ( m.start() > offset ) + { + result.append( Pattern.quote( value.substring( offset, m.start() ) ) ); + } + result.append( "\\s+" ); + offset = m.end(); + } + if ( offset < value.length() ) + { + result.append( Pattern.quote( value.substring( offset ) ) ); + } + return result.toString(); + } + else + { + return value; + } + } + } diff --git a/src/main/java/org/codehaus/mojo/license/download/ProjectLicense.java b/src/main/java/org/codehaus/mojo/license/download/ProjectLicense.java index 8cdb162f9..f9c3e5826 100644 --- a/src/main/java/org/codehaus/mojo/license/download/ProjectLicense.java +++ b/src/main/java/org/codehaus/mojo/license/download/ProjectLicense.java @@ -29,7 +29,7 @@ * * @since 1.17 */ -public class ProjectLicense +public class ProjectLicense implements Cloneable { @@ -226,4 +226,74 @@ public void setFile( String file ) this.file = file; } + @Override + public ProjectLicense clone() + throws CloneNotSupportedException + { + return (ProjectLicense) super.clone(); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ( ( comments == null ) ? 0 : comments.hashCode() ); + result = prime * result + ( ( distribution == null ) ? 0 : distribution.hashCode() ); + result = prime * result + ( ( file == null ) ? 0 : file.hashCode() ); + result = prime * result + ( ( name == null ) ? 0 : name.hashCode() ); + result = prime * result + ( ( url == null ) ? 0 : url.hashCode() ); + return result; + } + + @Override + public boolean equals( Object obj ) + { + // CHECKSTYLE_OFF: NeedBraces + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + ProjectLicense other = (ProjectLicense) obj; + if ( comments == null ) + { + if ( other.comments != null ) + return false; + } + else if ( !comments.equals( other.comments ) ) + return false; + if ( distribution == null ) + { + if ( other.distribution != null ) + return false; + } + else if ( !distribution.equals( other.distribution ) ) + return false; + if ( file == null ) + { + if ( other.file != null ) + return false; + } + else if ( !file.equals( other.file ) ) + return false; + if ( name == null ) + { + if ( other.name != null ) + return false; + } + else if ( !name.equals( other.name ) ) + return false; + if ( url == null ) + { + if ( other.url != null ) + return false; + } + else if ( !url.equals( other.url ) ) + return false; + return true; + // CHECKSTYLE_ON: NeedBraces + } + } diff --git a/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java b/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java index d6a2433af..b4ac186cf 100644 --- a/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java +++ b/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Contains the license information for a single project/dependency @@ -43,8 +44,13 @@ public class ProjectLicenseInfo private List licenses = new ArrayList<>(); + private List matchLicenses = new ArrayList<>(); + private boolean hasMatchLicenses = false; + private List downloaderMessages = new ArrayList<>(); + private boolean approved; + /** * Default constructor. */ @@ -60,6 +66,14 @@ public ProjectLicenseInfo( String groupId, String artifactId, String version ) this.version = version; } + public ProjectLicenseInfo( String groupId, String artifactId, String version, boolean hasMatchLicenses ) + { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.hasMatchLicenses = hasMatchLicenses; + } + public String getGroupId() { return groupId; @@ -105,6 +119,31 @@ public void addLicense( ProjectLicense license ) licenses.add( license ); } + public List getMatchLicenses() + { + return matchLicenses; + } + + public void setMatchLicenses( List matchLicenses ) + { + this.matchLicenses = matchLicenses; + } + + public void addMatchLicense( ProjectLicense license ) + { + matchLicenses.add( license ); + } + + public boolean hasMatchLicenses() + { + return hasMatchLicenses; + } + + public void setHasMatchLicenses( boolean hasMatchLicenses ) + { + this.hasMatchLicenses = hasMatchLicenses; + } + /** * The unique ID for the project * @@ -159,6 +198,14 @@ public boolean equals( Object compareTo ) return false; } + public boolean deepEquals( ProjectLicenseInfo other ) + { + return Objects.equals( groupId, other.groupId ) && Objects.equals( artifactId, other.artifactId ) + && Objects.equals( version, other.version ) && Objects.equals( licenses, other.licenses ) + && Objects.equals( matchLicenses, other.matchLicenses ) + && Objects.equals( downloaderMessages, other.downloaderMessages ); + } + /** * {@inheritDoc} */ @@ -168,4 +215,38 @@ public int hashCode() return getId().hashCode(); } + + /** + * @return a deep clone of {@link #licenses} + */ + public List cloneLicenses() + { + try + { + final ArrayList result = new ArrayList<>( licenses != null ? licenses.size() : 0 ); + if ( licenses != null ) + { + for ( ProjectLicense license : licenses ) + { + result.add( license.clone() ); + } + } + return result; + } + catch ( CloneNotSupportedException e ) + { + throw new RuntimeException( e ); + } + } + + public void setApproved( boolean approved ) + { + this.approved = approved; + } + + public boolean isApproved() + { + return approved; + } + } diff --git a/src/site/apt/examples/example-download-licenses.apt.vm b/src/site/apt/examples/example-download-licenses.apt.vm index a17c16e14..1a3a625d3 100644 --- a/src/site/apt/examples/example-download-licenses.apt.vm +++ b/src/site/apt/examples/example-download-licenses.apt.vm @@ -36,10 +36,8 @@ Download Licenses Examples based on the url field of the dependency POM. After downloading the license files, the plugin will create a summary file in XML format which describes - the license(s) associated with each dependency. Note that subsequent runs of the plugin will attempt - to use the existing summary file if found instead of re-downloading the licenses every build. - If the license summary file is found, only the licenses of new dependencies not in the file will - be downloaded. + the license(s) associated with each dependency. Note that subsequent runs of the plugin will avoid downloading + the licenses unless the local file exists or forceDownload is set to true. * Basic Example diff --git a/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java b/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java new file mode 100644 index 000000000..f0a67d16a --- /dev/null +++ b/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java @@ -0,0 +1,160 @@ +package org.codehaus.mojo.license.download; + +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2018 Codehaus + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.List; + +import org.codehaus.mojo.license.download.LicenseMatchers.DependencyMatcher; +import org.codehaus.mojo.license.download.LicenseMatchers.LicenseMatcher; +import org.junit.Assert; +import org.junit.Test; + +public class LicenseMatchersTest +{ + @Test + public void licenseMatches() + { + final LicenseMatcher lm1 = new LicenseMatcher( "my license", "http://some.com", null, "important comment" ); + final ProjectLicense lic = + new ProjectLicense( "my license", "http://some.com", null, "important comment", null ); + + Assert.assertTrue( lm1.matches( lic ) ); + lic.setName( "other license" ); + Assert.assertFalse( lm1.matches( lic ) ); + + final LicenseMatcher lm2 = new LicenseMatcher( "other.*", "http://some.com", null, "important comment" ); + Assert.assertTrue( lm2.matches( lic ) ); + lic.setUrl( "http://other.org" ); + Assert.assertFalse( lm2.matches( lic ) ); + + final LicenseMatcher lm3 = new LicenseMatcher( "other.*", "http://other\\..*", null, "important comment" ); + Assert.assertTrue( lm3.matches( lic ) ); + lic.setComments( "other comment" ); + Assert.assertFalse( lm3.matches( lic ) ); + + final LicenseMatcher lm4 = new LicenseMatcher( "other.*", "http://other\\..*", null, ".*comment" ); + Assert.assertTrue( lm4.matches( lic ) ); + + + lic.setComments( null ); + lic.setDistribution( null ); + lic.setFile( null ); + lic.setName( null ); + lic.setUrl( null ); + final LicenseMatcher lm5 = new LicenseMatcher( (String) null, (String) null, (String) null, (String) null ); + Assert.assertTrue( lm5.matches( lic ) ); + + lic.setComments( "" ); + lic.setDistribution( "" ); + lic.setFile( "" ); + lic.setName( "" ); + lic.setUrl( "" ); + Assert.assertTrue( lm5.matches( lic ) ); + + } + + @Test + public void replaceMatchesLegacy() + { + final ProjectLicenseInfo dep = new ProjectLicenseInfo( "myGroup", "myArtifact", "1a2.3" ); + final ProjectLicenseInfo pli1 = new ProjectLicenseInfo( "myGroup", "myArtifact", "1.2.3", false ); + final ProjectLicense lic2 = new ProjectLicense("lic2", "http://other.org", null, "other comment", null); + pli1.addLicense( lic2 ); + final DependencyMatcher m1 = DependencyMatcher.of( pli1 ); + Assert.assertTrue( m1.matches( dep ) ); // legacy mode disregards the version + dep.setVersion( "1.2.3" ); + Assert.assertTrue( m1.matches( dep ) ); + + dep.addLicense( new ProjectLicense( "lic1", "http://some.org", null, "comment", null ) ); + Assert.assertTrue( m1.matches( dep ) ); + final LicenseMatchers matchers1 = LicenseMatchers.builder().matcher( m1 ).build(); + matchers1.replaceMatches( dep ); + Assert.assertEquals( 1, dep.getLicenses().size() ); + Assert.assertEquals( lic2, dep.getLicenses().get( 0 ) ); + Assert.assertNotSame( lic2, dep.getLicenses().get( 0 ) ); + } + + @Test + public void replaceMatches() + { + + final ProjectLicenseInfo dep = new ProjectLicenseInfo( "myGroup", "myArtifact", "1.2.3" ); + final DependencyMatcher m0 = + DependencyMatcher.of( new ProjectLicenseInfo( "myGroup", "myArtifact", null, true ) ); + Assert.assertTrue( m0.matches( dep ) ); + + final DependencyMatcher m1 = + DependencyMatcher.of( new ProjectLicenseInfo( "myGroup", "myArtifact", "1\\.2\\.3", true ) ); + Assert.assertTrue( m1.matches( dep ) ); + dep.setGroupId( "otherGroup" ); + Assert.assertFalse( m1.matches( dep ) ); + + final DependencyMatcher m2 = + DependencyMatcher.of( new ProjectLicenseInfo( "other.*", "myArtifact", "1\\.2\\.3", true ) ); + Assert.assertTrue( m2.matches( dep ) ); + dep.setArtifactId( "otherArtifact" ); + Assert.assertFalse( m2.matches( dep ) ); + + final DependencyMatcher m3 = + DependencyMatcher.of( new ProjectLicenseInfo( "other.*", ".*Artifact", "1\\.2\\.3", true ) ); + Assert.assertTrue( m3.matches( dep ) ); + dep.setVersion( "2.2.2" ); + Assert.assertFalse( m3.matches( dep ) ); + + final DependencyMatcher m4 = + DependencyMatcher.of( new ProjectLicenseInfo( "other.*", ".*Artifact", "2\\.2\\..*", true ) ); + Assert.assertTrue( m4.matches( dep ) ); + + final LicenseMatchers matchers1 = LicenseMatchers.builder().matcher( m1 ).build(); + + final List oldLics = dep.cloneLicenses(); + matchers1.replaceMatches( dep ); + Assert.assertEquals( oldLics, dep.getLicenses() ); + + dep.addLicense( new ProjectLicense( "lic1", "http://some.org", null, "comment", null ) ); + Assert.assertFalse( m1.matches( dep ) ); + + final ProjectLicenseInfo dep11 = new ProjectLicenseInfo( "myGroup", "myArtifact", "1.2.3" ); + dep11.addLicense( new ProjectLicense( "lic1", "http://some.org", null, "comment", null ) ); + final List oldLics11 = dep.cloneLicenses(); + final ProjectLicenseInfo pli11 = new ProjectLicenseInfo( "myGroup", "myArtifact", "1\\.2\\.3", true ); + pli11.addMatchLicense( new ProjectLicense( "lic1", "http://some\\.org", null, "comment", null ) ); + pli11.addLicense( new ProjectLicense( "lic2", "http://other.org", null, "other comment", null ) ); + final DependencyMatcher m11 = DependencyMatcher.of( pli11 ); + Assert.assertTrue( m11.matches( dep11 ) ); + + Assert.assertEquals( oldLics11, dep11.getLicenses() ); + + final LicenseMatchers matchers11 = LicenseMatchers.builder().matcher( m11 ).build(); + matchers11.replaceMatches( dep11 ); + + Assert.assertNotEquals( oldLics11, dep11.getLicenses() ); + + final List newLics = dep11.getLicenses(); + Assert.assertEquals( 1, newLics.size() ); + Assert.assertEquals( 1, newLics.size() ); + + } + +} diff --git a/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java b/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java index 262591e74..058e922f2 100644 --- a/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java +++ b/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java @@ -131,4 +131,14 @@ public void testWriteReadLicenseSummary() } } + + @Test + public void patternOrText() + { + Assert.assertEquals( "\\Qsimple\\E", LicenseSummaryWriter.patternOrText( "simple", true ) ); + Assert.assertEquals( "\\Qone two\\E", LicenseSummaryWriter.patternOrText( "one two", true ) ); + Assert.assertEquals( "\\Qone\\E\\s+\\Qtwo\\E", LicenseSummaryWriter.patternOrText( "one two", true ) ); + Assert.assertEquals( "\\Qone\ntwo\\E", LicenseSummaryWriter.patternOrText( "one\ntwo", true ) ); + Assert.assertEquals( "\\Qone\\E\\s+\\Qtwo\\E", LicenseSummaryWriter.patternOrText( "one\n\t\ttwo", true ) ); + } }