|  | @@ -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;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 |