|
@@ -0,0 +1,258 @@
|
|
|
+/*
|
|
|
+ * Licensed to Elasticsearch under one or more contributor
|
|
|
+ * license agreements. See the NOTICE file distributed with
|
|
|
+ * this work for additional information regarding copyright
|
|
|
+ * ownership. Elasticsearch licenses this file to you under
|
|
|
+ * the Apache License, Version 2.0 (the "License"); you may
|
|
|
+ * not use this file except in compliance with the License.
|
|
|
+ * You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing,
|
|
|
+ * software distributed under the License is distributed on an
|
|
|
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
+ * KIND, either express or implied. See the License for the
|
|
|
+ * specific language governing permissions and limitations
|
|
|
+ * under the License.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.elasticsearch.gradle.internal.precommit;
|
|
|
+
|
|
|
+import org.apache.rat.Defaults;
|
|
|
+import org.apache.rat.ReportConfiguration;
|
|
|
+import org.apache.rat.analysis.IHeaderMatcher;
|
|
|
+import org.apache.rat.analysis.util.HeaderMatcherMultiplexer;
|
|
|
+import org.apache.rat.anttasks.SubstringLicenseMatcher;
|
|
|
+import org.apache.rat.api.RatException;
|
|
|
+import org.apache.rat.document.impl.FileDocument;
|
|
|
+import org.apache.rat.license.SimpleLicenseFamily;
|
|
|
+import org.apache.rat.report.RatReport;
|
|
|
+import org.apache.rat.report.claim.ClaimStatistic;
|
|
|
+import org.apache.rat.report.xml.XmlReportFactory;
|
|
|
+import org.apache.rat.report.xml.writer.impl.base.XmlWriter;
|
|
|
+import org.gradle.api.DefaultTask;
|
|
|
+import org.gradle.api.GradleException;
|
|
|
+import org.gradle.api.file.FileCollection;
|
|
|
+import org.gradle.api.provider.ListProperty;
|
|
|
+import org.gradle.api.tasks.CacheableTask;
|
|
|
+import org.gradle.api.tasks.Input;
|
|
|
+import org.gradle.api.tasks.InputFiles;
|
|
|
+import org.gradle.api.tasks.Internal;
|
|
|
+import org.gradle.api.tasks.OutputFile;
|
|
|
+import org.gradle.api.tasks.PathSensitive;
|
|
|
+import org.gradle.api.tasks.PathSensitivity;
|
|
|
+import org.gradle.api.tasks.SkipWhenEmpty;
|
|
|
+import org.gradle.api.tasks.TaskAction;
|
|
|
+import org.w3c.dom.Element;
|
|
|
+import org.w3c.dom.NodeList;
|
|
|
+import org.xml.sax.SAXException;
|
|
|
+
|
|
|
+import javax.xml.parsers.DocumentBuilderFactory;
|
|
|
+import javax.xml.parsers.ParserConfigurationException;
|
|
|
+import java.io.BufferedWriter;
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileWriter;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.Writer;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Checks files for license headers..
|
|
|
+ */
|
|
|
+@CacheableTask
|
|
|
+public abstract class LicenseHeadersTask extends DefaultTask {
|
|
|
+ public LicenseHeadersTask() {
|
|
|
+ setDescription("Checks sources for missing, incorrect, or unacceptable license headers");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The list of java files to check. protected so the afterEvaluate closure in the
|
|
|
+ * constructor can write to it.
|
|
|
+ */
|
|
|
+ @InputFiles
|
|
|
+ @SkipWhenEmpty
|
|
|
+ @PathSensitive(PathSensitivity.RELATIVE)
|
|
|
+ public List<FileCollection> getJavaFiles() {
|
|
|
+ return getSourceFolders().get();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Internal
|
|
|
+ public abstract ListProperty<FileCollection> getSourceFolders();
|
|
|
+
|
|
|
+ public File getReportFile() {
|
|
|
+ return reportFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setReportFile(File reportFile) {
|
|
|
+ this.reportFile = reportFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<String> getApprovedLicenses() {
|
|
|
+ return approvedLicenses;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setApprovedLicenses(List<String> approvedLicenses) {
|
|
|
+ this.approvedLicenses = approvedLicenses;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<String> getExcludes() {
|
|
|
+ return excludes;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, String> getAdditionalLicenses() {
|
|
|
+ return additionalLicenses;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setExcludes(List<String> excludes) {
|
|
|
+ this.excludes = excludes;
|
|
|
+ }
|
|
|
+
|
|
|
+ @OutputFile
|
|
|
+ private File reportFile = new File(getProject().getBuildDir(), "reports/licenseHeaders/rat.xml");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Allowed license families for this project.
|
|
|
+ */
|
|
|
+ @Input
|
|
|
+ private List<String> approvedLicenses = new ArrayList<String>(Arrays.asList("Apache", "Generated", "Vendored"));
|
|
|
+ /**
|
|
|
+ * Files that should be excluded from the license header check. Use with extreme care, only in situations where the license on the
|
|
|
+ * source file is compatible with the codebase but we do not want to add the license to the list of approved headers (to avoid the
|
|
|
+ * possibility of inadvertently using the license on our own source files).
|
|
|
+ */
|
|
|
+ @Input
|
|
|
+ private List<String> excludes = new ArrayList<String>();
|
|
|
+ /**
|
|
|
+ * Additional license families that may be found. The key is the license category name (5 characters),
|
|
|
+ * followed by the family name and the value list of patterns to search for.
|
|
|
+ */
|
|
|
+ @Input
|
|
|
+ protected Map<String, String> additionalLicenses = new HashMap<String, String>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a new license type.
|
|
|
+ * <p>
|
|
|
+ * The license may be added to the {@link #approvedLicenses} using the {@code familyName}.
|
|
|
+ *
|
|
|
+ * @param categoryName A 5-character string identifier for the license
|
|
|
+ * @param familyName An expanded string name for the license
|
|
|
+ * @param pattern A pattern to search for, which if found, indicates a file contains the license
|
|
|
+ */
|
|
|
+ public void additionalLicense(final String categoryName, String familyName, String pattern) {
|
|
|
+ if (categoryName.length() != 5) {
|
|
|
+ throw new IllegalArgumentException("License category name must be exactly 5 characters, got " + categoryName);
|
|
|
+ }
|
|
|
+
|
|
|
+ additionalLicenses.put(categoryName + familyName, pattern);
|
|
|
+ }
|
|
|
+
|
|
|
+ @TaskAction
|
|
|
+ public void runRat() {
|
|
|
+ ReportConfiguration reportConfiguration = new ReportConfiguration();
|
|
|
+ reportConfiguration.setAddingLicenses(true);
|
|
|
+ List<IHeaderMatcher> matchers = new ArrayList<>();
|
|
|
+ matchers.add(Defaults.createDefaultMatcher());
|
|
|
+
|
|
|
+ // BSD 4-clause stuff (is disallowed below)
|
|
|
+ // we keep this here, in case someone adds BSD code for some reason, it should never be allowed.
|
|
|
+ matchers.add(subStringMatcher("BSD4 ", "Original BSD License (with advertising clause)", "All advertising materials"));
|
|
|
+ // Apache
|
|
|
+ matchers.add(subStringMatcher("AL ", "Apache", "Licensed to Elasticsearch under one or more contributor"));
|
|
|
+ // Generated resources
|
|
|
+ matchers.add(subStringMatcher("GEN ", "Generated", "ANTLR GENERATED CODE"));
|
|
|
+ // Vendored Code
|
|
|
+ matchers.add(subStringMatcher("VEN ", "Vendored", "@notice"));
|
|
|
+
|
|
|
+ for (Map.Entry<String, String> additional : additionalLicenses.entrySet()) {
|
|
|
+ String category = additional.getKey().substring(0, 5);
|
|
|
+ String family = additional.getKey().substring(5);
|
|
|
+ matchers.add(subStringMatcher(category, family, additional.getValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ reportConfiguration.setHeaderMatcher(new HeaderMatcherMultiplexer(matchers.toArray(IHeaderMatcher[]::new)));
|
|
|
+ reportConfiguration.setApprovedLicenseNames(approvedLicenses.stream().map(license -> {
|
|
|
+ SimpleLicenseFamily simpleLicenseFamily = new SimpleLicenseFamily();
|
|
|
+ simpleLicenseFamily.setFamilyName(license);
|
|
|
+ return simpleLicenseFamily;
|
|
|
+ }).toArray(SimpleLicenseFamily[]::new));
|
|
|
+
|
|
|
+ ClaimStatistic stats = generateReport(reportConfiguration, getReportFile());
|
|
|
+ boolean unknownLicenses = stats.getNumUnknown() > 0;
|
|
|
+ boolean unApprovedLicenses = stats.getNumUnApproved() > 0;
|
|
|
+ if (unknownLicenses || unApprovedLicenses) {
|
|
|
+ unapprovedFiles(getReportFile()).stream().forEachOrdered(unapprovedFile -> getLogger().error(unapprovedFile));
|
|
|
+ throw new GradleException("License header problems were found! Full details: " + reportFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IHeaderMatcher subStringMatcher(String licenseFamilyCategory, String licenseFamilyName, String substringPattern) {
|
|
|
+ SubstringLicenseMatcher substringLicenseMatcher = new SubstringLicenseMatcher();
|
|
|
+ substringLicenseMatcher.setLicenseFamilyCategory(licenseFamilyCategory);
|
|
|
+ substringLicenseMatcher.setLicenseFamilyName(licenseFamilyName);
|
|
|
+
|
|
|
+ SubstringLicenseMatcher.Pattern pattern = new SubstringLicenseMatcher.Pattern();
|
|
|
+ pattern.setSubstring(substringPattern);
|
|
|
+ substringLicenseMatcher.addConfiguredPattern(pattern);
|
|
|
+ return substringLicenseMatcher;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ClaimStatistic generateReport(ReportConfiguration config, File xmlReportFile) {
|
|
|
+ try {
|
|
|
+ Files.deleteIfExists(reportFile.toPath());
|
|
|
+ BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(xmlReportFile));
|
|
|
+ return toXmlReportFile(config, bufferedWriter);
|
|
|
+ } catch (IOException | RatException exception) {
|
|
|
+ throw new GradleException("Cannot generate license header report for " + getPath(), exception);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private ClaimStatistic toXmlReportFile(ReportConfiguration config, Writer writer) throws RatException, IOException {
|
|
|
+ ClaimStatistic stats = new ClaimStatistic();
|
|
|
+ RatReport standardReport = XmlReportFactory.createStandardReport(new XmlWriter(writer), stats, config);
|
|
|
+
|
|
|
+ standardReport.startReport();
|
|
|
+ for (FileCollection dirSet : getSourceFolders().get()) {
|
|
|
+ for (File f : dirSet.getAsFileTree().matching(patternFilterable -> patternFilterable.exclude(getExcludes()))) {
|
|
|
+ standardReport.report(new FileDocument(f));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ standardReport.endReport();
|
|
|
+ writer.flush();
|
|
|
+ writer.close();
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<String> unapprovedFiles(File xmlReportFile) {
|
|
|
+ try {
|
|
|
+ NodeList resourcesNodes = DocumentBuilderFactory.newInstance()
|
|
|
+ .newDocumentBuilder()
|
|
|
+ .parse(xmlReportFile)
|
|
|
+ .getElementsByTagName("resource");
|
|
|
+ return elementList(resourcesNodes).stream()
|
|
|
+ .filter(
|
|
|
+ resource -> elementList(resource.getChildNodes()).stream()
|
|
|
+ .anyMatch(n -> n.getTagName().equals("license-approval") && n.getAttribute("name").equals("false"))
|
|
|
+ )
|
|
|
+ .map(e -> e.getAttribute("name"))
|
|
|
+ .sorted()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ } catch (SAXException | IOException | ParserConfigurationException e) {
|
|
|
+ throw new GradleException("Error parsing xml report " + xmlReportFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Element> elementList(NodeList resourcesNodes) {
|
|
|
+ List<Element> nodeList = new ArrayList<>(resourcesNodes.getLength());
|
|
|
+ for (int idx = 0; idx < resourcesNodes.getLength(); idx++) {
|
|
|
+ nodeList.add((Element) resourcesNodes.item(idx));
|
|
|
+ }
|
|
|
+ return nodeList;
|
|
|
+ }
|
|
|
+}
|