From fe0747afb7a9b0e709d1b70b9c183f7b16a6e538 Mon Sep 17 00:00:00 2001 From: JD Date: Sun, 10 Dec 2023 09:56:33 +0100 Subject: [PATCH] Write Excel files with extensive License Information (inc. infos from the JARs) (#527) Co-authored-by: Jan-Hendrik Diederich Co-authored-by: Jan-Hendrik Diederich Co-authored-by: Jan Diederich --- pom.xml | 43 +- .../child1/pom.xml | 56 + .../child2/pom.xml | 64 + .../invoker.properties | 23 + .../pom.xml | 77 ++ .../postbuild.groovy | 86 ++ .../license/AbstractAddThirdPartyMojo.java | 2 +- .../license/AbstractDownloadLicensesMojo.java | 155 ++- .../mojo/license/AbstractLicensesXmlMojo.java | 2 +- .../license/AbstractThirdPartyReportMojo.java | 23 +- .../AggregateDownloadLicensesMojo.java | 15 +- .../license/AggregatorAddThirdPartyMojo.java | 2 +- .../mojo/license/DownloadLicensesMojo.java | 4 +- .../LicensesXmlInsertVersionsMojo.java | 4 +- .../license/api/DefaultThirdPartyHelper.java | 3 +- .../codehaus/mojo/license/download/Cache.java | 22 + .../license/download/LicenseDownloader.java | 78 +- .../download/LicenseSummaryWriter.java | 144 ++- .../license/download/LicensedArtifact.java | 222 +++- .../download/LicensedArtifactResolver.java | 48 +- .../license/download/ProjectLicenseInfo.java | 10 +- .../mojo/license/extended/ExtendedInfo.java | 138 +++ .../mojo/license/extended/InfoFile.java | 95 ++ .../extended/spreadsheet/CalcFileWriter.java | 1029 +++++++++++++++++ .../extended/spreadsheet/ExcelFileWriter.java | 892 ++++++++++++++ .../extended/spreadsheet/SpreadsheetUtil.java | 111 ++ .../mojo/license/model/LicenseMap.java | 7 +- .../mojo/license/utils/StringToList.java | 6 +- .../org/codehaus/mojo/license/licenses.xsd | 331 ++++-- .../license/download/LicenseMatchersTest.java | 6 +- .../license/download/LicenseSummaryTest.java | 133 ++- 31 files changed, 3665 insertions(+), 166 deletions(-) create mode 100644 src/it/aggregate-download-licenses-extended-spreadsheet/child1/pom.xml create mode 100644 src/it/aggregate-download-licenses-extended-spreadsheet/child2/pom.xml create mode 100644 src/it/aggregate-download-licenses-extended-spreadsheet/invoker.properties create mode 100644 src/it/aggregate-download-licenses-extended-spreadsheet/pom.xml create mode 100644 src/it/aggregate-download-licenses-extended-spreadsheet/postbuild.groovy create mode 100644 src/main/java/org/codehaus/mojo/license/extended/ExtendedInfo.java create mode 100644 src/main/java/org/codehaus/mojo/license/extended/InfoFile.java create mode 100644 src/main/java/org/codehaus/mojo/license/extended/spreadsheet/CalcFileWriter.java create mode 100644 src/main/java/org/codehaus/mojo/license/extended/spreadsheet/ExcelFileWriter.java create mode 100644 src/main/java/org/codehaus/mojo/license/extended/spreadsheet/SpreadsheetUtil.java diff --git a/pom.xml b/pom.xml index a5ab531e4..c146bbf24 100644 --- a/pom.xml +++ b/pom.xml @@ -316,6 +316,28 @@ junit test + + + + org.osgi + org.osgi.core + 6.0.0 + + + + + org.apache.poi + poi-ooxml + 5.2.4 + + + + + org.odftoolkit + odfdom-java + + 0.9.0 + @@ -363,7 +385,7 @@ org.codehaus.mojo.signature - java17 + java18 1.0 @@ -492,6 +514,25 @@ ${project.version} + + + + org.apache.poi + poi-ooxml + 5.2.4 + + + + org.odftoolkit + odfdom-java + 0.9.0 + + + commons-io + commons-io + 2.13.0 + + all-integration-test diff --git a/src/it/aggregate-download-licenses-extended-spreadsheet/child1/pom.xml b/src/it/aggregate-download-licenses-extended-spreadsheet/child1/pom.xml new file mode 100644 index 000000000..6ea8c2b92 --- /dev/null +++ b/src/it/aggregate-download-licenses-extended-spreadsheet/child1/pom.xml @@ -0,0 +1,56 @@ + + + + + + 4.0.0 + + + org.codehaus.mojo.license.test + test-aggregate-download-licenses-extended-excel + @project.version@ + + test-aggregate-download-licenses-extended-spreadsheet-child1 + + License Test :: aggregate-download-licenses - Extended Spreadsheet — child 1 + + + + + + commons-logging + commons-logging + 1.2 + + + org.eclipse.jdt + org.eclipse.jdt.core + 3.35.0 + + + + + + diff --git a/src/it/aggregate-download-licenses-extended-spreadsheet/child2/pom.xml b/src/it/aggregate-download-licenses-extended-spreadsheet/child2/pom.xml new file mode 100644 index 000000000..bbe7f6d7e --- /dev/null +++ b/src/it/aggregate-download-licenses-extended-spreadsheet/child2/pom.xml @@ -0,0 +1,64 @@ + + + + + + 4.0.0 + + + org.codehaus.mojo.license.test + test-aggregate-download-licenses-extended-excel + @project.version@ + + test-aggregate-download-licenses-extended-spreadsheet-child2 + + License Test :: aggregate-download-licenses - Extended Spreadsheet — child 2 + + + + + + com.jhlabs + filters + 2.0.235 + + + + + org.sonatype.plexus + plexus-cipher + 1.7 + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + + + + + + diff --git a/src/it/aggregate-download-licenses-extended-spreadsheet/invoker.properties b/src/it/aggregate-download-licenses-extended-spreadsheet/invoker.properties new file mode 100644 index 000000000..1821befda --- /dev/null +++ b/src/it/aggregate-download-licenses-extended-spreadsheet/invoker.properties @@ -0,0 +1,23 @@ +### +# #%L +# License Maven Plugin +# %% +# Copyright (C) 2008 - 2011 CodeLutin, Codehaus, 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% +### +invoker.goals=clean license:aggregate-download-licenses +invoker.failureBehavior=fail-fast diff --git a/src/it/aggregate-download-licenses-extended-spreadsheet/pom.xml b/src/it/aggregate-download-licenses-extended-spreadsheet/pom.xml new file mode 100644 index 000000000..440e75bf5 --- /dev/null +++ b/src/it/aggregate-download-licenses-extended-spreadsheet/pom.xml @@ -0,0 +1,77 @@ + + + + + + 4.0.0 + + org.codehaus.mojo.license.test + test-aggregate-download-licenses-extended-excel + @project.version@ + + + child1 + child2 + + + License Test :: aggregate-download-licenses - Extended Spreadsheet + + pom + + + UTF-8 + true + + + + + + + org.codehaus.mojo + license-maven-plugin + @project.version@ + + true + true + true + + The Apache Software License, Version 2.0|Apache License, Version 2.0|Apache Public License + 2.0 + + + + + + + + + + + org.sonarsource.sonarlint.core + sonarlint-core + 9.2.0.74516 + + + \ No newline at end of file diff --git a/src/it/aggregate-download-licenses-extended-spreadsheet/postbuild.groovy b/src/it/aggregate-download-licenses-extended-spreadsheet/postbuild.groovy new file mode 100644 index 000000000..6a6fe753f --- /dev/null +++ b/src/it/aggregate-download-licenses-extended-spreadsheet/postbuild.groovy @@ -0,0 +1,86 @@ +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2008 - 2011 CodeLutin, Codehaus, 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% + */ + +import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument +import org.odftoolkit.odfdom.doc.table.OdfTable + +import java.util.logging.Level +import java.util.logging.Logger + +import org.apache.poi.ss.usermodel.Cell +import org.apache.poi.ss.usermodel.CellType +import org.apache.poi.ss.usermodel.Row +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.ss.usermodel.WorkbookFactory + +log = Logger.getLogger("test-aggregate-download-licenses-extended-spreadsheet") + +static boolean searchTextInExcel(Sheet sheet, String searchText) { + def log2 = Logger.getLogger("test-aggregate-download-licenses-extended-spreadsheet-search") + + for (Iterator rowIterator = sheet.rowIterator(); rowIterator.hasNext();) { + Row row = rowIterator.next() + for (Iterator cellIterator = row.cellIterator(); cellIterator.hasNext();) { + Cell cell = cellIterator.next() + if (cell.cellType == CellType.STRING || cell.cellType == CellType.BLANK) { + def cellValue = cell.stringCellValue + if (cellValue == searchText) { + return true + } else { + log2.log(Level.FINEST, "Cell Value: {0}", cellValue) + } + } + } + } + return false +} + +// -------------- Excel ---------------------- +excelFile = new File(basedir, 'target/generated-resources/licenses.xlsx') +assert excelFile.exists() +assert excelFile.length() > 100 + +try (InputStream input = new FileInputStream(excelFile)) { + // So it can be easily opened and inspected manually. In a modern IDE it's just a (double-)click in the log output. + log.log(Level.FINE, "Excel export at: {}", excelFile.absolutePath) + Workbook workbook = WorkbookFactory.create(input) + Sheet sheet = workbook.getSheetAt(0) + + assert searchTextInExcel(sheet, "Maven information") + assert searchTextInExcel(sheet, "The Apache Software License, Version 2.0") + assert searchTextInExcel(sheet, "The Apache Software Foundation") +} + +// -------------- Calc ----------------- + +calcFile = new File(basedir, 'target/generated-resources/licenses.ods') +assert calcFile.exists() +assert calcFile.length() > 100 + +try (OdfSpreadsheetDocument spreadsheet = OdfSpreadsheetDocument.loadDocument(calcFile)) { + // So it can be easily opened and inspected manually. In a modern IDE it's just a (double-)click in the log output. + log.log(Level.FINE, "Calc export at: {}", calcFile.absolutePath) + List tableList = spreadsheet.getTableList() + OdfTable table = tableList.get(0) + assert table.getRowCount() >= 3 +} \ No newline at end of file diff --git a/src/main/java/org/codehaus/mojo/license/AbstractAddThirdPartyMojo.java b/src/main/java/org/codehaus/mojo/license/AbstractAddThirdPartyMojo.java index eb9ad7264..3853fc35d 100644 --- a/src/main/java/org/codehaus/mojo/license/AbstractAddThirdPartyMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AbstractAddThirdPartyMojo.java @@ -352,7 +352,7 @@ public abstract class AbstractAddThirdPartyMojo extends AbstractLicenseMojo { /** * A URL prepared either our of {@link #overrideFile} or {@link #overrideUrl} or the default value. * - * @see LicenseMojoUtils#prepareThirdPartyOverrideUrl(URL, File, String, File) + * @see LicenseMojoUtils#prepareThirdPartyOverrideUrl(String, File, String, File) */ protected String resolvedOverrideUrl; diff --git a/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java index c49dad059..ca741add0 100644 --- a/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AbstractDownloadLicensesMojo.java @@ -22,6 +22,9 @@ * #L% */ +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -41,6 +44,7 @@ import java.util.TreeMap; import java.util.regex.Pattern; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.maven.artifact.repository.ArtifactRepository; @@ -62,6 +66,9 @@ import org.codehaus.mojo.license.download.ProjectLicense; import org.codehaus.mojo.license.download.ProjectLicenseInfo; import org.codehaus.mojo.license.download.UrlReplacements; +import org.codehaus.mojo.license.extended.InfoFile; +import org.codehaus.mojo.license.extended.spreadsheet.CalcFileWriter; +import org.codehaus.mojo.license.extended.spreadsheet.ExcelFileWriter; import org.codehaus.mojo.license.spdx.SpdxLicenseList; import org.codehaus.mojo.license.spdx.SpdxLicenseList.Attachments.ContentSanitizer; import org.codehaus.mojo.license.utils.FileUtil; @@ -234,6 +241,32 @@ public abstract class AbstractDownloadLicensesMojo extends AbstractLicensesXmlMo defaultValue = "${project.build.directory}/generated-resources/licenses-errors.xml") private File licensesErrorsFile; + /** + * A file containing dependencies whose licenses could not be downloaded for some reason. The format is similar to + * {@link #licensesExcelOutputFile} but the entries in {@link #licensesExcelErrorFile} have + * {@code } elements attached to them. Those should explain what kind of error happened during + * the processing of the given dependency. + * + * @since 2.4 + */ + @Parameter( + property = "license.licensesExcelErrorFile", + defaultValue = "${project.build.directory}/generated-resources/licenses-errors.xlsx") + private File licensesExcelErrorFile; + + /** + * A file containing dependencies whose licenses could not be downloaded for some reason. The format is similar to + * {@link #licensesCalcOutputFile} but the entries in {@link #licensesCalcErrorFile} have + * {@code } elements attached to them. Those should explain what kind of error happened during + * the processing of the given dependency. + * + * @since 2.4 + */ + @Parameter( + property = "license.licensesCalcErrorFile", + defaultValue = "${project.build.directory}/generated-resources/licenses-errors.ods") + private File licensesCalcErrorFile; + /** * A filter to exclude some scopes. * @@ -660,6 +693,68 @@ public abstract class AbstractDownloadLicensesMojo extends AbstractLicensesXmlMo @Parameter(property = "license.useDefaultContentSanitizers", defaultValue = "false") private boolean useDefaultContentSanitizers; + /** + * Write Microsoft Office Excel file (XLSX) for goal license:aggregate-download-licenses. + * + * @since 2.4 + */ + @Parameter(property = "license.writeExcelFile", defaultValue = "false") + private boolean writeExcelFile; + + /** + * Write LibreOffice Calc file (ODS) for goal license:aggregate-download-licenses. + * + * @since 2.4 + */ + @Parameter(property = "license.writeCalcFile", defaultValue = "false") + private boolean writeCalcFile; + + /** + * To merge licenses in the Excel file. + *

+ * Each entry represents a merge (first license is main license to keep), licenses are separated by {@code |}. + *

+ * Example: + *

+ *

+     * <licenseMerges>
+     * <licenseMerge>The Apache Software License|Version 2.0,Apache License, Version 2.0</licenseMerge>
+     * </licenseMerges>
+     * </pre>
+     *
+     * @since 2.2.1
+     */
+    @Parameter
+    List licenseMerges;
+
+    /**
+     * The Excel output file used if {@link #writeExcelFile} is true,
+     * containing a mapping between each dependency and its license information.
+     * With extended information, if available.
+     *
+     * @see AbstractDownloadLicensesMojo#writeExcelFile
+     * @see AggregateDownloadLicensesMojo#extendedInfo
+     * @since 2.4
+     */
+    @Parameter(
+            property = "license.licensesExcelOutputFile",
+            defaultValue = "${project.build.directory}/generated-resources/licenses.xlsx")
+    protected File licensesExcelOutputFile;
+
+    /**
+     * The Calc output file used if {@link #writeCalcFile} is true,
+     * containing a mapping between each dependency and its license information.
+     * With extended information, if available.
+     *
+     * @see AbstractDownloadLicensesMojo#writeCalcFile
+     * @see AggregateDownloadLicensesMojo#extendedInfo
+     * @since 2.4
+     */
+    @Parameter(
+            property = "license.licensesCalcOutputFile",
+            defaultValue = "${project.build.directory}/generated-resources/licenses.ods")
+    protected File licensesCalcOutputFile;
+
     // ----------------------------------------------------------------------
     // Plexus Components
     // ----------------------------------------------------------------------
@@ -753,7 +848,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
                 contentSanitizers(),
                 getCharset())) {
             for (LicensedArtifact artifact : dependencies.values()) {
-                LOG.debug("Checking licenses for project " + artifact);
+                LOG.debug("Checking licenses for project {}", artifact);
                 final ProjectLicenseInfo depProject = createDependencyProject(artifact);
                 matchers.replaceMatches(depProject);
 
@@ -780,6 +875,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {
                     downloadLicenses(licenseDownloader, depProject, false);
                 }
             }
+
+            filterCopyrightLines(depProjectLicenses);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -790,9 +887,14 @@ public void execute() throws MojoExecutionException, MojoFailureException {
             }
 
             List depProjectLicensesWithErrors = filterErrors(depProjectLicenses);
-            writeLicenseSummary(depProjectLicenses, licensesOutputFile, writeVersions);
-            if (depProjectLicensesWithErrors != null && !depProjectLicensesWithErrors.isEmpty()) {
-                writeLicenseSummary(depProjectLicensesWithErrors, licensesErrorsFile, writeVersions);
+            writeLicenseSummaries(
+                    depProjectLicenses, licensesOutputFile, licensesExcelOutputFile, licensesCalcOutputFile);
+            if (!CollectionUtils.isEmpty(depProjectLicensesWithErrors)) {
+                writeLicenseSummaries(
+                        depProjectLicensesWithErrors,
+                        licensesErrorsFile,
+                        licensesExcelErrorFile,
+                        licensesCalcErrorFile);
             }
 
             removeOrphanFiles(depProjectLicenses);
@@ -819,6 +921,44 @@ public void execute() throws MojoExecutionException, MojoFailureException {
         }
     }
 
+    private void writeLicenseSummaries(
+            List depProjectLicenses, File outputFile, File excelOutputFile, File calcOutputFile)
+            throws ParserConfigurationException, TransformerException, IOException {
+        writeLicenseSummary(depProjectLicenses, outputFile, writeVersions);
+        if (writeExcelFile) {
+            ExcelFileWriter.write(depProjectLicenses, excelOutputFile);
+        }
+        if (writeCalcFile) {
+            CalcFileWriter.write(depProjectLicenses, calcOutputFile);
+        }
+    }
+
+    /**
+     * Removes all extracted copyright lines if the license is an unmodified original license.
+ * So no actual copyright lines are contained in the extracted copyright lines. + * + * @param depProjectLicenses Projects with extracted copyright lines. + */ + private void filterCopyrightLines(List depProjectLicenses) { + for (ProjectLicenseInfo projectLicenseInfo : depProjectLicenses) { + if (projectLicenseInfo.getExtendedInfo() == null + || CollectionUtils.isEmpty( + projectLicenseInfo.getExtendedInfo().getInfoFiles())) { + continue; + } + List infoFiles = projectLicenseInfo.getExtendedInfo().getInfoFiles(); + for (InfoFile infoFile : infoFiles) { + if (cache.hasNormalizedContentChecksum(infoFile.getContentChecksum())) { + LOG.debug( + "Removed extracted copyright lines for {} ({})", + projectLicenseInfo.getExtendedInfo().getName(), + projectLicenseInfo.toGavString()); + infoFile.setExtractedCopyrightLines(null); + } + } + } + } + private UrlReplacements urlReplacements() { UrlReplacements.Builder b = UrlReplacements.builder().useDefaults(useDefaultUrlReplacements); if (licenseUrlReplacements != null) { @@ -907,7 +1047,7 @@ private List filterErrors(List depProjec while (it.hasNext()) { final ProjectLicenseInfo dep = it.next(); final List messages = dep.getDownloaderMessages(); - if (messages != null && !messages.isEmpty()) { + if (CollectionUtils.isNotEmpty(messages)) { it.remove(); result.add(dep); } @@ -1026,7 +1166,10 @@ private Proxy findActiveProxy() throws MojoExecutionException { */ private ProjectLicenseInfo createDependencyProject(LicensedArtifact depMavenProject) throws MojoFailureException { final ProjectLicenseInfo dependencyProject = new ProjectLicenseInfo( - depMavenProject.getGroupId(), depMavenProject.getArtifactId(), depMavenProject.getVersion()); + depMavenProject.getGroupId(), + depMavenProject.getArtifactId(), + depMavenProject.getVersion(), + depMavenProject.getExtendedInfos()); final List licenses = depMavenProject.getLicenses(); for (org.codehaus.mojo.license.download.License license : licenses) { dependencyProject.addLicense(new ProjectLicense( diff --git a/src/main/java/org/codehaus/mojo/license/AbstractLicensesXmlMojo.java b/src/main/java/org/codehaus/mojo/license/AbstractLicensesXmlMojo.java index cd20e2de2..b77b007d4 100644 --- a/src/main/java/org/codehaus/mojo/license/AbstractLicensesXmlMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AbstractLicensesXmlMojo.java @@ -53,7 +53,7 @@ public abstract class AbstractLicensesXmlMojo extends AbstractMojo { private static final Logger LOG = LoggerFactory.getLogger(AbstractLicensesXmlMojo.class); /** - * The output file containing a mapping between each dependency and it's license information. + * The output file containing a mapping between each dependency and its license information. * * @since 1.0 */ diff --git a/src/main/java/org/codehaus/mojo/license/AbstractThirdPartyReportMojo.java b/src/main/java/org/codehaus/mojo/license/AbstractThirdPartyReportMojo.java index 2ee510c01..401ec2f36 100644 --- a/src/main/java/org/codehaus/mojo/license/AbstractThirdPartyReportMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AbstractThirdPartyReportMojo.java @@ -384,21 +384,14 @@ protected void executeReport(Locale locale) throws MavenReportException { try { init(); details = createThirdPartyDetails(); - } catch (IOException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (ThirdPartyToolException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (ProjectBuildingException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (ArtifactNotFoundException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (ArtifactResolutionException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (MojoFailureException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (DependenciesToolException e) { - throw new MavenReportException(e.getMessage(), e); - } catch (MojoExecutionException e) { + } catch (IOException + | ThirdPartyToolException + | ArtifactNotFoundException + | ArtifactResolutionException + | ProjectBuildingException + | MojoFailureException + | DependenciesToolException + | MojoExecutionException e) { throw new MavenReportException(e.getMessage(), e); } diff --git a/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java index 5e860ea29..68ade2d08 100644 --- a/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AggregateDownloadLicensesMojo.java @@ -97,6 +97,17 @@ public class AggregateDownloadLicensesMojo extends AbstractDownloadLicensesMojo @Parameter(property = "reactorProjects", readonly = true, required = true) private List reactorProjects; + /** + * Extract and use non-maven data for license, copyright and vendor information, + * including the plugins' archive file content. + *

+ * This means it will read info from the JAR's MANIFEST.MF file, look for NOTICE.txt and similar files. + * + * @since 2.4 + */ + @Parameter(property = "license.extendedInfo", defaultValue = "false") + private boolean extendedInfo; + // ---------------------------------------------------------------------- // AbstractDownloadLicensesMojo Implementation // ---------------------------------------------------------------------- @@ -120,7 +131,9 @@ protected Map getDependencies() { new ResolvedProjectDependencies(p.getArtifacts(), MojoHelper.getDependencyArtifacts(p)), this, remoteRepositories, - result); + result, + extendedInfo, + licenseMerges); } return result; } diff --git a/src/main/java/org/codehaus/mojo/license/AggregatorAddThirdPartyMojo.java b/src/main/java/org/codehaus/mojo/license/AggregatorAddThirdPartyMojo.java index d24e5c020..f2d691482 100644 --- a/src/main/java/org/codehaus/mojo/license/AggregatorAddThirdPartyMojo.java +++ b/src/main/java/org/codehaus/mojo/license/AggregatorAddThirdPartyMojo.java @@ -255,7 +255,7 @@ protected void doAction() throws Exception { } // ---------------------------------------------------------------------- - // AbstractAddThirdPartyMojo Implementaton + // AbstractAddThirdPartyMojo Implementation // ---------------------------------------------------------------------- /** diff --git a/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java b/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java index 7662adbda..34a059510 100644 --- a/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java +++ b/src/main/java/org/codehaus/mojo/license/DownloadLicensesMojo.java @@ -88,7 +88,9 @@ protected Map getDependencies() { new ResolvedProjectDependencies(project.getArtifacts(), MojoHelper.getDependencyArtifacts(project)), this, remoteRepositories, - result); + result, + false, + licenseMerges); return result; } } diff --git a/src/main/java/org/codehaus/mojo/license/LicensesXmlInsertVersionsMojo.java b/src/main/java/org/codehaus/mojo/license/LicensesXmlInsertVersionsMojo.java index d1b81eff2..fe1fae60d 100644 --- a/src/main/java/org/codehaus/mojo/license/LicensesXmlInsertVersionsMojo.java +++ b/src/main/java/org/codehaus/mojo/license/LicensesXmlInsertVersionsMojo.java @@ -133,7 +133,9 @@ public ArtifactFilters getArtifactFilters() { new ResolvedProjectDependencies(project.getArtifacts(), MojoHelper.getDependencyArtifacts(project)), config, remoteRepositories, - resolvedDeps); + resolvedDeps, + false, + null); final Map resolvedDepsMap = new HashMap<>(resolvedDeps.size()); for (LicensedArtifact dep : resolvedDeps.values()) { resolvedDepsMap.put(dep.getGroupId() + ":" + dep.getArtifactId(), dep); diff --git a/src/main/java/org/codehaus/mojo/license/api/DefaultThirdPartyHelper.java b/src/main/java/org/codehaus/mojo/license/api/DefaultThirdPartyHelper.java index 4915b2608..48e20849c 100644 --- a/src/main/java/org/codehaus/mojo/license/api/DefaultThirdPartyHelper.java +++ b/src/main/java/org/codehaus/mojo/license/api/DefaultThirdPartyHelper.java @@ -45,6 +45,7 @@ import org.apache.maven.project.ProjectBuildingException; import org.codehaus.mojo.license.model.LicenseMap; import org.codehaus.mojo.license.utils.SortedProperties; +import org.codehaus.mojo.license.utils.StringToList; import org.eclipse.aether.repository.RemoteRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -275,7 +276,7 @@ public void mergeLicenses(List licenseMerges, LicenseMap licenseMap) thr for (String merge : licenseMerges) { merge = merge.trim(); - String[] split = merge.split("\\s*\\|\\s*"); + String[] split = merge.split(StringToList.LIST_OF_LICENSES_REG_EX); String mainLicense = split[0]; diff --git a/src/main/java/org/codehaus/mojo/license/download/Cache.java b/src/main/java/org/codehaus/mojo/license/download/Cache.java index 9e92ce8cf..793163bec 100644 --- a/src/main/java/org/codehaus/mojo/license/download/Cache.java +++ b/src/main/java/org/codehaus/mojo/license/download/Cache.java @@ -28,6 +28,8 @@ import java.util.Map.Entry; import org.codehaus.mojo.license.download.LicenseDownloader.LicenseDownloadResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A simple {@link HashMap} based in-memory cache for storing {@link LicenseDownloadResult}s. @@ -36,10 +38,14 @@ * @since 1.18 */ public class Cache { + private static final Logger LOG = LoggerFactory.getLogger(Cache.class); + private final Map urlToFile = new HashMap<>(); private final Map sha1ToFile = new HashMap<>(); + private final Map normalizedContentToFile = new HashMap<>(); + private final boolean enforcingUniqueSha1s; public Cache(boolean enforcingUniqueSha1s) { @@ -56,6 +62,16 @@ public LicenseDownloadResult get(String url) { return urlToFile.get(url); } + /** + * If this cache has a normalized version of a license file. + * + * @param normalizedContentChecksum Normalized file content checksum. + * @return If the cache has a license file with the same normalized content. + */ + public boolean hasNormalizedContentChecksum(String normalizedContentChecksum) { + return normalizedContentToFile.get(normalizedContentChecksum) != null; + } + /** * Binds the given {@code url} to the give {@link LicenseDownloadResult}. If both {@link #enforcingUniqueSha1s} and * {@link LicenseDownloadResult#isSuccess()} are {@code true} and an entry with the given @@ -87,6 +103,12 @@ public void put(String url, LicenseDownloadResult entry) { + "' should belong to licenseUrlFileName having key '" + existingFile.getName() + "' together with URLs '" + sb.toString() + "'"); } + final String normalizedContentChecksum = entry.getNormalizedContentChecksum(); + if (normalizedContentChecksum != null) { + normalizedContentToFile.put(normalizedContentChecksum, entry); + } else { + LOG.warn("Couldn't find normalized content checksum for license download " + entry); + } } urlToFile.put(url, entry); } diff --git a/src/main/java/org/codehaus/mojo/license/download/LicenseDownloader.java b/src/main/java/org/codehaus/mojo/license/download/LicenseDownloader.java index 940348d26..a714c7ed9 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicenseDownloader.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicenseDownloader.java @@ -31,6 +31,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -155,7 +156,7 @@ protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpCont * can be adjusted based on the mime type of the HTTP response. * * @param licenseUrlString the URL - * @param outputFile a hint where to store the license file + * @param fileNameEntry a hint where to store the license file * @return the path to the file where the downloaded license file was stored * @throws IOException * @throws URISyntaxException @@ -164,7 +165,7 @@ protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpCont public LicenseDownloadResult downloadLicense(String licenseUrlString, FileNameEntry fileNameEntry) throws IOException, URISyntaxException, MojoFailureException { final File outputFile = fileNameEntry.getFile(); - if (licenseUrlString == null || licenseUrlString.length() == 0) { + if (licenseUrlString == null || licenseUrlString.isEmpty()) { throw new IllegalArgumentException("Null URL for file " + outputFile.getPath()); } @@ -317,14 +318,25 @@ private LicenseDownloadResult(File file, String sha1, boolean preferredFileName, this.errorMessage = errorMessage; this.sha1 = sha1; this.preferredFileName = preferredFileName; + this.normalizedContentChecksum = LicenseDownloader.calculateFileChecksum(file); } private final File file; private final String errorMessage; + /** + * Checksum to file name. + */ private final String sha1; + /** + * Checksum to "normalized" content of the file. + *
+ * Normalized means: Removal of all newlines - in all formats, lines trimmed, all chars converted to lowercase. + */ + private final String normalizedContentChecksum; + private final boolean preferredFileName; public File getFile() { @@ -335,6 +347,11 @@ public String getErrorMessage() { return errorMessage; } + /** + * Is true when no errorMessage is set. + * + * @return If errorMessage is set. + */ public boolean isSuccess() { return errorMessage == null; } @@ -347,8 +364,65 @@ public String getSha1() { return sha1; } + public String getNormalizedContentChecksum() { + return normalizedContentChecksum; + } + public LicenseDownloadResult withFile(File otherFile) { return new LicenseDownloadResult(otherFile, sha1, preferredFileName, errorMessage); } + + @Override + public String toString() { + return "LicenseDownloadResult{" + + "file=" + file + + ", errorMessage='" + errorMessage + '\'' + + ", sha1='" + sha1 + '\'' + + ", normalizedContentChecksum='" + normalizedContentChecksum + '\'' + + ", preferredFileName=" + preferredFileName + + '}'; + } + } + + private static String calculateFileChecksum(File file) { + if (file == null) { + return null; + } + try { + String contentString = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + return calculateStringChecksum(contentString); + } catch (IOException e) { + LOG.error("Error reading license file and normalizing it ", e); + return null; + } + } + + public static String calculateStringChecksum(String contentString) { + contentString = normalizeString(contentString); + try { + final MessageDigest md = MessageDigest.getInstance("SHA-1"); + return Hex.encodeHexString(md.digest(contentString.getBytes())); + } catch (NoSuchAlgorithmException e) { + LOG.error("Error fetching SHA-1 hashsum generator ", e); + return null; + } + } + + private static String normalizeString(String contentString) { + return contentString + // Windows + .replace("\r\n", " ") + // Classic MacOS + .replace("\r", " ") + // *nix + .replace("\n", " ") + // Set all spaces, tabs, etc. to one space width + .replaceAll("\\s\\s+", " ") + /* License files exist which are completely identical, except that someone changed + to . + */ + .replace("http://", "https://") + // All to lowercase + .toLowerCase(); } } 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 b7a102503..2eef92a9b 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicenseSummaryWriter.java @@ -37,12 +37,22 @@ import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.file.Files; +import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.collections.CollectionUtils; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; import org.codehaus.mojo.license.Eol; +import org.codehaus.mojo.license.extended.ExtendedInfo; +import org.codehaus.mojo.license.extended.InfoFile; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; /** @@ -53,6 +63,11 @@ * @since 1.0 */ public class LicenseSummaryWriter { + + static final String LICENSES_XSD_FILE = "licenses.xsd"; + private static final String LICENSE_FOLDER = "/org/codehaus/mojo/license/"; + static final String LICENSE_PATH = LICENSE_FOLDER + LICENSES_XSD_FILE; + public static void writeLicenseSummary( List dependencies, File outputFile, Charset charset, Eol eol, boolean writeVersions) throws ParserConfigurationException, TransformerException, IOException { @@ -60,7 +75,8 @@ public static void writeLicenseSummary( DocumentBuilder parser = fact.newDocumentBuilder(); Document doc = parser.newDocument(); - Node root = doc.createElement("licenseSummary"); + Element root = doc.createElement("licenseSummary"); + doc.appendChild(root); Node dependenciesNode = doc.createElement("dependencies"); root.appendChild(dependenciesNode); @@ -108,7 +124,7 @@ public static Node createDependencyNode(Document doc, ProjectLicenseInfo dep, bo if (hasDownloaderMessages) { Node matchLicensesNode = doc.createElement("matchLicenses"); - if (dep.getLicenses() == null || dep.getLicenses().size() == 0) { + if (dep.getLicenses() == null || dep.getLicenses().isEmpty()) { matchLicensesNode.appendChild(doc.createComment(" Match dependency with no licenses ")); } else { for (ProjectLicense lic : dep.getLicenses()) { @@ -118,8 +134,10 @@ public static Node createDependencyNode(Document doc, ProjectLicenseInfo dep, bo depNode.appendChild(matchLicensesNode); } + addExtendedInfo(doc, dep, depNode); + Node licensesNode = doc.createElement("licenses"); - if (dep.getLicenses() == null || dep.getLicenses().size() == 0) { + if (CollectionUtils.isEmpty(dep.getLicenses())) { final String comment = hasDownloaderMessages ? " Manually add license elements here: " : " No license information available. "; @@ -147,6 +165,68 @@ public static Node createDependencyNode(Document doc, ProjectLicenseInfo dep, bo return depNode; } + private static void addExtendedInfo(Document doc, ProjectLicenseInfo dep, Node depNode) { + if (dep.getExtendedInfo() != null) { + ExtendedInfo extendedInfo = dep.getExtendedInfo(); + addTextPropertyIfSet(doc, depNode, "name", extendedInfo.getName()); + addTextPropertyIfSet(doc, depNode, "bundleLicense", extendedInfo.getBundleLicense()); + addCdataIfSet(doc, depNode, "bundleVendor", extendedInfo.getBundleVendor()); + appendChildNodesIfSet( + doc, + depNode, + "developers", + extendedInfo.getDevelopers(), + (doc1, developer) -> createDeveloperNode(doc, developer)); + addCdataIfSet(doc, depNode, "implementationVendor", extendedInfo.getImplementationVendor()); + addTextPropertyIfSet(doc, depNode, "inceptionYear", extendedInfo.getInceptionYear()); + appendChildNodesIfSet( + doc, + depNode, + "infoFiles", + extendedInfo.getInfoFiles(), + (doc1, infoFile) -> createInfoFileNode(doc, infoFile)); + if (extendedInfo.getOrganization() != null + && (extendedInfo.getOrganization().getName() != null + || extendedInfo.getOrganization().getUrl() != null)) { + Node organizationNode = doc.createElement("organization"); + final Organization organization = extendedInfo.getOrganization(); + addTextPropertyIfSet(doc, organizationNode, "name", organization.getName()); + addTextPropertyIfSet(doc, organizationNode, "url", organization.getUrl()); + depNode.appendChild(organizationNode); + } + addTextPropertyIfSet( + doc, + depNode, + "scm", + Optional.ofNullable(extendedInfo.getScm()).map(Scm::getUrl).orElse(null)); + addTextPropertyIfSet(doc, depNode, "url", extendedInfo.getUrl()); + } + } + + /** + * Lambda interface for {@link #appendChildNodesIfSet(Document, Node, String, Collection, CreateSubNode)}. + * + * @param Type in collection to add as child node entries. + */ + interface CreateSubNode { + Node createSubNode(Document doc, T t); + } + + private static void appendChildNodesIfSet( + Document doc, + Node parentNode, + String elementName, + Collection collection, + CreateSubNode createSubNode) { + if (!CollectionUtils.isEmpty(collection)) { + Node developersNode = doc.createElement(elementName); + for (T t : collection) { + developersNode.appendChild(createSubNode.createSubNode(doc, t)); + } + parentNode.appendChild(developersNode); + } + } + public static Node createLicenseNode(Document doc, ProjectLicense lic, boolean isMatcher) { Node licenseNode = doc.createElement("license"); @@ -183,6 +263,64 @@ public static Node createLicenseNode(Document doc, ProjectLicense lic, boolean i return licenseNode; } + private static Node createDeveloperNode(Document doc, Developer developer) { + Node developerNode = doc.createElement("developer"); + + addTextPropertyIfSet(doc, developerNode, "id", developer.getId()); + addTextPropertyIfSet(doc, developerNode, "email", developer.getEmail()); + addTextPropertyIfSet(doc, developerNode, "name", developer.getName()); + addTextPropertyIfSet(doc, developerNode, "organization", developer.getOrganization()); + addTextPropertyIfSet(doc, developerNode, "organizationUrl", developer.getOrganizationUrl()); + addTextPropertyIfSet(doc, developerNode, "url", developer.getUrl()); + addTextPropertyIfSet(doc, developerNode, "timezone", developer.getTimezone()); + + return developerNode; + } + + private static Node createInfoFileNode(Document doc, InfoFile infoFile) { + Node infoFileNode = doc.createElement("infoFile"); + + addCdataIfSet(doc, infoFileNode, "content", infoFile.getContent()); + appendChildNodesIfSet( + doc, infoFileNode, "extractedCopyrightLines", infoFile.getExtractedCopyrightLines(), (doc1, line) -> { + Node devNameNode = doc.createElement("line"); + devNameNode.appendChild(doc.createCDATASection(line)); + return devNameNode; + }); + addCdataIfSet(doc, infoFileNode, "fileName", infoFile.getFileName()); + addTextPropertyIfSet(doc, infoFileNode, "type", infoFile.getType().toString()); + + return infoFileNode; + } + + private static void addTextPropertyIfSet(Document doc, Node parentNode, String elementName, String property) { + addPropertyIfSet(doc, parentNode, elementName, property, () -> doc.createTextNode(property)); + } + + private static void addCdataIfSet(Document doc, Node parentNode, String elementName, String property) { + addPropertyIfSet(doc, parentNode, elementName, property, () -> doc.createCDATASection(prepareCdata(property))); + } + + /** + * Fix string to being written as CDATA under windows, also compatible with *nix systems.
+ * See https://bugs.openjdk.java.net/browse/JDK-8133452 + * + * @param property Property to prepare being written as XML CDATA + * @return The properly prepared string. + */ + private static String prepareCdata(String property) { + return property.replace("\r\n", "\n").replace("\f", "\n"); + } + + private static void addPropertyIfSet( + Document doc, Node parentNode, String elementName, String property, Supplier nodeSupplier) { + if (property != null) { + Node devNameNode = doc.createElement(elementName); + devNameNode.appendChild(nodeSupplier.get()); + parentNode.appendChild(devNameNode); + } + } + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s{2,}"); static String patternOrText(String value, boolean isMatcher) { diff --git a/src/main/java/org/codehaus/mojo/license/download/LicensedArtifact.java b/src/main/java/org/codehaus/mojo/license/download/LicensedArtifact.java index 604ec3206..ec6799d5c 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicensedArtifact.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicensedArtifact.java @@ -22,18 +22,45 @@ * #L% */ +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; +import org.codehaus.mojo.license.extended.ExtendedInfo; +import org.codehaus.mojo.license.extended.InfoFile; +import org.codehaus.mojo.license.spdx.SpdxLicenseInfo; +import org.codehaus.mojo.license.spdx.SpdxLicenseList; +import org.osgi.framework.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * @author Peter Palaga + * @author Peter Palaga, Jan-Hendrik Diederich (for the extended information) * @since 1.20 */ public class LicensedArtifact { + private static final Logger LOG = LoggerFactory.getLogger(LicensedArtifact.class); - public static Builder builder(String groupId, String artifactId, String version) { - return new Builder(groupId, artifactId, version); + public static Builder builder(Artifact artifact, boolean useNonMavenData) { + return new Builder(artifact, useNonMavenData); } private final String groupId; @@ -46,14 +73,22 @@ public static Builder builder(String groupId, String artifactId, String version) private final List errorMessages; + private final ExtendedInfo extendedInfos; + LicensedArtifact( - String groupId, String artifactId, String version, List licenses, List errorMessages) { + String groupId, + String artifactId, + String version, + List licenses, + List errorMessages, + ExtendedInfo extendedInfos) { super(); this.groupId = groupId; this.artifactId = artifactId; this.version = version; this.licenses = licenses; this.errorMessages = errorMessages; + this.extendedInfos = extendedInfos; } @Override @@ -110,16 +145,26 @@ public List getErrorMessages() { return errorMessages; } + /** + * Gets the extended information. + * + * @return Extended information. + * @since 2.1.0 + */ + public ExtendedInfo getExtendedInfos() { + return extendedInfos; + } + /** * A {@link LicensedArtifact} builder. */ public static class Builder { - - public Builder(String groupId, String artifactId, String version) { + public Builder(Artifact artifact, boolean useNonMavenData) { super(); - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; + this.groupId = artifact.getGroupId(); + this.artifactId = artifact.getArtifactId(); + this.version = artifact.getVersion(); + this.extendedInfos = useNonMavenData ? extraInfosFromArtifact(artifact) : null; } private final String groupId; @@ -132,6 +177,8 @@ public Builder(String groupId, String artifactId, String version) { private List errorMessages = new ArrayList<>(); + private final ExtendedInfo extendedInfos; + public Builder errorMessage(String errorMessage) { this.errorMessages.add(errorMessage); return this; @@ -147,7 +194,162 @@ public LicensedArtifact build() { licenses = null; final List msgs = Collections.unmodifiableList(errorMessages); errorMessages = null; - return new LicensedArtifact(groupId, artifactId, version, lics, msgs); + return new LicensedArtifact(groupId, artifactId, version, lics, msgs, extendedInfos); + } + + private ExtendedInfo extraInfosFromArtifact(Artifact artifact) { + if (artifact.getFile() == null) { + LOG.error("Artifact " + artifact + " has no valid file set"); + return null; + } + ExtendedInfo result = new ExtendedInfo(); + result.setArtifact(artifact); + try (ZipFile zipFile = new ZipFile(artifact.getFile())) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + final String fileName = zipEntry.getName().toLowerCase(); + if (textFileMatcher(fileName, "notice")) { + // Should match "NOTICE.txt", "NOTICES.txt"..., + // if someone decides to go slightly against convention. + final InfoFile infoFile = buildInfoFile(zipFile, zipEntry, InfoFile.Type.NOTICE); + result.getInfoFiles().add(infoFile); + } else if (textFileMatcher(fileName, "license", "licence")) { + // Match against british and american english writing type of "license" + final InfoFile infoFile = buildInfoFile(zipFile, zipEntry, InfoFile.Type.LICENSE); + result.getInfoFiles().add(infoFile); + } else if (fileMatchesSpdxId(fileName)) { + final InfoFile infoFile = buildInfoFile(zipFile, zipEntry, InfoFile.Type.SPDX_LICENSE); + result.getInfoFiles().add(infoFile); + } else if (fileName.equals("meta-inf/manifest.mf")) { + try (InputStream inputStream = zipFile.getInputStream(zipEntry)) { + Manifest manifest = new Manifest(inputStream); + final Attributes mainAttributes = manifest.getMainAttributes(); + // Fetch Java standard JAR manifest attributes. + final Object implementationVendor = + mainAttributes.get(Attributes.Name.IMPLEMENTATION_VENDOR); + if (implementationVendor instanceof String) { + result.setImplementationVendor((String) implementationVendor); + } + // Fetch OSGI framework JAR manifest attributes. + final String bundleVendor = mainAttributes.getValue(Constants.BUNDLE_VENDOR); + result.setBundleVendor(bundleVendor); + final String bundleLicense = mainAttributes.getValue(Constants.BUNDLE_LICENSE); + result.setBundleLicense(bundleLicense); + } catch (IOException e) { + LOG.warn("Error at reading data from jar manifest", e); + } + } + } + } catch (IOException e) { + LOG.warn("Can't open zip file \"" + artifact.getFile() + "\"", e); + } + return result; + } + + private InfoFile buildInfoFile(ZipFile zipFile, ZipEntry zipEntry, InfoFile.Type type) { + InfoFile infoFile = new InfoFile(); + infoFile.setFileName(zipEntry.getName()); + infoFile.setType(type); + Pair contentWithLines = readZipEntryTextLines(zipFile, zipEntry); + if (contentWithLines != null) { + Set copyrights = scanForCopyrights(contentWithLines.getRight(), "(c)", "copyright"); + if (!CollectionUtils.isEmpty(copyrights)) { + infoFile.getExtractedCopyrightLines().addAll(copyrights); + } + infoFile.setContent(contentWithLines.getLeft()); + } + return infoFile; + } + + private boolean fileMatchesSpdxId(String fileName) { + final SpdxLicenseList spdxList = SpdxLicenseList.getLatest(); + for (Map.Entry entry : + spdxList.getLicenses().entrySet()) { + if (textFileMatcher(fileName, entry.getValue().getLicenseId().toLowerCase())) { + return true; + } + } + return false; + } + + private boolean textFileMatcher(String fileName, String... matchStrings) { + for (String matchString : matchStrings) { + if (fileName.matches(".*" + matchString + ".*\\.txt")) { + return true; + } + } + return false; + } + + /** + * Scans for given copyright matchers in param copyrightMatchers. + * + * @param lines The lines to check + * @param copyrightMatchers Lines containing one of these strings are returned. Arguments must be all lowercase. + * @return The found lines containing copyright claims. + */ + private Set scanForCopyrights(String[] lines, String... copyrightMatchers) { + if (lines == null) { + return null; + } + Set result = new HashSet<>(); + for (String line : lines) { + for (String copyrightMatcher : copyrightMatchers) { + final String trimmedLine = line.trim(); + if (trimmedLine.toLowerCase().contains(copyrightMatcher)) { + result.add(trimmedLine); + } + } + } + return result; + } + + private Pair readZipEntryTextLines(ZipFile zipFile, ZipEntry zipEntry) { + try (InputStream inputStream = zipFile.getInputStream(zipEntry)) { + byte[] content = IOUtils.readFully(inputStream, (int) zipEntry.getSize()); + String contentString = new String(content); + return new ImmutablePair<>(contentString, contentString.split("\\R+")); + } catch (IOException e) { + LOG.warn("Can't read zip file entry " + zipEntry, e); + return null; + } + } + + public void setInceptionYear(String inceptionYear) { + if (extendedInfos != null) { + this.extendedInfos.setInceptionYear(inceptionYear); + } + } + + public void setOrganization(Organization organization) { + if (extendedInfos != null) { + this.extendedInfos.setOrganization(organization); + } + } + + public void setDevelopers(List developers) { + if (extendedInfos != null) { + this.extendedInfos.setDevelopers(developers); + } + } + + public void setUrl(String url) { + if (extendedInfos != null) { + this.extendedInfos.setUrl(url); + } + } + + public void setScm(Scm scm) { + if (extendedInfos != null) { + this.extendedInfos.setScm(scm); + } + } + + public void setName(String name) { + if (extendedInfos != null) { + this.extendedInfos.setName(name); + } } } } diff --git a/src/main/java/org/codehaus/mojo/license/download/LicensedArtifactResolver.java b/src/main/java/org/codehaus/mojo/license/download/LicensedArtifactResolver.java index a23320a55..0b595dc2b 100644 --- a/src/main/java/org/codehaus/mojo/license/download/LicensedArtifactResolver.java +++ b/src/main/java/org/codehaus/mojo/license/download/LicensedArtifactResolver.java @@ -47,6 +47,7 @@ import org.codehaus.mojo.license.api.ResolvedProjectDependencies; import org.codehaus.mojo.license.download.LicensedArtifact.Builder; import org.codehaus.mojo.license.utils.MojoHelper; +import org.codehaus.mojo.license.utils.StringToList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,16 +82,20 @@ public class LicensedArtifactResolver { * For a given {@code project}, obtain the universe of its dependencies after applying transitivity and filtering * rules given in the {@code configuration} object. Result is given in a map where keys are unique artifact id * - * @param configuration the configuration - * @param remoteRepositories remote repositories used to resolv dependencies + * @param artifacts Dependencies + * @param configuration the configuration + * @param remoteRepositories remote repositories used to resolve dependencies + * @param result Map with Key/Value = PluginID/LicensedArtifact + * @param licenseMerges List of license names to merge. * @see MavenProjectDependenciesConfigurator */ public void loadProjectDependencies( ResolvedProjectDependencies artifacts, MavenProjectDependenciesConfigurator configuration, List remoteRepositories, - Map result) { - + Map result, + boolean extendedInfo, + List licenseMerges) { final ArtifactFilters artifactFilters = configuration.getArtifactFilters(); final boolean excludeTransitiveDependencies = configuration.isExcludeTransitiveDependencies(); @@ -117,6 +122,8 @@ public void loadProjectDependencies( .setResolveDependencies(false) .setProcessPlugins(false); + final Map mergedLicenses = buildMergedLicenses(licenseMerges); + for (Artifact artifact : depArtifacts) { excludeArtifacts.put(artifact.getId(), artifact); @@ -148,17 +155,25 @@ public void loadProjectDependencies( LOG.debug("Dependency [{}] already present in the result", id); } else { // build project - final Builder laBuilder = LicensedArtifact.builder( - artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); + final Builder laBuilder = LicensedArtifact.builder(artifact, extendedInfo); try { final MavenProject project = mavenProjectBuilder .build(artifact, true, projectBuildingRequest) .getProject(); + if (extendedInfo) { + laBuilder.setName(project.getName()); + laBuilder.setInceptionYear(project.getInceptionYear()); + laBuilder.setOrganization(project.getOrganization()); + laBuilder.setDevelopers(project.getDevelopers()); + laBuilder.setUrl(project.getUrl()); + laBuilder.setScm(project.getScm()); + } List lics = project.getLicenses(); if (lics != null) { for (org.apache.maven.model.License lic : lics) { + final String mergedLicense = mergedLicenses.getOrDefault(lic.getName(), lic.getName()); laBuilder.license( - new License(lic.getName(), lic.getUrl(), lic.getDistribution(), lic.getComments())); + new License(mergedLicense, lic.getUrl(), lic.getDistribution(), lic.getComments())); } } } catch (ProjectBuildingException e) { @@ -200,4 +215,23 @@ public void loadProjectDependencies( } } // CHECKSTYLE_ON: MethodLength + + private static Map buildMergedLicenses(List licenseMerges) { + final Map mergedLicenses = new HashMap<>(); + if (licenseMerges != null) { + for (String licenseMerge : licenseMerges) { + String[] splited = licenseMerge + .trim() + // Replace newlines + .replace('\n', ' ') + // Replace multiple spaces with one. + .replaceAll(" +", " ") + .split(StringToList.LIST_OF_LICENSES_REG_EX); + for (String split : splited) { + mergedLicenses.put(split, splited[0]); + } + } + } + return mergedLicenses; + } } 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 f6e27fbdb..3ad7ff109 100644 --- a/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java +++ b/src/main/java/org/codehaus/mojo/license/download/ProjectLicenseInfo.java @@ -27,6 +27,7 @@ import java.util.Objects; import org.apache.maven.artifact.Artifact; +import org.codehaus.mojo.license.extended.ExtendedInfo; /** * Contains the license information for a single project/dependency @@ -50,15 +51,18 @@ public class ProjectLicenseInfo { private boolean approved; + private ExtendedInfo extendedInfo; + /** * Default constructor. */ public ProjectLicenseInfo() {} - public ProjectLicenseInfo(String groupId, String artifactId, String version) { + public ProjectLicenseInfo(String groupId, String artifactId, String version, ExtendedInfo extendedInfo) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; + this.extendedInfo = extendedInfo; } public ProjectLicenseInfo(String groupId, String artifactId, String version, boolean hasMatchLicenses) { @@ -214,4 +218,8 @@ public void setApproved(boolean approved) { public boolean isApproved() { return approved; } + + public ExtendedInfo getExtendedInfo() { + return extendedInfo; + } } diff --git a/src/main/java/org/codehaus/mojo/license/extended/ExtendedInfo.java b/src/main/java/org/codehaus/mojo/license/extended/ExtendedInfo.java new file mode 100644 index 000000000..24a7b7d0e --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/extended/ExtendedInfo.java @@ -0,0 +1,138 @@ +package org.codehaus.mojo.license.extended; + +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2019 Jan-Hendrik Diederich + * %% + * 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.apache.maven.artifact.Artifact; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; + +/** + * Class which contains extended licensing information which was found in other files in the JAR, + * not only Mavens pom.xml's. + */ +public class ExtendedInfo { + private String name; + private Artifact artifact; + private List infoFiles = new ArrayList<>(); + private String implementationVendor; + private String bundleVendor; + private String bundleLicense; + + private String inceptionYear; + private Organization organization; + private List developers; + private Scm scm; + private String url; + + public Artifact getArtifact() { + return artifact; + } + + public void setArtifact(Artifact artifact) { + this.artifact = artifact; + } + + public List getInfoFiles() { + return infoFiles; + } + + public void setInfoFiles(List infoFiles) { + this.infoFiles = infoFiles; + } + + public String getImplementationVendor() { + return implementationVendor; + } + + public void setImplementationVendor(String implementationVendor) { + this.implementationVendor = implementationVendor; + } + + public String getBundleVendor() { + return bundleVendor; + } + + public void setBundleVendor(String bundleVendor) { + this.bundleVendor = bundleVendor; + } + + public String getBundleLicense() { + return bundleLicense; + } + + public void setBundleLicense(String bundleLicense) { + this.bundleLicense = bundleLicense; + } + + public String getInceptionYear() { + return inceptionYear; + } + + public void setInceptionYear(String inceptionYear) { + this.inceptionYear = inceptionYear; + } + + public Organization getOrganization() { + return organization; + } + + public void setOrganization(Organization organization) { + this.organization = organization; + } + + public List getDevelopers() { + return developers; + } + + public void setDevelopers(List developers) { + this.developers = developers; + } + + public Scm getScm() { + return scm; + } + + public void setScm(Scm scm) { + this.scm = scm; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/codehaus/mojo/license/extended/InfoFile.java b/src/main/java/org/codehaus/mojo/license/extended/InfoFile.java new file mode 100644 index 000000000..f4ce76cf4 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/extended/InfoFile.java @@ -0,0 +1,95 @@ +package org.codehaus.mojo.license.extended; + +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2019 Jan-Hendrik Diederich + * %% + * 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.HashSet; +import java.util.Set; + +import org.codehaus.mojo.license.download.LicenseDownloader; + +/** + * Information about a NOTICE or LICENSE file. + */ +public class InfoFile { + /** + * The type of the source for the info file. + */ + public enum Type { + /** + * Generic ...NOTICE....txt file. + */ + NOTICE, + /** + * Generic ...LICENSE...txt file. + */ + LICENSE, + /** + * File name matches a SPDX license id. + */ + SPDX_LICENSE + } + + private String fileName; + private String content; + private Set extractedCopyrightLines = new HashSet<>(); + private Type type; + + private String contentChecksum; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + this.contentChecksum = LicenseDownloader.calculateStringChecksum(content); + } + + public Set getExtractedCopyrightLines() { + return extractedCopyrightLines; + } + + public void setExtractedCopyrightLines(Set extractedCopyrightLines) { + this.extractedCopyrightLines = extractedCopyrightLines; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getContentChecksum() { + return contentChecksum; + } +} diff --git a/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/CalcFileWriter.java b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/CalcFileWriter.java new file mode 100644 index 000000000..5cce59b4f --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/CalcFileWriter.java @@ -0,0 +1,1029 @@ +package org.codehaus.mojo.license.extended.spreadsheet; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; +import org.codehaus.mojo.license.download.ProjectLicense; +import org.codehaus.mojo.license.download.ProjectLicenseInfo; +import org.codehaus.mojo.license.extended.ExtendedInfo; +import org.codehaus.mojo.license.extended.InfoFile; +import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument; +import org.odftoolkit.odfdom.doc.table.OdfTable; +import org.odftoolkit.odfdom.doc.table.OdfTableCell; +import org.odftoolkit.odfdom.doc.table.OdfTableCellRange; +import org.odftoolkit.odfdom.doc.table.OdfTableColumn; +import org.odftoolkit.odfdom.doc.table.OdfTableRow; +import org.odftoolkit.odfdom.dom.OdfContentDom; +import org.odftoolkit.odfdom.dom.OdfSettingsDom; +import org.odftoolkit.odfdom.dom.element.config.ConfigConfigItemElement; +import org.odftoolkit.odfdom.dom.element.config.ConfigConfigItemMapEntryElement; +import org.odftoolkit.odfdom.dom.element.style.StyleParagraphPropertiesElement; +import org.odftoolkit.odfdom.dom.element.style.StyleTableCellPropertiesElement; +import org.odftoolkit.odfdom.dom.element.style.StyleTextPropertiesElement; +import org.odftoolkit.odfdom.dom.element.text.TextAElement; +import org.odftoolkit.odfdom.dom.style.OdfStyleFamily; +import org.odftoolkit.odfdom.dom.style.props.OdfTableColumnProperties; +import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles; +import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle; +import org.odftoolkit.odfdom.type.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.COPYRIGHT_JOIN_SEPARATOR; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.CurrentRowData; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.DEVELOPERS_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.DEVELOPERS_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.DEVELOPERS_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.EXTENDED_INFO_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.EXTENDED_INFO_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.GAP_WIDTH; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.GENERAL_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.GENERAL_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INCEPTION_YEAR_WIDTH; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_LICENSES_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_LICENSES_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_LICENSES_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_NOTICES_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_NOTICES_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_NOTICES_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_SPDX_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_SPDX_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.INFO_SPDX_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.LICENSES_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.LICENSES_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.LICENSES_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MANIFEST_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MANIFEST_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MAVEN_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MAVEN_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MISC_COLUMNS; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MISC_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.MISC_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.PLUGIN_ID_END_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.PLUGIN_ID_START_COLUMN; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.TABLE_NAME; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.TIMEZONE_WIDTH; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.getDownloadColumn; + +/** + * Writes LibreOffices Calc ODS file. + */ +public class CalcFileWriter { + private static final Logger LOG = LoggerFactory.getLogger(CalcFileWriter.class); + + private static final String HEADER_CELL_STYLE = "headerCellStyle"; + private static final String HYPERLINK_NORMAL_STYLE = "hyperlinkNormalStyle"; + private static final String HYPERLINK_GRAY_STYLE = "hyperlinkGrayStyle"; + private static final String GRAY_CELL_STYLE = "grayCellStyle"; + private static final String NORMAL_CELL_STYLE = "normalCellStyle"; + private static final int DOWNLOAD_COLUMN_WIDTH = 6_000; + private static final String VALUE_TYPE_STRING = "string"; + private static final String CONFIG_TYPE_SHORT = "short"; + + private CalcFileWriter() {} + + public static void write(List projectLicenseInfos, final File licensesCalcOutputFile) { + if (CollectionUtils.isEmpty(projectLicenseInfos)) { + LOG.debug("Nothing to write to excel, no project data."); + return; + } + LOG.debug("Write LibreOffice Calc file {}", licensesCalcOutputFile); + + try (OdfSpreadsheetDocument spreadsheet = OdfSpreadsheetDocument.newSpreadsheetDocument()) { + List tableList = spreadsheet.getTableList(); + final OdfTable table; + if (!tableList.isEmpty()) { + table = tableList.get(0); + } else { + table = OdfTable.newTable(spreadsheet); + } + table.setTableName(TABLE_NAME); + + createHeaderStyle(spreadsheet); + + createHeader(projectLicenseInfos, spreadsheet, table); + + writeData( + projectLicenseInfos, spreadsheet, table, convertToOdfColor(SpreadsheetUtil.ALTERNATING_ROWS_COLOR)); + + try (OutputStream fileOut = Files.newOutputStream(licensesCalcOutputFile.toPath())) { + spreadsheet.save(fileOut); + LOG.debug("Written LibreOffice Calc file {}", licensesCalcOutputFile); + } catch (IOException e) { + LOG.error("Error on storing LibreOffice Calc file with license and other information", e); + } + } catch (Exception e) { + LOG.error("Error on creating LibreOffice Calc file with license and other information", e); + } + } + + private static Color convertToOdfColor(final int[] color) { + return new Color(color[0], color[1], color[2]); + } + + private static void createHeader( + List projectLicenseInfos, OdfSpreadsheetDocument spreadsheet, OdfTable table) { + boolean hasExtendedInfo = false; + for (ProjectLicenseInfo projectLicenseInfo : projectLicenseInfos) { + if (projectLicenseInfo.getExtendedInfo() != null) { + hasExtendedInfo = true; + break; + } + } + + /* + All rows must be added before any cell merges, or merges on new lines will be merged like the previous + rows. + */ + + // Create 1st header row. The Maven/JAR header row + OdfTableRow mavenJarRow = table.getRowByIndex(0); + + // Create 2nd header row + OdfTableRow secondHeaderRow = table.appendRow(); + + // Create 3rd header row + OdfTableRow thirdHeaderRow = table.appendRow(); + + // Create Maven header cell + createMergedCellsInRow( + table, MAVEN_START_COLUMN, MAVEN_END_COLUMN, mavenJarRow, "Maven information", 0, HEADER_CELL_STYLE); + + if (hasExtendedInfo) { + // Create JAR header cell + createMergedCellsInRow( + table, + EXTENDED_INFO_START_COLUMN, + EXTENDED_INFO_END_COLUMN, + mavenJarRow, + "JAR Content", + 0, + HEADER_CELL_STYLE); + } + + // Create Maven "General" header + createMergedCellsInRow( + table, GENERAL_START_COLUMN, GENERAL_END_COLUMN, secondHeaderRow, "General", 1, HEADER_CELL_STYLE); + + // Create Maven "Plugin ID" header + createMergedCellsInRow( + table, + PLUGIN_ID_START_COLUMN, + PLUGIN_ID_END_COLUMN, + secondHeaderRow, + "Plugin ID", + 1, + HEADER_CELL_STYLE); + + // Gap "General" <-> "Plugin ID". + setColumnWidth(table, GENERAL_END_COLUMN, GAP_WIDTH); + + // Create Maven "Licenses" header + createMergedCellsInRow( + table, LICENSES_START_COLUMN, LICENSES_END_COLUMN, secondHeaderRow, "Licenses", 1, HEADER_CELL_STYLE); + + // Gap "Plugin ID" <-> "Licenses". + setColumnWidth(table, PLUGIN_ID_END_COLUMN, GAP_WIDTH); + + // Create Maven "Developers" header + createMergedCellsInRow( + table, + DEVELOPERS_START_COLUMN, + DEVELOPERS_END_COLUMN, + secondHeaderRow, + "Developers", + 1, + HEADER_CELL_STYLE); + + // Gap "Licenses" <-> "Developers". + setColumnWidth(table, LICENSES_END_COLUMN, GAP_WIDTH); + + // Create Maven "Miscellaneous" header + createMergedCellsInRow( + table, MISC_START_COLUMN, MISC_END_COLUMN, secondHeaderRow, "Miscellaneous", 1, HEADER_CELL_STYLE); + + // Gap "Developers" <-> "Miscellaneous". + setColumnWidth(table, DEVELOPERS_END_COLUMN, GAP_WIDTH); + + if (hasExtendedInfo) { + createMergedCellsInRow( + table, + MANIFEST_START_COLUMN, + MANIFEST_END_COLUMN, + secondHeaderRow, + "MANIFEST.MF", + 1, + HEADER_CELL_STYLE); + + // Gap "Miscellaneous" <-> "MANIFEST.MF". + setColumnWidth(table, DEVELOPERS_END_COLUMN, GAP_WIDTH); + + createMergedCellsInRow( + table, + INFO_NOTICES_START_COLUMN, + INFO_NOTICES_END_COLUMN, + secondHeaderRow, + "Notices text files", + 1, + HEADER_CELL_STYLE); + + // Gap "MANIFEST.MF" <-> "Notice text files". + setColumnWidth(table, MANIFEST_END_COLUMN, GAP_WIDTH); + + createMergedCellsInRow( + table, + INFO_LICENSES_START_COLUMN, + INFO_LICENSES_END_COLUMN, + secondHeaderRow, + "License text files", + 1, + HEADER_CELL_STYLE); + + // Gap "Notice text files" <-> "License text files". + setColumnWidth(table, INFO_NOTICES_END_COLUMN, GAP_WIDTH); + + createMergedCellsInRow( + table, + INFO_SPDX_START_COLUMN, + INFO_SPDX_END_COLUMN, + secondHeaderRow, + "SPDX license id matched", + 1, + HEADER_CELL_STYLE); + + // Gap "License text files" <-> "SPDX license matches". + setColumnWidth(table, INFO_LICENSES_END_COLUMN, GAP_WIDTH); + } + // sheet.setColumnGroupCollapsed(); + + setColumnWidth(table, getDownloadColumn(hasExtendedInfo) - 1, GAP_WIDTH); + setColumnWidth(table, getDownloadColumn(hasExtendedInfo), DOWNLOAD_COLUMN_WIDTH); + + // General + createCellsInRow(thirdHeaderRow, GENERAL_START_COLUMN, HEADER_CELL_STYLE, "Name"); + // Plugin ID + createCellsInRow( + thirdHeaderRow, PLUGIN_ID_START_COLUMN, HEADER_CELL_STYLE, "Group ID", "Artifact ID", "Version"); + // Licenses + createCellsInRow( + thirdHeaderRow, + LICENSES_START_COLUMN, + HEADER_CELL_STYLE, + "Name", + "URL", + "Distribution", + "Comments", + "File"); + // Developers + createCellsInRow( + thirdHeaderRow, + DEVELOPERS_START_COLUMN, + HEADER_CELL_STYLE, + "Id", + "Email", + "Name", + "Organization", + "Organization URL", + "URL", + "Timezone"); + // Miscellaneous + createCellsInRow( + thirdHeaderRow, MISC_START_COLUMN, HEADER_CELL_STYLE, "Inception Year", "Organization", "SCM", "URL"); + + int headerLineCount = 3; + + if (hasExtendedInfo) { + // MANIFEST.MF + createCellsInRow( + thirdHeaderRow, + MANIFEST_START_COLUMN, + HEADER_CELL_STYLE, + "Bundle license", + "Bundle vendor", + "Implementation vendor"); + // 3 InfoFile groups: Notices, Licenses and SPDX-Licenses. + createInfoFileCellsInRow( + thirdHeaderRow, + HEADER_CELL_STYLE, + INFO_NOTICES_START_COLUMN, + INFO_LICENSES_START_COLUMN, + INFO_SPDX_START_COLUMN); + + createFreezePane(spreadsheet, table, getDownloadColumn(true) - 1, headerLineCount); + } else { + createFreezePane(spreadsheet, table, getDownloadColumn(false) - 1, headerLineCount); + } + + createFreezePane(spreadsheet, table, GENERAL_END_COLUMN, headerLineCount); + } + + private static void setColumnWidth(OdfTable table, int column, int width) { + table.getColumnByIndex(column) + .getOdfElement() + .setProperty(OdfTableColumnProperties.ColumnWidth, (width / 100) + "mm"); + } + + private static void createFreezePane( + OdfSpreadsheetDocument spreadsheet, OdfTable table, int column, int lineCount) { + // TODO: Find out why this perfect XML is ignored. Use FreezePane function from ODFToolkit after they add it. + + final OdfSettingsDom settingsDom; + try { + settingsDom = spreadsheet.getSettingsDom(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + NodeList childNodes = settingsDom.getFirstChild().getFirstChild().getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node child = childNodes.item(i); + if ("config:config-item-set".equals(child.getNodeName()) + && "ooo:view-settings".equals(((Element) child).getAttribute("config:name"))) { + + NodeList subChilds = child.getChildNodes(); + for (int j = 0; j < subChilds.getLength(); j++) { + Node subChild = subChilds.item(j); + if ("config:config-item-map-indexed".equals(subChild.getNodeName()) + && "Views".equals(((Element) subChild).getAttribute("config:name"))) { + + break; + } + } + break; + } + } + + XPath xpath = settingsDom.getXPath(); + NodeList list; + try { + list = (NodeList) xpath.evaluate( + "/office:document-settings/" + "office:settings/" + + "config:config-item-set/" + + "config:config-item-map-indexed/" + + "config:config-item-map-entry/" + + "config:config-item-map-named/" + + "config:config-item-map-entry", + // "/config:config-item-set[@config:name=\"ooo:view-settings\"]" + + settingsDom, + XPathConstants.NODE); + + /* + 2 + 2 + 1 + 4 + 3 + + 0 + 1 + 0 + 3 + */ + if (list instanceof ConfigConfigItemMapEntryElement) { + ConfigConfigItemMapEntryElement entryElement = (ConfigConfigItemMapEntryElement) list; + + appendConfigItemElement(entryElement, "HorizontalSplitMode", CONFIG_TYPE_SHORT, "2"); + appendConfigItemElement(entryElement, "VerticalSplitMode", CONFIG_TYPE_SHORT, "2"); + + appendConfigItemElement(entryElement, "HorizontalSplitPosition", "int", "1"); + appendConfigItemElement(entryElement, "VerticalSplitPosition", "int", "3"); + + appendConfigItemElement(entryElement, "ActiveSplitRange", CONFIG_TYPE_SHORT, "3"); + + appendConfigItemElement(entryElement, "PositionLeft", "int", "0"); + appendConfigItemElement(entryElement, "PositionRight", "int", "1"); + appendConfigItemElement(entryElement, "PositionTop", "int", "0"); + appendConfigItemElement(entryElement, "PositionBottom", "int", "3"); + + appendConfigItemElement(entryElement, "ShowGrid", "boolean", "true"); + appendConfigItemElement(entryElement, "AnchoredTextOverflowLegacy", "boolean", "false"); + } + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } + + private static void appendConfigItemElement( + ConfigConfigItemMapEntryElement entryElement, String configName, String configType, String nodeValue) { + ConfigConfigItemElement horizontalSplitMode = null; + if (entryElement.hasChildNodes()) { + NodeList nodeList = entryElement.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node instanceof ConfigConfigItemElement) { + ConfigConfigItemElement itemElement = (ConfigConfigItemElement) node; + if (configName.equals(itemElement.getConfigNameAttribute())) { + horizontalSplitMode = (ConfigConfigItemElement) node; + break; + } + } + } + } + if (horizontalSplitMode == null) { + horizontalSplitMode = entryElement.newConfigConfigItemElement(configName, configType); + } else { + if (horizontalSplitMode.hasChildNodes()) { + // Find text node and set new value if found. + for (int i = 0; i < horizontalSplitMode.getLength(); i++) { + Node child = horizontalSplitMode.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + child.setNodeValue(nodeValue); + return; + } + } + } + } + horizontalSplitMode.newTextNode(nodeValue); + } + + private static void createHeaderStyle(OdfSpreadsheetDocument spreadsheet) { + OdfOfficeStyles styles = spreadsheet.getOrCreateDocumentStyles(); + OdfStyle headerStyle = styles.newStyle(HEADER_CELL_STYLE, OdfStyleFamily.TableCell); + + headerStyle.setProperty(StyleTextPropertiesElement.FontFamily, "Arial"); + headerStyle.setProperty(StyleTextPropertiesElement.FontWeight, "bold"); + headerStyle.setProperty(StyleTableCellPropertiesElement.BackgroundColor, "#CCFFCC"); + headerStyle.setProperty(StyleParagraphPropertiesElement.TextAlign, "center"); + headerStyle.setProperty(StyleTableCellPropertiesElement.VerticalAlign, "middle"); + headerStyle.setProperty(StyleTableCellPropertiesElement.Border, "1.0pt solid #000000"); + } + + /* Improvement: Clean this method up. + Reduce parameters, complicated parameters/DTO pattern. + But keep it still threadsafe. + */ + private static void writeData( + List projectLicenseInfos, + OdfSpreadsheetDocument wb, + OdfTable table, + Color alternatingRowsColor) { + final int firstRowIndex = 3; + int currentRowIndex = firstRowIndex; + final Map rowMap = new HashMap<>(); + boolean hasExtendedInfo = false; + + final OdfStyle hyperlinkStyleNormal = createHyperlinkStyle(wb, HYPERLINK_NORMAL_STYLE, null); + final OdfStyle hyperlinkStyleGray = createHyperlinkStyle(wb, HYPERLINK_GRAY_STYLE, alternatingRowsColor); + + boolean grayBackground = false; + OdfOfficeStyles officeStyles = wb.getOrCreateDocumentStyles(); + OdfStyle styleGray = officeStyles.newStyle(GRAY_CELL_STYLE, OdfStyleFamily.TableCell); + styleGray.setProperty(StyleTableCellPropertiesElement.BackgroundColor, alternatingRowsColor.toString()); + styleGray.setProperty(OdfTableColumnProperties.UseOptimalColumnWidth, String.valueOf(true)); + + /* Set own, empty style, instead of leaving the style out, + because otherwise it copies the style of the row above. */ + OdfStyle styleNormal = officeStyles.newStyle(NORMAL_CELL_STYLE, OdfStyleFamily.TableCell); + styleNormal.setProperty(OdfTableColumnProperties.UseOptimalColumnWidth, String.valueOf(true)); + + for (ProjectLicenseInfo projectInfo : projectLicenseInfos) { + final OdfStyle cellStyle, hyperlinkStyle; + LOG.debug( + "Writing {}:{} into LibreOffice calc file", projectInfo.getGroupId(), projectInfo.getArtifactId()); + if (grayBackground) { + cellStyle = styleGray; + hyperlinkStyle = hyperlinkStyleGray; + } else { + cellStyle = styleNormal; + hyperlinkStyle = hyperlinkStyleNormal; + } + grayBackground = !grayBackground; + + int extraRows = 0; + OdfTableRow currentRow = table.appendRow(); + rowMap.put(currentRowIndex, currentRow); + // Plugin ID + createDataCellsInRow( + currentRow, + PLUGIN_ID_START_COLUMN, + cellStyle, + projectInfo.getGroupId(), + projectInfo.getArtifactId(), + projectInfo.getVersion()); + // Licenses + final CellListParameter cellListParameter = new CellListParameter(table, rowMap, cellStyle); + CurrentRowData currentRowData = new CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addList( + cellListParameter, + currentRowData, + LICENSES_START_COLUMN, + LICENSES_COLUMNS, + projectInfo.getLicenses(), + (OdfTableRow licenseRow, ProjectLicense license) -> { + OdfTableCell[] licenses = createDataCellsInRow( + licenseRow, + LICENSES_START_COLUMN, + cellStyle, + license.getName(), + license.getUrl(), + license.getDistribution(), + license.getComments(), + license.getFile()); + addHyperlinkIfExists(table, licenses[1], hyperlinkStyle); + }); + + final ExtendedInfo extendedInfo = projectInfo.getExtendedInfo(); + if (extendedInfo != null) { + hasExtendedInfo = true; + // General + createDataCellsInRow(currentRow, GENERAL_START_COLUMN, cellStyle, extendedInfo.getName()); + // Developers + currentRowData = new CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addList( + cellListParameter, + currentRowData, + DEVELOPERS_START_COLUMN, + DEVELOPERS_COLUMNS, + extendedInfo.getDevelopers(), + (OdfTableRow developerRow, Developer developer) -> { + OdfTableCell[] licenses = createDataCellsInRow( + developerRow, + DEVELOPERS_START_COLUMN, + cellStyle, + developer.getId(), + developer.getEmail(), + developer.getName(), + developer.getOrganization(), + developer.getOrganizationUrl(), + developer.getUrl(), + developer.getTimezone()); + addHyperlinkIfExists(table, licenses[1], hyperlinkStyle, true); + addHyperlinkIfExists(table, licenses[4], hyperlinkStyle); + addHyperlinkIfExists(table, licenses[5], hyperlinkStyle); + }); + // Miscellaneous + OdfTableCell[] miscCells = createDataCellsInRow( + currentRow, + MISC_START_COLUMN, + cellStyle, + extendedInfo.getInceptionYear(), + Optional.ofNullable(extendedInfo.getOrganization()) + .map(Organization::getName) + .orElse(null), + Optional.ofNullable(extendedInfo.getScm()) + .map(Scm::getUrl) + .orElse(null), + extendedInfo.getUrl()); + addHyperlinkIfExists(table, miscCells[2], hyperlinkStyle); + addHyperlinkIfExists(table, miscCells[3], hyperlinkStyle); + + // MANIFEST.MF + createDataCellsInRow( + currentRow, + MANIFEST_START_COLUMN, + cellStyle, + extendedInfo.getBundleLicense(), + extendedInfo.getBundleVendor(), + extendedInfo.getImplementationVendor()); + + // Info files + if (!CollectionUtils.isEmpty(extendedInfo.getInfoFiles())) { + // Sort all info files by type into 3 different lists, each list for each of the 3 types. + List notices = new ArrayList<>(); + List licenses = new ArrayList<>(); + List spdxs = new ArrayList<>(); + extendedInfo.getInfoFiles().forEach(infoFile -> { + switch (infoFile.getType()) { + case LICENSE: + licenses.add(infoFile); + break; + case NOTICE: + notices.add(infoFile); + break; + case SPDX_LICENSE: + spdxs.add(infoFile); + break; + default: + break; + } + }); + // InfoFile notices text file + currentRowData = new CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, + currentRowData, + INFO_NOTICES_START_COLUMN, + INFO_NOTICES_COLUMNS, + notices); + // InfoFile licenses text file + currentRowData = new CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, + currentRowData, + INFO_LICENSES_START_COLUMN, + INFO_LICENSES_COLUMNS, + licenses); + // InfoFile spdx licenses text file + currentRowData = new CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, currentRowData, INFO_SPDX_START_COLUMN, INFO_SPDX_COLUMNS, spdxs); + } else if (cellListParameter.cellStyle != null) { + setStyleOnEmptyCells( + cellListParameter, currentRowData, INFO_NOTICES_START_COLUMN, INFO_NOTICES_COLUMNS); + setStyleOnEmptyCells( + cellListParameter, currentRowData, INFO_LICENSES_START_COLUMN, INFO_LICENSES_COLUMNS); + setStyleOnEmptyCells(cellListParameter, currentRowData, INFO_SPDX_START_COLUMN, INFO_SPDX_COLUMNS); + } + } else { + createDataCellsInRow(currentRow, GENERAL_START_COLUMN, cellStyle, 1); + createDataCellsInRow(currentRow, DEVELOPERS_START_COLUMN, cellStyle, DEVELOPERS_COLUMNS); + createDataCellsInRow(currentRow, MISC_START_COLUMN, cellStyle, MISC_COLUMNS); + } + + final int downloadColumn = getDownloadColumn(hasExtendedInfo); + if (CollectionUtils.isNotEmpty(projectInfo.getDownloaderMessages())) { + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addList( + cellListParameter, + currentRowData, + downloadColumn, + SpreadsheetUtil.DOWNLOAD_MESSAGE_COLUMNS, + projectInfo.getDownloaderMessages(), + (OdfTableRow licenseRow, String message) -> { + OdfTableCell[] licenses = + createDataCellsInRow(licenseRow, downloadColumn, cellStyle, message); + if (message.matches(SpreadsheetUtil.VALID_LINK)) { + addHyperlinkIfExists(table, licenses[0], hyperlinkStyle); + } + }); + } else { + // Add empty cell, so it doesn't copy the previous row cell. + OdfTableCell cell = currentRow.getCellByIndex(downloadColumn); + cell.setValueType(VALUE_TYPE_STRING); + cell.getOdfElement().setStyleName(getCellStyleName(cellStyle)); + } + currentRowIndex += extraRows + 1; + } + + autosizeColumns(table, hasExtendedInfo, currentRowIndex); + } + + private static OdfStyle createHyperlinkStyle(OdfSpreadsheetDocument wb, String name, Color backgroundColor) { + OdfOfficeStyles styles = wb.getOrCreateDocumentStyles(); + OdfStyle hyperlinkStyle = styles.newStyle(name, OdfStyleFamily.TableCell); + + hyperlinkStyle.setProperty(StyleTextPropertiesElement.FontFamily, "Arial"); + hyperlinkStyle.setProperty(StyleTextPropertiesElement.Color, Color.BLUE.toString()); + hyperlinkStyle.setProperty(StyleParagraphPropertiesElement.TextAlign, "center"); + hyperlinkStyle.setProperty(StyleTableCellPropertiesElement.VerticalAlign, "middle"); + + if (backgroundColor != null) { + hyperlinkStyle.setProperty(StyleTableCellPropertiesElement.BackgroundColor, backgroundColor.toString()); + } + return hyperlinkStyle; + } + + private static void autosizeColumns(OdfTable table, boolean hasExtendedInfo, int rows) { + autosizeColumns( + table, + rows, + new ImmutablePair<>(GENERAL_START_COLUMN, GENERAL_END_COLUMN), + new ImmutablePair<>(PLUGIN_ID_START_COLUMN, PLUGIN_ID_END_COLUMN), + new ImmutablePair<>(LICENSES_START_COLUMN, LICENSES_END_COLUMN), + new ImmutablePair<>(DEVELOPERS_START_COLUMN, DEVELOPERS_END_COLUMN - 1), + new ImmutablePair<>(MISC_START_COLUMN + 1, MISC_END_COLUMN)); + // The column header widths are most likely wider than the actual cells content. + setColumnWidth(table, DEVELOPERS_END_COLUMN - 1, TIMEZONE_WIDTH); + setColumnWidth(table, MISC_START_COLUMN, INCEPTION_YEAR_WIDTH); + if (hasExtendedInfo) { + autosizeColumns( + table, + rows, + new ImmutablePair<>(MANIFEST_START_COLUMN, MANIFEST_END_COLUMN), + new ImmutablePair<>(INFO_NOTICES_START_COLUMN + 2, INFO_NOTICES_END_COLUMN), + new ImmutablePair<>(INFO_LICENSES_START_COLUMN + 2, INFO_LICENSES_END_COLUMN), + new ImmutablePair<>(INFO_SPDX_START_COLUMN + 2, INFO_SPDX_END_COLUMN)); + } + } + + @SafeVarargs + private static void autosizeColumns(OdfTable sheet, int rows, Pair... ranges) { + for (Pair range : ranges) { + for (int i = range.getLeft(); i < range.getRight(); i++) { + final float sizeFactor = 2.0f; + float size = 25; + // Get max width by taking the max string length multiplied by sizeFactor. + for (int row = 0; row < rows; row++) { + OdfTableCell cell = sheet.getCellByPosition(i, row); + if (VALUE_TYPE_STRING.equals(cell.getValueType())) { + String stringValue = cell.getStringValue(); + size = Math.max(stringValue.length() * sizeFactor, size); + } + } + final OdfTableColumn column = sheet.getColumnByIndex(i); + // The attribute is ignored by LibreOffice Calc, set it for other applications. + column.setUseOptimalWidth(true); + + column.setWidth((long) size); + } + } + } + + private static int addInfoFileList( + CellListParameter cellListParameter, + CurrentRowData currentRowData, + int startColumn, + int columnsToFill, + List infoFiles) { + return addList( + cellListParameter, + currentRowData, + startColumn, + columnsToFill, + infoFiles, + (OdfTableRow infoFileRow, InfoFile infoFile) -> { + final String copyrightLines = Optional.ofNullable(infoFile.getExtractedCopyrightLines()) + .map(strings -> String.join(COPYRIGHT_JOIN_SEPARATOR, strings)) + .orElse(null); + createDataCellsInRow( + infoFileRow, + startColumn, + cellListParameter.getCellStyle(), + // This would otherwise lead to invalid XML characters in the ODS file content. + infoFile.getContent().replace("\f", "\n"), + copyrightLines, + infoFile.getFileName()); + }); + } + + private static int addList( + CellListParameter cellListParameter, + CurrentRowData currentRowData, + int startColumn, + int columnsToFill, + List list, + BiConsumer biConsumer) { + if (!CollectionUtils.isEmpty(list)) { + for (int i = 0; i < list.size(); i++) { + T type = list.get(i); + Integer index = currentRowData.getCurrentRowIndex() + i; + OdfTableRow row = cellListParameter.getRows().get(index); + if (row == null) { + row = cellListParameter.getSheet().appendRow(); + cellListParameter.getRows().put(index, row); + if (cellListParameter.getCellStyle() != null) { + // Style all empty left cells, in the columns left from this + createAndStyleCells( + row, + cellListParameter.getCellStyle(), + new ImmutablePair<>(GENERAL_START_COLUMN, GENERAL_END_COLUMN), + new ImmutablePair<>(PLUGIN_ID_START_COLUMN, PLUGIN_ID_END_COLUMN), + new ImmutablePair<>(LICENSES_START_COLUMN, LICENSES_END_COLUMN)); + if (currentRowData.isHasExtendedInfo()) { + createAndStyleCells( + row, + cellListParameter.getCellStyle(), + new ImmutablePair<>(DEVELOPERS_START_COLUMN, DEVELOPERS_END_COLUMN), + new ImmutablePair<>(MISC_START_COLUMN, MISC_END_COLUMN), + // JAR + new ImmutablePair<>(MANIFEST_START_COLUMN, MANIFEST_END_COLUMN), + new ImmutablePair<>(INFO_LICENSES_START_COLUMN, INFO_LICENSES_END_COLUMN), + new ImmutablePair<>(INFO_NOTICES_START_COLUMN, INFO_NOTICES_END_COLUMN), + new ImmutablePair<>(INFO_SPDX_START_COLUMN, INFO_SPDX_END_COLUMN)); + } + } + currentRowData.setExtraRows(currentRowData.getExtraRows() + 1); + } + biConsumer.accept(row, type); + } + } else if (cellListParameter.cellStyle != null) { + setStyleOnEmptyCells(cellListParameter, currentRowData, startColumn, columnsToFill); + } + return currentRowData.getExtraRows(); + } + + /** + * If no cells are set, color at least the background, + * to color concatenated blocks with the same background color. + * + * @param cellListParameter Passes data about sheet, row, cell style. + * @param currentRowData Passes data about the current indices for rows and columns. + * @param startColumn Column where to start setting the style. + * @param columnsToFill How many columns to set the style on, starting from 'startColumn'. + */ + private static void setStyleOnEmptyCells( + CellListParameter cellListParameter, CurrentRowData currentRowData, int startColumn, int columnsToFill) { + OdfTableRow row = cellListParameter.getRows().get(currentRowData.getCurrentRowIndex()); + for (int i = 0; i < columnsToFill; i++) { + OdfTableCell cell = row.getCellByIndex(startColumn + i); + cell.setValueType(VALUE_TYPE_STRING); + cell.getOdfElement().setStyleName(getCellStyleName(cellListParameter.getCellStyle())); + } + } + + @SafeVarargs + private static void createAndStyleCells(OdfTableRow row, OdfStyle cellStyle, Pair... ranges) { + for (Pair range : ranges) { + for (int i = range.getLeft(); i < range.getRight(); i++) { + OdfTableCell cell = row.getCellByIndex(i); + cell.setValueType(VALUE_TYPE_STRING); + cell.getOdfElement().setStyleName(getCellStyleName(cellStyle)); + } + } + } + + public static void applyHyperlink(OdfTable table, OdfTableCell cell, String hyperlink, boolean isEmail) { + + TextAElement aElement; + aElement = ((OdfContentDom) (table.getOdfElement().getOwnerDocument())).newOdfElement(TextAElement.class); + aElement.setXlinkTypeAttribute("simple"); + hyperlink = hyperlink.trim().replace(" dot ", "."); + if (isEmail) { + hyperlink = hyperlink.replace(" at ", "@"); + if (hyperlink.contains("@") && hyperlink.matches(".*\\s[a-zA-Z]{2,3}$")) { + hyperlink = hyperlink.replace(" ", "."); + } + } + aElement.setXlinkHrefAttribute(isEmail ? "mailto:" + hyperlink : hyperlink); + aElement.setTextContent(hyperlink); + Node node = cell.getOdfElement().getFirstChild(); + + node.appendChild(aElement); + } + + private static void addHyperlinkIfExists(OdfTable table, OdfTableCell cell, OdfStyle hyperlinkStyle) { + addHyperlinkIfExists(table, cell, hyperlinkStyle, false); + } + + private static void addHyperlinkIfExists( + OdfTable table, OdfTableCell cell, OdfStyle hyperlinkStyle, boolean isEmail) { + if (!StringUtils.isEmpty(cell.getStringValue())) { + try { + cell.getOdfElement().setStyleName(getCellStyleName(hyperlinkStyle)); + + String content = cell.getStringValue(); + cell.setStringValue(""); + + applyHyperlink(table, cell, content, isEmail); + } catch (IllegalArgumentException e) { + LOG.debug( + "Can't set Hyperlink for cell value " + cell.getStringValue() + " it gets rejected as URI", e); + } + } + } + + /** + * Create data cells in row. + * + * @param row Row. + * @param startColumn Starting column. + * @param cellStyle Cell style. + * @param names Name of cell values. + * @return Array of created table cells. + */ + private static OdfTableCell[] createDataCellsInRow( + OdfTableRow row, int startColumn, OdfStyle cellStyle, String... names) { + OdfTableCell[] result = new OdfTableCell[names.length]; + for (int i = 0; i < names.length; i++) { + OdfTableCell cell = row.getCellByIndex(startColumn + i); + cell.setValueType(VALUE_TYPE_STRING); + if (cellStyle != null) { + cell.getOdfElement().setStyleName(getCellStyleName(cellStyle)); + } + if (!StringUtils.isEmpty(names[i])) { + final String value; + final int maxCellStringLength = Short.MAX_VALUE; + if (names[i].length() > maxCellStringLength) { + value = names[i].substring(0, maxCellStringLength - 3) + "..."; + } else { + value = names[i]; + } + cell.setStringValue(value); + } + result[i] = cell; + } + return result; + } + + /** + * Fills cells with empty strings, so they get created and don't copy the previous rows content and style, + * like the header's background color and bold border. + * + * @param row Row. + * @param startColumn Starting column (inclusive). + * @param cellStyle Cell style. + * @param count Number of columns to set. + */ + private static void createDataCellsInRow(OdfTableRow row, int startColumn, OdfStyle cellStyle, int count) { + for (int i = 0; i < count; i++) { + OdfTableCell cell = row.getCellByIndex(startColumn + i); + cell.setValueType(VALUE_TYPE_STRING); + if (cellStyle != null) { + cell.getOdfElement().setStyleName(getCellStyleName(cellStyle)); + } + cell.setStringValue(""); + } + } + + private static String getCellStyleName(OdfStyle cellStyle) { + return cellStyle.getAttributes().item(1).getNodeValue(); + } + + /** + * Create cells for InfoFile content. + * + * @param row The row to insert cells into. + * @param styleName Name of the style. + * @param startPositions The start position of the 3 columns for an InfoFile. + */ + private static void createInfoFileCellsInRow(OdfTableRow row, String styleName, int... startPositions) { + for (int startPosition : startPositions) { + createCellsInRow(row, startPosition, styleName, "Content", "Extracted copyright lines", "File"); + } + } + + private static void createCellsInRow(OdfTableRow row, int startColumn, String styleName, String... names) { + for (int i = 0; i < names.length; i++) { + OdfTableCell cell = row.getCellByIndex(startColumn + i); + cell.setValueType(VALUE_TYPE_STRING); + cell.getOdfElement().setStyleName(styleName); + cell.setStringValue(names[i]); + } + } + + private static void createMergedCellsInRow( + OdfTable table, + int startColumn, + int endColumn, + OdfTableRow row, + String cellValue, + int rowIndex, + String styleName) { + OdfTableCell cell = createCellsInRow(startColumn, endColumn, row); + if (cell == null) { + return; + } + final boolean merge = endColumn - 1 > startColumn; + + if (merge) { + OdfTableCellRange cellRange = table.getCellRangeByPosition(startColumn, rowIndex, endColumn - 1, rowIndex); + cellRange.merge(); + } + + // Set value and style only after merge + cell.setStringValue(cellValue); + cell.getOdfElement().setStyleName(styleName); + + // TODO: Add grouping, with a hierarchy, after ODFToolkit offers it. + } + + private static OdfTableCell createCellsInRow(int startColumn, int exclusiveEndColumn, OdfTableRow inRow) { + OdfTableCell firstCell = null; + for (int i = startColumn; i < exclusiveEndColumn; i++) { + OdfTableCell cell = inRow.getCellByIndex(i); + if (i == startColumn) { + firstCell = cell; + } + } + return firstCell; + } + + /** + * Parameters for cells which apply to all cells in each loop iteration. + */ + private static class CellListParameter { + private final OdfTable sheet; + private final Map rows; + private final OdfStyle cellStyle; + + private CellListParameter(OdfTable sheet, Map rows, OdfStyle cellStyle) { + this.sheet = sheet; + this.rows = rows; + this.cellStyle = cellStyle; + } + + OdfTable getSheet() { + return sheet; + } + + Map getRows() { + return rows; + } + + OdfStyle getCellStyle() { + return cellStyle; + } + } +} diff --git a/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/ExcelFileWriter.java b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/ExcelFileWriter.java new file mode 100644 index 000000000..eaba69c72 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/ExcelFileWriter.java @@ -0,0 +1,892 @@ +package org.codehaus.mojo.license.extended.spreadsheet; + +/* + * #%L + * License Maven Plugin + * %% + * Copyright (C) 2019 Jan-Hendrik Diederich + * %% + * 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.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Hyperlink; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; +import org.apache.poi.ss.util.WorkbookUtil; +import org.apache.poi.xssf.usermodel.IndexedColorMap; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.codehaus.mojo.license.download.ProjectLicense; +import org.codehaus.mojo.license.download.ProjectLicenseInfo; +import org.codehaus.mojo.license.extended.ExtendedInfo; +import org.codehaus.mojo.license.extended.InfoFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.GAP_WIDTH; +import static org.codehaus.mojo.license.extended.spreadsheet.SpreadsheetUtil.getDownloadColumn; + +/** + * Writes project license infos into Excel file. + */ +public class ExcelFileWriter { + private static final BorderStyle HEADER_CELLS_BORDER_STYLE = BorderStyle.MEDIUM; + private static final Logger LOG = LoggerFactory.getLogger(ExcelFileWriter.class); + + private ExcelFileWriter() {} + + /** + * Writes a list of projects into Excel file. + * + * @param projectLicenseInfos Project license infos to write. + * @param licensesExcelOutputFile Excel output file in latest format (OOXML). + */ + public static void write(List projectLicenseInfos, final File licensesExcelOutputFile) { + if (CollectionUtils.isEmpty(projectLicenseInfos)) { + LOG.debug("Nothing to write to excel, no project data."); + return; + } + LOG.debug("Write Microsoft Excel file {}", licensesExcelOutputFile); + + final XSSFWorkbook wb = new XSSFWorkbook(); + final Sheet sheet = wb.createSheet(WorkbookUtil.createSafeSheetName(SpreadsheetUtil.TABLE_NAME)); + + final IndexedColorMap colorMap = wb.getStylesSource().getIndexedColors(); + final XSSFColor alternatingRowsColor = new XSSFColor( + new byte[] { + (byte) SpreadsheetUtil.ALTERNATING_ROWS_COLOR[0], + (byte) SpreadsheetUtil.ALTERNATING_ROWS_COLOR[1], + (byte) SpreadsheetUtil.ALTERNATING_ROWS_COLOR[2] + }, + colorMap); + + createHeader(projectLicenseInfos, wb, sheet); + + writeData(projectLicenseInfos, wb, sheet, alternatingRowsColor); + + try (OutputStream fileOut = Files.newOutputStream(licensesExcelOutputFile.toPath())) { + wb.write(fileOut); + LOG.debug("Written Microsoft Excel file {}", licensesExcelOutputFile); + } catch (IOException e) { + LOG.error("Error on storing Microsoft Excel file with license and other information", e); + } + } + + private static void createHeader(List projectLicenseInfos, Workbook wb, Sheet sheet) { + boolean hasExtendedInfo = false; + for (ProjectLicenseInfo projectLicenseInfo : projectLicenseInfos) { + if (projectLicenseInfo.getExtendedInfo() != null) { + hasExtendedInfo = true; + break; + } + } + + // Create header style + CellStyle headerCellStyle = wb.createCellStyle(); + headerCellStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex()); + headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + setBorderStyle(headerCellStyle, HEADER_CELLS_BORDER_STYLE); + + // Create 1st header row. The Maven/JAR header row + Row mavenJarRow = sheet.createRow(0); + + // Create Maven header cell + createMergedCellsInRow( + sheet, + SpreadsheetUtil.MAVEN_START_COLUMN, + SpreadsheetUtil.MAVEN_END_COLUMN, + headerCellStyle, + mavenJarRow, + "Maven information", + 0); + + if (hasExtendedInfo) { + // Create JAR header cell + createMergedCellsInRow( + sheet, + SpreadsheetUtil.EXTENDED_INFO_START_COLUMN, + SpreadsheetUtil.EXTENDED_INFO_END_COLUMN, + headerCellStyle, + mavenJarRow, + "JAR Content", + 0); + } + + // Create 2nd header row + Row secondHeaderRow = sheet.createRow(1); + + // Create Maven "General" header + createMergedCellsInRow( + sheet, + SpreadsheetUtil.GENERAL_START_COLUMN, + SpreadsheetUtil.GENERAL_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "General", + 1); + + // Create Maven "Plugin ID" header + createMergedCellsInRow( + sheet, + SpreadsheetUtil.PLUGIN_ID_START_COLUMN, + SpreadsheetUtil.PLUGIN_ID_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "Plugin ID", + 1); + + // Gap "General" <-> "Plugin ID". + sheet.setColumnWidth(SpreadsheetUtil.GENERAL_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + // Create Maven "Licenses" header + createMergedCellsInRow( + sheet, + SpreadsheetUtil.LICENSES_START_COLUMN, + SpreadsheetUtil.LICENSES_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "Licenses", + 1); + + // Gap "Plugin ID" <-> "Licenses". + sheet.setColumnWidth(SpreadsheetUtil.PLUGIN_ID_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + // Create Maven "Developers" header + createMergedCellsInRow( + sheet, + SpreadsheetUtil.DEVELOPERS_START_COLUMN, + SpreadsheetUtil.DEVELOPERS_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "Developers", + 1); + + // Gap "Licenses" <-> "Developers". + sheet.setColumnWidth(SpreadsheetUtil.LICENSES_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + // Create Maven "Miscellaneous" header + createMergedCellsInRow( + sheet, + SpreadsheetUtil.MISC_START_COLUMN, + SpreadsheetUtil.MISC_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "Miscellaneous", + 1); + + // Gap "Developers" <-> "Miscellaneous". + sheet.setColumnWidth(SpreadsheetUtil.DEVELOPERS_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + if (hasExtendedInfo) { + createMergedCellsInRow( + sheet, + SpreadsheetUtil.MANIFEST_START_COLUMN, + SpreadsheetUtil.MANIFEST_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "MANIFEST.MF", + 1); + + // Gap "Miscellaneous" <-> "MANIFEST.MF". + sheet.setColumnWidth(SpreadsheetUtil.DEVELOPERS_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + createMergedCellsInRow( + sheet, + SpreadsheetUtil.INFO_NOTICES_START_COLUMN, + SpreadsheetUtil.INFO_NOTICES_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "Notices text files", + 1); + + // Gap "MANIFEST.MF" <-> "Notice text files". + sheet.setColumnWidth(SpreadsheetUtil.MANIFEST_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + createMergedCellsInRow( + sheet, + SpreadsheetUtil.INFO_LICENSES_START_COLUMN, + SpreadsheetUtil.INFO_LICENSES_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "License text files", + 1); + + // Gap "Notice text files" <-> "License text files". + sheet.setColumnWidth(SpreadsheetUtil.INFO_NOTICES_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + + createMergedCellsInRow( + sheet, + SpreadsheetUtil.INFO_SPDX_START_COLUMN, + SpreadsheetUtil.INFO_SPDX_END_COLUMN, + headerCellStyle, + secondHeaderRow, + "SPDX license id matched", + 1); + + // Gap "License text files" <-> "SPDX license matches". + sheet.setColumnWidth(SpreadsheetUtil.INFO_LICENSES_END_COLUMN, SpreadsheetUtil.GAP_WIDTH); + } + // sheet.setColumnGroupCollapsed(); + + sheet.setColumnWidth(getDownloadColumn(hasExtendedInfo) - 1, GAP_WIDTH); + + // Create 3rd header row + Row thirdHeaderRow = sheet.createRow(2); + + // General + createCellsInRow(thirdHeaderRow, SpreadsheetUtil.GENERAL_START_COLUMN, headerCellStyle, "Name"); + // Plugin ID + createCellsInRow( + thirdHeaderRow, + SpreadsheetUtil.PLUGIN_ID_START_COLUMN, + headerCellStyle, + "Group ID", + "Artifact ID", + "Version"); + // Licenses + createCellsInRow( + thirdHeaderRow, + SpreadsheetUtil.LICENSES_START_COLUMN, + headerCellStyle, + "Name", + "URL", + "Distribution", + "Comments", + "File"); + // Developers + createCellsInRow( + thirdHeaderRow, + SpreadsheetUtil.DEVELOPERS_START_COLUMN, + headerCellStyle, + "Id", + "Email", + "Name", + "Organization", + "Organization URL", + "URL", + "Timezone"); + // Miscellaneous + createCellsInRow( + thirdHeaderRow, + SpreadsheetUtil.MISC_START_COLUMN, + headerCellStyle, + "Inception Year", + "Organization", + "SCM", + "URL"); + + int headerLineCount = 3; + + if (hasExtendedInfo) { + // MANIFEST.MF + createCellsInRow( + thirdHeaderRow, + SpreadsheetUtil.MANIFEST_START_COLUMN, + headerCellStyle, + "Bundle license", + "Bundle vendor", + "Implementation vendor"); + // 3 InfoFile groups: Notices, Licenses and SPDX-Licenses. + createInfoFileCellsInRow( + thirdHeaderRow, + headerCellStyle, + SpreadsheetUtil.INFO_NOTICES_START_COLUMN, + SpreadsheetUtil.INFO_LICENSES_START_COLUMN, + SpreadsheetUtil.INFO_SPDX_START_COLUMN); + + sheet.createFreezePane(getDownloadColumn(true) - 1, headerLineCount); + } else { + sheet.createFreezePane(getDownloadColumn(false) - 1, headerLineCount); + } + + sheet.createFreezePane(SpreadsheetUtil.GENERAL_END_COLUMN, headerLineCount); + } + + /* Possible improvement: + Clean this method up. + Reduce parameters, complicated parameters/DTO pattern. + But keep it still threadsafe. */ + private static void writeData( + List projectLicenseInfos, + XSSFWorkbook wb, + Sheet sheet, + XSSFColor alternatingRowsColor) { + final int firstRowIndex = 3; + int currentRowIndex = firstRowIndex; + final Map rowMap = new HashMap<>(); + boolean hasExtendedInfo = false; + + final CellStyle hyperlinkStyleNormal = createHyperlinkStyle(wb, null); + final CellStyle hyperlinkStyleGray = createHyperlinkStyle(wb, alternatingRowsColor); + + boolean grayBackground = false; + XSSFCellStyle styleGray = wb.createCellStyle(); + styleGray.setFillForegroundColor(alternatingRowsColor); + styleGray.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + for (ProjectLicenseInfo projectInfo : projectLicenseInfos) { + final CellStyle cellStyle, hyperlinkStyle; + LOG.debug("Writing {}:{} into Microsoft Excel file", projectInfo.getGroupId(), projectInfo.getArtifactId()); + if (grayBackground) { + cellStyle = styleGray; + hyperlinkStyle = hyperlinkStyleGray; + } else { + cellStyle = null; + hyperlinkStyle = hyperlinkStyleNormal; + } + grayBackground = !grayBackground; + + int extraRows = 0; + Row currentRow = sheet.createRow(currentRowIndex); + rowMap.put(currentRowIndex, currentRow); + // Plugin ID + createDataCellsInRow( + currentRow, + SpreadsheetUtil.PLUGIN_ID_START_COLUMN, + cellStyle, + projectInfo.getGroupId(), + projectInfo.getArtifactId(), + projectInfo.getVersion()); + // Licenses + final CellListParameter cellListParameter = new CellListParameter(sheet, rowMap, cellStyle); + SpreadsheetUtil.CurrentRowData currentRowData = + new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addList( + cellListParameter, + currentRowData, + SpreadsheetUtil.LICENSES_START_COLUMN, + SpreadsheetUtil.LICENSES_COLUMNS, + projectInfo.getLicenses(), + (Row licenseRow, ProjectLicense license) -> { + Cell[] licenses = createDataCellsInRow( + licenseRow, + SpreadsheetUtil.LICENSES_START_COLUMN, + cellStyle, + license.getName(), + license.getUrl(), + license.getDistribution(), + license.getComments(), + license.getFile()); + addHyperlinkIfExists(wb, licenses[1], hyperlinkStyle, HyperlinkType.URL); + }); + + final ExtendedInfo extendedInfo = projectInfo.getExtendedInfo(); + if (extendedInfo != null) { + hasExtendedInfo = true; + // General + createDataCellsInRow( + currentRow, SpreadsheetUtil.GENERAL_START_COLUMN, cellStyle, extendedInfo.getName()); + // Developers + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addList( + cellListParameter, + currentRowData, + SpreadsheetUtil.DEVELOPERS_START_COLUMN, + SpreadsheetUtil.DEVELOPERS_COLUMNS, + extendedInfo.getDevelopers(), + (Row developerRow, Developer developer) -> { + Cell[] licenses = createDataCellsInRow( + developerRow, + SpreadsheetUtil.DEVELOPERS_START_COLUMN, + cellStyle, + developer.getId(), + developer.getEmail(), + developer.getName(), + developer.getOrganization(), + developer.getOrganizationUrl(), + developer.getUrl(), + developer.getTimezone()); + addHyperlinkIfExists(wb, licenses[1], hyperlinkStyle, HyperlinkType.EMAIL); + addHyperlinkIfExists(wb, licenses[4], hyperlinkStyle, HyperlinkType.URL); + addHyperlinkIfExists(wb, licenses[5], hyperlinkStyle, HyperlinkType.URL); + }); + // Miscellaneous + Cell[] miscCells = createDataCellsInRow( + currentRow, + SpreadsheetUtil.MISC_START_COLUMN, + cellStyle, + extendedInfo.getInceptionYear(), + Optional.ofNullable(extendedInfo.getOrganization()) + .map(Organization::getName) + .orElse(null), + Optional.ofNullable(extendedInfo.getScm()) + .map(Scm::getUrl) + .orElse(null), + extendedInfo.getUrl()); + addHyperlinkIfExists(wb, miscCells[2], hyperlinkStyle, HyperlinkType.URL); + addHyperlinkIfExists(wb, miscCells[3], hyperlinkStyle, HyperlinkType.URL); + + // MANIFEST.MF + createDataCellsInRow( + currentRow, + SpreadsheetUtil.MANIFEST_START_COLUMN, + cellStyle, + extendedInfo.getBundleLicense(), + extendedInfo.getBundleVendor(), + extendedInfo.getImplementationVendor()); + + // Info files + if (!CollectionUtils.isEmpty(extendedInfo.getInfoFiles())) { + // Sort all info files by type into 3 different lists, each list for each of the 3 types. + List notices = new ArrayList<>(); + List licenses = new ArrayList<>(); + List spdxs = new ArrayList<>(); + extendedInfo.getInfoFiles().forEach(infoFile -> { + switch (infoFile.getType()) { + case LICENSE: + licenses.add(infoFile); + break; + case NOTICE: + notices.add(infoFile); + break; + case SPDX_LICENSE: + spdxs.add(infoFile); + break; + default: + break; + } + }); + // InfoFile notices text file + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_NOTICES_START_COLUMN, + SpreadsheetUtil.INFO_NOTICES_COLUMNS, + notices); + // InfoFile licenses text file + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_LICENSES_START_COLUMN, + SpreadsheetUtil.INFO_LICENSES_COLUMNS, + licenses); + // InfoFile spdx licenses text file + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + extraRows = addInfoFileList( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_SPDX_START_COLUMN, + SpreadsheetUtil.INFO_SPDX_COLUMNS, + spdxs); + } else if (cellListParameter.cellStyle != null) { + setStyleOnEmptyCells( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_NOTICES_START_COLUMN, + SpreadsheetUtil.INFO_NOTICES_COLUMNS); + setStyleOnEmptyCells( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_LICENSES_START_COLUMN, + SpreadsheetUtil.INFO_LICENSES_COLUMNS); + setStyleOnEmptyCells( + cellListParameter, + currentRowData, + SpreadsheetUtil.INFO_SPDX_START_COLUMN, + SpreadsheetUtil.INFO_SPDX_COLUMNS); + } + } + if (CollectionUtils.isNotEmpty(projectInfo.getDownloaderMessages())) { + currentRowData = new SpreadsheetUtil.CurrentRowData(currentRowIndex, extraRows, hasExtendedInfo); + + int startColumn = hasExtendedInfo + ? SpreadsheetUtil.DOWNLOAD_MESSAGE_EXTENDED_COLUMN + : SpreadsheetUtil.DOWNLOAD_MESSAGE_NOT_EXTENDED_COLUMN; + extraRows = addList( + cellListParameter, + currentRowData, + startColumn, + SpreadsheetUtil.DOWNLOAD_MESSAGE_COLUMNS, + projectInfo.getDownloaderMessages(), + (Row licenseRow, String message) -> { + Cell[] licenses = createDataCellsInRow(licenseRow, startColumn, cellStyle, message); + if (message.matches(SpreadsheetUtil.VALID_LINK)) { + addHyperlinkIfExists(wb, licenses[0], hyperlinkStyle, HyperlinkType.URL); + } + }); + } + currentRowIndex += extraRows + 1; + } + + autosizeColumns(sheet, hasExtendedInfo); + } + + private static CellStyle createHyperlinkStyle(XSSFWorkbook wb, XSSFColor backgroundColor) { + Font hyperlinkFont = wb.createFont(); + hyperlinkFont.setUnderline(Font.U_SINGLE); + hyperlinkFont.setColor(IndexedColors.BLUE.getIndex()); + XSSFCellStyle hyperlinkStyle = wb.createCellStyle(); + if (backgroundColor != null) { + hyperlinkStyle.setFillForegroundColor(backgroundColor); + hyperlinkStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + } + hyperlinkStyle.setFont(hyperlinkFont); + return hyperlinkStyle; + } + + private static void autosizeColumns(Sheet sheet, boolean hasExtendedInfo) { + autosizeColumns( + sheet, + new ImmutablePair<>(SpreadsheetUtil.GENERAL_START_COLUMN, SpreadsheetUtil.GENERAL_END_COLUMN), + new ImmutablePair<>(SpreadsheetUtil.PLUGIN_ID_START_COLUMN, SpreadsheetUtil.PLUGIN_ID_END_COLUMN), + new ImmutablePair<>(SpreadsheetUtil.LICENSES_START_COLUMN, SpreadsheetUtil.LICENSES_END_COLUMN), + new ImmutablePair<>(SpreadsheetUtil.DEVELOPERS_START_COLUMN, SpreadsheetUtil.DEVELOPERS_END_COLUMN - 1), + new ImmutablePair<>(SpreadsheetUtil.MISC_START_COLUMN + 1, SpreadsheetUtil.MISC_END_COLUMN)); + // The column header widths are most likely wider than the actual cells content. + sheet.setColumnWidth(SpreadsheetUtil.DEVELOPERS_END_COLUMN - 1, SpreadsheetUtil.TIMEZONE_WIDTH); + sheet.setColumnWidth(SpreadsheetUtil.MISC_START_COLUMN, SpreadsheetUtil.INCEPTION_YEAR_WIDTH); + if (hasExtendedInfo) { + autosizeColumns( + sheet, + new ImmutablePair<>(SpreadsheetUtil.MANIFEST_START_COLUMN, SpreadsheetUtil.MANIFEST_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_NOTICES_START_COLUMN + 2, SpreadsheetUtil.INFO_NOTICES_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_LICENSES_START_COLUMN + 2, SpreadsheetUtil.INFO_LICENSES_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_SPDX_START_COLUMN + 2, SpreadsheetUtil.INFO_SPDX_END_COLUMN)); + } + autosizeColumns( + sheet, + new ImmutablePair<>( + getDownloadColumn(hasExtendedInfo), + getDownloadColumn(hasExtendedInfo) + SpreadsheetUtil.DOWNLOAD_MESSAGE_COLUMNS)); + } + + @SafeVarargs + private static void autosizeColumns(Sheet sheet, Pair... ranges) { + for (Pair range : ranges) { + for (int i = range.getLeft(); i < range.getRight(); i++) { + sheet.autoSizeColumn(i); + } + } + } + + private static int addInfoFileList( + CellListParameter cellListParameter, + SpreadsheetUtil.CurrentRowData currentRowData, + int startColumn, + int columnsToFill, + List infoFiles) { + return addList( + cellListParameter, + currentRowData, + startColumn, + columnsToFill, + infoFiles, + (Row infoFileRow, InfoFile infoFile) -> { + final String copyrightLines = Optional.ofNullable(infoFile.getExtractedCopyrightLines()) + .map(strings -> String.join(SpreadsheetUtil.COPYRIGHT_JOIN_SEPARATOR, strings)) + .orElse(null); + createDataCellsInRow( + infoFileRow, + startColumn, + cellListParameter.getCellStyle(), + infoFile.getContent(), + copyrightLines, + infoFile.getFileName()); + }); + } + + private static int addList( + CellListParameter cellListParameter, + SpreadsheetUtil.CurrentRowData currentRowData, + int startColumn, + int columnsToFill, + List list, + BiConsumer biConsumer) { + if (!CollectionUtils.isEmpty(list)) { + for (int i = 0; i < list.size(); i++) { + T type = list.get(i); + Integer index = currentRowData.getCurrentRowIndex() + i; + Row row = cellListParameter.getRows().get(index); + if (row == null) { + row = cellListParameter.getSheet().createRow(index); + cellListParameter.getRows().put(index, row); + if (cellListParameter.getCellStyle() != null) { + // Style all empty left cells, in the columns left from this + createAndStyleCells( + row, + cellListParameter.getCellStyle(), + new ImmutablePair<>( + SpreadsheetUtil.GENERAL_START_COLUMN, SpreadsheetUtil.GENERAL_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.PLUGIN_ID_START_COLUMN, SpreadsheetUtil.PLUGIN_ID_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.LICENSES_START_COLUMN, SpreadsheetUtil.LICENSES_END_COLUMN)); + if (currentRowData.isHasExtendedInfo()) { + createAndStyleCells( + row, + cellListParameter.getCellStyle(), + new ImmutablePair<>( + SpreadsheetUtil.DEVELOPERS_START_COLUMN, + SpreadsheetUtil.DEVELOPERS_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.MISC_START_COLUMN, SpreadsheetUtil.MISC_END_COLUMN), + // JAR + new ImmutablePair<>( + SpreadsheetUtil.MANIFEST_START_COLUMN, SpreadsheetUtil.MANIFEST_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_LICENSES_START_COLUMN, + SpreadsheetUtil.INFO_LICENSES_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_NOTICES_START_COLUMN, + SpreadsheetUtil.INFO_NOTICES_END_COLUMN), + new ImmutablePair<>( + SpreadsheetUtil.INFO_SPDX_START_COLUMN, + SpreadsheetUtil.INFO_SPDX_END_COLUMN)); + } + } + currentRowData.setExtraRows(currentRowData.getExtraRows() + 1); + } + biConsumer.accept(row, type); + } + } else if (cellListParameter.cellStyle != null) { + setStyleOnEmptyCells(cellListParameter, currentRowData, startColumn, columnsToFill); + } + return currentRowData.getExtraRows(); + } + + /** + * If no cells are set, color at least the background, + * to color concatenated blocks with the same background color. + * + * @param cellListParameter Passes data about sheet, row, cell style. + * @param currentRowData Passes data about the current indices for rows and columns. + * @param startColumn Column where to start setting the style. + * @param columnsToFill How many columns to set the style on, starting from 'startColumn'. + */ + private static void setStyleOnEmptyCells( + CellListParameter cellListParameter, + SpreadsheetUtil.CurrentRowData currentRowData, + int startColumn, + int columnsToFill) { + Row row = cellListParameter.getRows().get(currentRowData.getCurrentRowIndex()); + for (int i = 0; i < columnsToFill; i++) { + Cell cell = row.createCell(startColumn + i, CellType.STRING); + cell.setCellStyle(cellListParameter.getCellStyle()); + } + } + + @SafeVarargs + private static void createAndStyleCells(Row row, CellStyle cellStyle, Pair... ranges) { + for (Pair range : ranges) { + for (int i = range.getLeft(); i < range.getRight(); i++) { + Cell cell = row.createCell(i, CellType.STRING); + cell.setCellStyle(cellStyle); + } + } + } + + private static void addHyperlinkIfExists( + Workbook workbook, Cell cell, CellStyle hyperlinkStyle, HyperlinkType hyperlinkType) { + final String link = cell.getStringCellValue(); + if (!StringUtils.isEmpty(link)) { + Hyperlink hyperlink = workbook.getCreationHelper().createHyperlink(hyperlinkType); + final String modifiedLink = prefixedHyperlink(hyperlinkType, link); + try { + hyperlink.setAddress(modifiedLink); + cell.setHyperlink(hyperlink); + cell.setCellStyle(hyperlinkStyle); + } catch (IllegalArgumentException e) { + LOG.debug( + "Can't set Hyperlink for cell value " + link + " (" + modifiedLink + + ") it gets rejected as URI", + e); + } + } + } + + /** + * Adds "https://" prefix to link if it's missing. + * + * @param hyperlinkType Type of hyperlink. + * @param link Hyperlink address. + * @return Prefixed hyperlink. + */ + private static String prefixedHyperlink(HyperlinkType hyperlinkType, String link) { + final String modifiedLink; + link = link.trim().replace(" dot ", "."); + if (hyperlinkType == HyperlinkType.EMAIL) { + // Replace all "bla com" with "bla.com". + link = link.replace(" at ", "@"); + if (link.contains("@") && link.matches(".*\\s[a-zA-Z]{2,3}$")) { + modifiedLink = link.replace(" ", "."); + } else { + modifiedLink = link; + } + } else if (!link.startsWith("http://") && !link.startsWith("https://")) { + modifiedLink = "https://" + link; + } else { + modifiedLink = link; + } + return modifiedLink; + } + + private static Cell[] createDataCellsInRow(Row row, int startColumn, CellStyle cellStyle, String... names) { + Cell[] result = new Cell[names.length]; + for (int i = 0; i < names.length; i++) { + Cell cell = row.createCell(startColumn + i, CellType.STRING); + if (cellStyle != null) { + cell.setCellStyle(cellStyle); + } + if (!StringUtils.isEmpty(names[i])) { + final String value; + final int maxCellStringLength = Short.MAX_VALUE; + if (names[i].length() > maxCellStringLength) { + value = names[i].substring(0, maxCellStringLength - 3) + "..."; + } else { + value = names[i]; + } + cell.setCellValue(value); + } + result[i] = cell; + } + return result; + } + + /** + * Create cells for InfoFile content. + * + * @param row The row to insert cells into. + * @param cellStyle The cell style of the created cell. + * @param startPositions The start position of the 3 columns for an InfoFile. + */ + private static void createInfoFileCellsInRow(Row row, CellStyle cellStyle, int... startPositions) { + for (int startPosition : startPositions) { + createCellsInRow(row, startPosition, cellStyle, "Content", "Extracted copyright lines", "File"); + } + } + + private static void createCellsInRow(Row row, int startColumn, CellStyle cellStyle, String... names) { + for (int i = 0; i < names.length; i++) { + Cell cell = row.createCell(startColumn + i, CellType.STRING); + cell.setCellStyle(cellStyle); + cell.setCellValue(names[i]); + } + } + + private static void createMergedCellsInRow( + Sheet sheet, int startColumn, int endColumn, CellStyle cellStyle, Row row, String cellValue, int rowIndex) { + Cell cell = createCellsInRow(startColumn, endColumn, row); + if (cell == null) { + return; + } + final boolean merge = endColumn - 1 > startColumn; + CellRangeAddress mergeAddress = null; + if (merge) { + mergeAddress = new CellRangeAddress(rowIndex, rowIndex, startColumn, endColumn - 1); + sheet.addMergedRegion(mergeAddress); + } + // Set value and style only after merge + cell.setCellValue(cellValue); + cell.setCellStyle(cellStyle); + if (merge) { + setBorderAroundRegion(sheet, mergeAddress, HEADER_CELLS_BORDER_STYLE); + sheet.groupColumn(startColumn, endColumn - 1); + } + } + + private static void setBorderAroundRegion( + Sheet sheet, CellRangeAddress licensesHeaderAddress, BorderStyle borderStyle) { + RegionUtil.setBorderLeft(borderStyle, licensesHeaderAddress, sheet); + RegionUtil.setBorderTop(borderStyle, licensesHeaderAddress, sheet); + RegionUtil.setBorderRight(borderStyle, licensesHeaderAddress, sheet); + RegionUtil.setBorderBottom(borderStyle, licensesHeaderAddress, sheet); + } + + private static Cell createCellsInRow(int startColumn, int exclusiveEndColumn, Row inRow) { + Cell firstCell = null; + for (int i = startColumn; i < exclusiveEndColumn; i++) { + Cell cell = inRow.createCell(i); + if (i == startColumn) { + firstCell = cell; + } + } + return firstCell; + } + + private static void setBorderStyle(CellStyle cellStyle, BorderStyle borderStyle) { + cellStyle.setBorderLeft(borderStyle); + cellStyle.setBorderTop(borderStyle); + cellStyle.setBorderRight(borderStyle); + cellStyle.setBorderBottom(borderStyle); + } + + /** + * Parameters for cells which apply to all cells in each loop iteration. + */ + private static class CellListParameter { + private final Sheet sheet; + private final Map rows; + private final CellStyle cellStyle; + + private CellListParameter(Sheet sheet, Map rows, CellStyle cellStyle) { + this.sheet = sheet; + this.rows = rows; + this.cellStyle = cellStyle; + } + + Sheet getSheet() { + return sheet; + } + + Map getRows() { + return rows; + } + + CellStyle getCellStyle() { + return cellStyle; + } + } +} diff --git a/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/SpreadsheetUtil.java b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/SpreadsheetUtil.java new file mode 100644 index 000000000..fad3da3ca --- /dev/null +++ b/src/main/java/org/codehaus/mojo/license/extended/spreadsheet/SpreadsheetUtil.java @@ -0,0 +1,111 @@ +package org.codehaus.mojo.license.extended.spreadsheet; + +/** + * Utility class to build spreadsheets. + */ +class SpreadsheetUtil { + static final String TABLE_NAME = "License information"; + static final String VALID_LINK = "\\bhttps?://\\S+"; + + // Columns values for Maven data, including separator gaps for data grouping. + static final int GENERAL_START_COLUMN = 0; + private static final int GENERAL_COLUMNS = 1; + static final int GENERAL_END_COLUMN = GENERAL_START_COLUMN + GENERAL_COLUMNS; + static final int PLUGIN_ID_START_COLUMN = GENERAL_END_COLUMN + 1; + private static final int PLUGIN_ID_COLUMNS = 3; + static final int PLUGIN_ID_END_COLUMN = PLUGIN_ID_START_COLUMN + PLUGIN_ID_COLUMNS; + // "Start" column are the actual start columns, they are inclusive. + // "End" columns point just one column behind the last one, it's the exclusive column index. + static final int LICENSES_START_COLUMN = PLUGIN_ID_END_COLUMN + 1; + static final int LICENSES_COLUMNS = 5; + static final int LICENSES_END_COLUMN = LICENSES_START_COLUMN + LICENSES_COLUMNS; + static final int DEVELOPERS_START_COLUMN = LICENSES_END_COLUMN + 1; + static final int DEVELOPERS_COLUMNS = 7; + static final int DEVELOPERS_END_COLUMN = DEVELOPERS_START_COLUMN + DEVELOPERS_COLUMNS; + static final int MISC_START_COLUMN = DEVELOPERS_END_COLUMN + 1; + static final int MISC_COLUMNS = 4; + private static final int MAVEN_DATA_COLUMNS = + GENERAL_COLUMNS + PLUGIN_ID_COLUMNS + LICENSES_COLUMNS + DEVELOPERS_COLUMNS + MISC_COLUMNS; + static final int MISC_END_COLUMN = MISC_START_COLUMN + MISC_COLUMNS; + private static final int MAVEN_COLUMN_GROUPING_GAPS = 4; + private static final int MAVEN_COLUMNS = MAVEN_DATA_COLUMNS + MAVEN_COLUMN_GROUPING_GAPS; + static final int MANIFEST_START_COLUMN = MAVEN_COLUMNS + 1; + static final int MAVEN_START_COLUMN = 0; + static final int MAVEN_END_COLUMN = MAVEN_START_COLUMN + MAVEN_COLUMNS; + static final int EXTENDED_INFO_START_COLUMN = MAVEN_END_COLUMN + 1; + // Columns values for JAR data, including separator gaps for data grouping. + private static final int INFO_FILES_GAPS = 2; + private static final int MANIFEST_GAPS = 1; + private static final int MANIFEST_COLUMNS = 3; + static final int MANIFEST_END_COLUMN = MANIFEST_START_COLUMN + MANIFEST_COLUMNS; + static final int INFO_NOTICES_START_COLUMN = MANIFEST_END_COLUMN + 1; + static final int INFO_NOTICES_COLUMNS = 3; + static final int INFO_NOTICES_END_COLUMN = INFO_NOTICES_START_COLUMN + INFO_NOTICES_COLUMNS; + static final int INFO_LICENSES_START_COLUMN = INFO_NOTICES_END_COLUMN + 1; + static final int INFO_LICENSES_COLUMNS = 3; + static final int INFO_LICENSES_END_COLUMN = INFO_LICENSES_START_COLUMN + INFO_LICENSES_COLUMNS; + static final int INFO_SPDX_START_COLUMN = INFO_LICENSES_END_COLUMN + 1; + static final int INFO_SPDX_COLUMNS = 3; + private static final int EXTENDED_INFO_COLUMNS = MANIFEST_COLUMNS + + INFO_NOTICES_COLUMNS + + INFO_LICENSES_COLUMNS + + INFO_SPDX_COLUMNS + + INFO_FILES_GAPS + + MANIFEST_GAPS; + static final int EXTENDED_INFO_END_COLUMN = EXTENDED_INFO_START_COLUMN + EXTENDED_INFO_COLUMNS; + static final int INFO_SPDX_END_COLUMN = INFO_SPDX_START_COLUMN + INFO_SPDX_COLUMNS; + + static final int DOWNLOAD_MESSAGE_EXTENDED_COLUMN = INFO_SPDX_END_COLUMN + 1; + static final int DOWNLOAD_MESSAGE_NOT_EXTENDED_COLUMN = MANIFEST_START_COLUMN; + static final int DOWNLOAD_MESSAGE_COLUMNS = 1; + + // Width of gap columns + private static final int EXCEL_WIDTH_SCALE = 256; + static final int INCEPTION_YEAR_WIDTH = " Inception Year ".length() * EXCEL_WIDTH_SCALE; + static final int TIMEZONE_WIDTH = " Timezone ".length() * EXCEL_WIDTH_SCALE; + static final int GAP_WIDTH = 3 * EXCEL_WIDTH_SCALE; + /** + * Color must be dark enough for low-contrast monitors. + *
If you get a compiler error here, make sure you're using Java 8, not higher. + */ + static final int[] ALTERNATING_ROWS_COLOR = new int[] {220, 220, 220}; + + static final String COPYRIGHT_JOIN_SEPARATOR = "§"; + + static int getDownloadColumn(boolean hasExtendedInfo) { + return hasExtendedInfo + ? SpreadsheetUtil.DOWNLOAD_MESSAGE_EXTENDED_COLUMN + : SpreadsheetUtil.DOWNLOAD_MESSAGE_NOT_EXTENDED_COLUMN; + } + + /** + * Parameters which may change constantly. + */ + static class CurrentRowData { + private final int currentRowIndex; + private int extraRows; + private final boolean hasExtendedInfo; + + CurrentRowData(int currentRowIndex, int extraRows, boolean hasExtendedInfo) { + this.currentRowIndex = currentRowIndex; + this.extraRows = extraRows; + this.hasExtendedInfo = hasExtendedInfo; + } + + int getCurrentRowIndex() { + return currentRowIndex; + } + + int getExtraRows() { + return extraRows; + } + + void setExtraRows(int extraRows) { + this.extraRows = extraRows; + } + + boolean isHasExtendedInfo() { + return hasExtendedInfo; + } + } +} diff --git a/src/main/java/org/codehaus/mojo/license/model/LicenseMap.java b/src/main/java/org/codehaus/mojo/license/model/LicenseMap.java index db66a654e..f1375c6f2 100644 --- a/src/main/java/org/codehaus/mojo/license/model/LicenseMap.java +++ b/src/main/java/org/codehaus/mojo/license/model/LicenseMap.java @@ -40,6 +40,11 @@ /** * Map of artifacts (stub in mavenproject) grouped by their license. * + *

    + *
  • key is the license on which to associate the given project.
  • + *
  • value list of projects belonging to the license.
  • + *
+ * * @author tchemit dev@tchemit.fr * @since 1.0 */ @@ -52,7 +57,7 @@ public class LicenseMap extends TreeMap> { private final Comparator projectComparator; /** - * Default contructor. + * Default constructor. */ public LicenseMap() { this(MojoHelper.newMavenProjectComparator()); diff --git a/src/main/java/org/codehaus/mojo/license/utils/StringToList.java b/src/main/java/org/codehaus/mojo/license/utils/StringToList.java index a34ef6bee..dc6446929 100644 --- a/src/main/java/org/codehaus/mojo/license/utils/StringToList.java +++ b/src/main/java/org/codehaus/mojo/license/utils/StringToList.java @@ -37,6 +37,10 @@ * @since 1.4 */ public class StringToList { + /** + * Regular expression to split license list. + */ + public static final String LIST_OF_LICENSES_REG_EX = "\\s*\\|\\s*"; /** * List of data. @@ -53,7 +57,7 @@ public StringToList() { public StringToList(String data) throws MojoExecutionException { this(); if (!UrlRequester.isStringUrl(data)) { - for (String s : data.split("\\s*\\|\\s*")) { + for (String s : data.split(LIST_OF_LICENSES_REG_EX)) { addEntryToList(s); } } else { diff --git a/src/main/resources/org/codehaus/mojo/license/licenses.xsd b/src/main/resources/org/codehaus/mojo/license/licenses.xsd index 6157420fa..978433e65 100644 --- a/src/main/resources/org/codehaus/mojo/license/licenses.xsd +++ b/src/main/resources/org/codehaus/mojo/license/licenses.xsd @@ -22,109 +22,234 @@ --> - - - - - - - - - - - - - - - - - - - - - - - - - - - 1.0+ - - This element describes all of the licenses for this project. Each license is described by a - license element, which is then described by additional elements. Projects should - only list the license(s) that applies to the project and not the licenses that apply to - dependencies. If multiple licenses are listed, it is assumed that the user can select - any of them, not that they must accept all. - - - - - - - - - - 3.0.0+ - - - Describes the licenses for this project. This is used to generate - the license page of the project's web site, as well as being taken into - consideration in other reporting and - validation. The licenses listed for the project are that of the project itself, - and not of dependencies. - - - - - - - 3.0.0+ - The full legal name of - the license. - - - - - 3.0.0+ - The official url for - the license text. - - - - - 3.0.0+ - - - The primary method by which this project may be distributed. -
-
repo
-
may be downloaded from the Maven repository
-
manual
-
user must manually download and install the dependency.
-
- -
-
-
- - - 3.0.0+ - A name of the license file - (without path) downloaded from {@link #url}. This path is - relative to the licenses.xml file. - - - - - 3.0.0+ - - Addendum information pertaining to this license. - - + elementFormDefault="qualified"> + + + + + + + -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.0+ + + This element describes all of the licenses for this project. Each license is described by a + license + element, which is then described by additional elements. Projects should + only list the license(s) that applies to the project and not the licenses that apply to + dependencies. If multiple licenses are listed, it is assumed that the user can select + any of them, not that they must accept all. + + + + + + + + + + 3.0.0+ + + + Describes the licenses for this project. This is used to generate + the license page of the project's web site, as well as being taken into + consideration in other reporting and + validation. The licenses listed for the project are that of the project itself, + and not of dependencies. + + + + + + + 3.0.0+ + The full legal name of + the license. + + + + + + 3.0.0+ + The official url for + the license text. + + + + + + 3.0.0+ + + + The primary method by which this project may be distributed. +
+
repo
+
may be downloaded from the Maven repository
+
manual
+
user must manually download and install the dependency.
+
+ +
+
+
+ + + 3.0.0+ + A name of the license file + (without path) downloaded from {@link #url}. This path is + relative to the licenses.xml file. + + + + + + 3.0.0+ + + Addendum information pertaining to this license. + + + +
+
+ + + + + + Downloader messages, probably errors + + + + + + + + + + + + + Information about the dependency developers + + + + + + + + + + + + + + Information about the dependency developer + + + + + + + + + + + + + + + + + + + Information extracted from files in the JAR. + + + + + + + + + + + + + + + + + + + + + Information extracted from a file in the JAR. + + + + + + + + + + + + + + + + Copyright lines extracted from the file in the JAR. + + + + + + + + + + + + + Organization info + + + + + + + +
diff --git a/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java b/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java index ba18673ad..28c758840 100644 --- a/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java +++ b/src/test/java/org/codehaus/mojo/license/download/LicenseMatchersTest.java @@ -70,7 +70,7 @@ public void licenseMatches() { @Test public void replaceMatchesLegacy() { - final ProjectLicenseInfo dep = new ProjectLicenseInfo("myGroup", "myArtifact", "1a2.3"); + final ProjectLicenseInfo dep = new ProjectLicenseInfo("myGroup", "myArtifact", "1a2.3", null); 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); @@ -91,7 +91,7 @@ public void replaceMatchesLegacy() { @Test public void replaceMatches() { - final ProjectLicenseInfo dep = new ProjectLicenseInfo("myGroup", "myArtifact", "1.2.3"); + final ProjectLicenseInfo dep = new ProjectLicenseInfo("myGroup", "myArtifact", "1.2.3", null); final DependencyMatcher m0 = DependencyMatcher.of(new ProjectLicenseInfo("myGroup", "myArtifact", null, true)); Assert.assertTrue(m0.matches(dep)); @@ -126,7 +126,7 @@ public void replaceMatches() { 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"); + final ProjectLicenseInfo dep11 = new ProjectLicenseInfo("myGroup", "myArtifact", "1.2.3", null); 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); 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 34889ab84..bea9a9892 100644 --- a/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java +++ b/src/test/java/org/codehaus/mojo/license/download/LicenseSummaryTest.java @@ -1,8 +1,14 @@ package org.codehaus.mojo.license.download; +import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; import java.io.File; import java.io.FileInputStream; @@ -10,18 +16,37 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.model.Developer; +import org.apache.maven.model.Organization; +import org.apache.maven.model.Scm; import org.codehaus.mojo.license.Eol; +import org.codehaus.mojo.license.extended.ExtendedInfo; +import org.codehaus.mojo.license.extended.InfoFile; +import org.codehaus.mojo.license.extended.spreadsheet.CalcFileWriter; +import org.codehaus.mojo.license.extended.spreadsheet.ExcelFileWriter; import org.junit.Assert; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; +import static org.codehaus.mojo.license.download.LicenseSummaryWriter.LICENSE_PATH; +import static org.junit.Assert.assertTrue; + /** * @since 1.0 */ public class LicenseSummaryTest { + private static final Logger LOG = LoggerFactory.getLogger(LicenseSummaryTest.class); /** * Test reading the license summary xml file into ProjectLicenseInfo objects @@ -33,7 +58,7 @@ public class LicenseSummaryTest { @Test public void testReadLicenseSummary() throws IOException, SAXException, ParserConfigurationException { File licenseSummaryFile = new File("src/test/resources/license-summary-test.xml"); - Assert.assertTrue(licenseSummaryFile.exists()); + assertTrue(licenseSummaryFile.exists()); List list; try (InputStream fis = Files.newInputStream(licenseSummaryFile.toPath())) { list = LicenseSummaryReader.parseLicenseSummary(fis); @@ -67,8 +92,10 @@ public void testWriteReadLicenseSummary() throws IOException, SAXException, ParserConfigurationException, TransformerFactoryConfigurationError, TransformerException { List licSummary = new ArrayList<>(); - ProjectLicenseInfo dep1 = new ProjectLicenseInfo("org.test", "test1", "1.0"); - ProjectLicenseInfo dep2 = new ProjectLicenseInfo("org.test", "test2", "2.0"); + ProjectLicenseInfo dep1 = new ProjectLicenseInfo("org.test", "test1", "1.0", buildExtendedInfo(1)); + ProjectLicenseInfo dep2 = new ProjectLicenseInfo("org.test", "test2", "2.0", buildExtendedInfo(2)); + ProjectLicenseInfo dep3 = new ProjectLicenseInfo("com.test", "test3", "3.0", buildExtendedInfo(3)); + ProjectLicenseInfo dep4 = new ProjectLicenseInfo("dk.test", "test4", "4.0", buildExtendedInfo(4)); ProjectLicense lic = new ProjectLicense(); lic.setName("lgpl"); @@ -77,16 +104,23 @@ public void testWriteReadLicenseSummary() lic.setComments("lgpl version 3.0"); dep1.addLicense(lic); dep2.addLicense(lic); + dep3.addLicense(lic); + + dep2.addDownloaderMessage("There were server problems"); + // Skip dependency 3, to test correct empty cell filling of ODS export. + dep4.addDownloaderMessage("http://google.de"); licSummary.add(dep1); licSummary.add(dep2); + licSummary.add(dep3); + licSummary.add(dep4); { File licenseSummaryFile = File.createTempFile("licSummary", "tmp"); LicenseSummaryWriter.writeLicenseSummary( licSummary, licenseSummaryFile, StandardCharsets.UTF_8, Eol.LF, true); - Assert.assertTrue(licenseSummaryFile.exists()); + assertTrue(licenseSummaryFile.exists()); FileInputStream fis = new FileInputStream(licenseSummaryFile); List list = LicenseSummaryReader.parseLicenseSummary(fis); fis.close(); @@ -102,14 +136,16 @@ public void testWriteReadLicenseSummary() Assert.assertEquals("http://www.gnu.org/licenses/lgpl-3.0.txt", lic0.getUrl()); Assert.assertEquals("lgpl-3.0.txt", lic0.getFile()); Assert.assertEquals("lgpl version 3.0", lic0.getComments()); + + validateXml(licenseSummaryFile); } { - File licenseSummaryFile = File.createTempFile("licSummaryNoVersion", "tmp"); + File licenseSummaryFile = File.createTempFile("licSummaryNoVersionNoXsd", "tmp"); LicenseSummaryWriter.writeLicenseSummary( licSummary, licenseSummaryFile, StandardCharsets.UTF_8, Eol.LF, false); - Assert.assertTrue(licenseSummaryFile.exists()); + assertTrue(licenseSummaryFile.exists()); FileInputStream fis = new FileInputStream(licenseSummaryFile); List list = LicenseSummaryReader.parseLicenseSummary(fis); fis.close(); @@ -125,7 +161,92 @@ public void testWriteReadLicenseSummary() Assert.assertEquals("http://www.gnu.org/licenses/lgpl-3.0.txt", lic0.getUrl()); Assert.assertEquals("lgpl-3.0.txt", lic0.getFile()); Assert.assertEquals("lgpl version 3.0", lic0.getComments()); + + validateXml(licenseSummaryFile); } + + Path licensesExcelOutputFile = Files.createTempFile("licExcel", ".xlsx"); + ExcelFileWriter.write(licSummary, licensesExcelOutputFile.toFile()); + + Path licensesCalcOutputFile = Files.createTempFile("licCalc", ".ods"); + CalcFileWriter.write(licSummary, licensesCalcOutputFile.toFile()); + } + + /** + * Validate XML against XSD. + * + * @param licenseSummaryFile License summary file. + * @throws SAXException SAX exception, validation problem. + * @throws IOException I/O exception, file problem. + */ + private static void validateXml(File licenseSummaryFile) throws SAXException, IOException { + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + try (InputStream inputStream = LicenseSummaryTest.class.getResourceAsStream(LICENSE_PATH)) { + Source schemaSource = new StreamSource(inputStream); + Schema schema = schemaFactory.newSchema(schemaSource); + Validator validator = schema.newValidator(); + Source xmlSource = new StreamSource(licenseSummaryFile); + validator.validate(xmlSource); + } + } + + private static ExtendedInfo buildExtendedInfo(int suffix) { + ExtendedInfo extendedInfo = new ExtendedInfo(); + Artifact artifact = new DefaultArtifact( + "org.test", "test" + suffix, "2.0", "compile", "jar", null, new DefaultArtifactHandler()); + extendedInfo.setArtifact(artifact); + extendedInfo.setBundleLicense("Bundle Test License " + suffix); + extendedInfo.setBundleVendor("Bundle Test Vendor " + suffix); + + List developers = new ArrayList<>(); + Developer developer = createDeveloper(suffix); + developers.add(developer); + extendedInfo.setDevelopers(developers); + extendedInfo.setImplementationVendor("Implementation vendor " + suffix); + extendedInfo.setInceptionYear((2021 + suffix) + ""); + + List infoFiles = new ArrayList<>(); + for (InfoFile.Type license : InfoFile.Type.values()) { + infoFiles.add(createInfoFile(license, suffix)); + } + extendedInfo.setInfoFiles(infoFiles); + + extendedInfo.setName("Test Project " + suffix); + + Organization organization = new Organization(); + organization.setName("Test Organization " + suffix); + organization.setUrl("www.github.com/" + suffix); + extendedInfo.setOrganization(organization); + + Scm scm = new Scm(); + scm.setUrl("www.github.com/" + suffix); + extendedInfo.setScm(scm); + + extendedInfo.setUrl("www.google.de/" + suffix); + return extendedInfo; + } + + private static InfoFile createInfoFile(InfoFile.Type noticeType, int suffix) { + InfoFile infoFile = new InfoFile(); + infoFile.setContent("This is " + noticeType.name() + " test content " + suffix); + infoFile.setExtractedCopyrightLines( + new HashSet<>(Collections.singletonList("Test " + noticeType.name() + suffix))); + infoFile.setFileName(noticeType.name() + " " + suffix + ".txt"); + infoFile.setType(noticeType); + return infoFile; + } + + private static Developer createDeveloper(int suffix) { + Developer developer = new Developer(); + developer.setEmail("developer" + suffix + "@google.com "); + developer.setId("developer.id " + suffix); + developer.setName("Top developer " + suffix); + developer.setOrganization("Developer Organization " + suffix); + developer.setOrganizationUrl("Test Organization " + suffix); + developer.setRoles(Collections.singletonList("Lead Developer " + suffix)); + developer.setTimezone("UTC+2"); + developer.setUrl("www.github.com"); + return developer; } @Test