| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed 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 com.googlesource.gerrit.plugins.copyright; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.server.config.PluginConfig; |
| import com.google.gerrit.server.git.validators.CommitValidationMessage; |
| import com.google.gerrit.server.git.validators.ValidationMessage; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import com.googlesource.gerrit.plugins.copyright.lib.CopyrightPatterns; |
| import com.googlesource.gerrit.plugins.copyright.lib.CopyrightPatterns.UnknownPatternName; |
| import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.LinkedHashSet; |
| import java.util.Objects; |
| import java.util.function.Consumer; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| /** Configuration state for {@link CopyrightValidator} from All-Projects project.config file. */ |
| class ScannerConfig { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public static final String KEY_ENABLE = "enable"; |
| |
| static final String KEY_TIME_TEST_MAX = "timeTestMax"; |
| static final String DEFAULT_REVIEW_LABEL = "Copyright-Review"; |
| static final String KEY_REVIEWER = "reviewer"; |
| static final String KEY_CC = "cc"; |
| static final String KEY_FROM = "fromAccountId"; |
| static final String KEY_REVIEW_LABEL = "reviewLabel"; |
| static final String KEY_EXCLUDE = "exclude"; |
| static final String KEY_FIRST_PARTY = "firstParty"; |
| static final String KEY_THIRD_PARTY = "thirdParty"; |
| static final String KEY_FORBIDDEN = "forbidden"; |
| static final String KEY_EXCLUDE_PATTERN = "excludePattern"; |
| static final String KEY_FIRST_PARTY_PATTERN = "firstPartyPattern"; |
| static final String KEY_THIRD_PARTY_PATTERN = "thirdPartyPattern"; |
| static final String KEY_FORBIDDEN_PATTERN = "forbiddenPattern"; |
| static final String KEY_ALWAYS_REVIEW_PATH = "alwaysReviewPath"; |
| static final String KEY_MATCH_PROJECTS = "matchProjects"; |
| static final String KEY_EXCLUDE_PROJECTS = "excludeProjects"; |
| static final String KEY_THIRD_PARTY_ALLOWED_PROJECTS = "thirdPartyAllowedProjects"; |
| private static final String OWNER = "owner "; |
| private static final String LICENSE = "license "; |
| |
| String pluginName; |
| CopyrightScanner scanner; |
| String patternSignature; |
| final ArrayList<CommitValidationMessage> messages; |
| final LinkedHashSet<Pattern> alwaysReviewPath; |
| final LinkedHashSet<Pattern> matchProjects; |
| final LinkedHashSet<Pattern> excludeProjects; |
| final LinkedHashSet<Pattern> thirdPartyAllowedProjects; |
| final LinkedHashSet<String> reviewers; |
| final LinkedHashSet<String> ccs; |
| String reviewLabel; |
| boolean defaultEnable; |
| int fromAccountId; |
| |
| ScannerConfig(String pluginName) { |
| this.pluginName = pluginName; |
| this.messages = new ArrayList<>(); |
| this.alwaysReviewPath = new LinkedHashSet<>(); |
| this.matchProjects = new LinkedHashSet<>(); |
| this.excludeProjects = new LinkedHashSet<>(); |
| this.thirdPartyAllowedProjects = new LinkedHashSet<>(); |
| this.reviewers = new LinkedHashSet<>(); |
| this.ccs = new LinkedHashSet<>(); |
| this.reviewLabel = DEFAULT_REVIEW_LABEL; |
| this.defaultEnable = false; |
| this.fromAccountId = 0; |
| } |
| |
| /** True when {@link CopyrightValidator} will behave the same with either config. */ |
| @Override |
| public boolean equals(Object other) { |
| if (this == other) { |
| return true; |
| } |
| if (other == null) { |
| return false; |
| } |
| if (other instanceof ScannerConfig) { |
| ScannerConfig otherConfig = (ScannerConfig) other; |
| return defaultEnable == otherConfig.defaultEnable |
| && fromAccountId == otherConfig.fromAccountId |
| && messages.equals(otherConfig.messages) |
| && alwaysReviewPath.equals(otherConfig.alwaysReviewPath) |
| && matchProjects.equals(otherConfig.matchProjects) |
| && excludeProjects.equals(otherConfig.excludeProjects) |
| && thirdPartyAllowedProjects.equals(otherConfig.thirdPartyAllowedProjects) |
| && reviewers.equals(otherConfig.reviewers) |
| && ccs.equals(otherConfig.ccs) |
| && Objects.equals(reviewLabel, otherConfig.reviewLabel) |
| && Objects.equals(scanner, otherConfig.scanner); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash( |
| defaultEnable, |
| fromAccountId, |
| messages, |
| alwaysReviewPath, |
| matchProjects, |
| excludeProjects, |
| thirdPartyAllowedProjects, |
| reviewers, |
| ccs, |
| reviewLabel, |
| scanner); |
| } |
| |
| /** Formats {@code message} into a message about the plugin configuration. */ |
| String pluginMessage(String message) { |
| return " in " + ProjectConfig.PROJECT_CONFIG + "\n[plugin \"" + pluginName + "\"]\n" + message; |
| } |
| |
| /** Formats {@code message} into a message about the {@code key = value} line of the config. */ |
| String pluginKeyValueMessage(String key, String value, String message) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(" "); |
| sb.append(key); |
| sb.append(" = "); |
| sb.append(value.trim()); |
| sb.append("\n"); |
| sb.append(" ".substring(0, key.length() + 5)); |
| sb.append("^\n"); // ^ aligned under start of value in message |
| sb.append(message); |
| return pluginMessage(sb.toString()); |
| } |
| |
| static CommitValidationMessage errorMessage(String message) { |
| return new CommitValidationMessage(message, ValidationMessage.Type.ERROR); |
| } |
| |
| static CommitValidationMessage warningMessage(String message) { |
| return new CommitValidationMessage(message, ValidationMessage.Type.WARNING); |
| } |
| |
| static CommitValidationMessage hintMessage(String message) { |
| return new CommitValidationMessage(message, ValidationMessage.Type.HINT); |
| } |
| |
| /** Formats {@code message} into a message about a required {@code key} not found in config. */ |
| String pluginKeyRequired(String key, String message) { |
| return pluginMessage("\nno \"" + key + " =\" key was found.\n\n" + message); |
| } |
| |
| /** |
| * Adjusts {@code scannerConfig} state per {@code cfg}. |
| * |
| * @param builder accumulates the scanner pattern rules used for constructing the scanner |
| * @param scannerConfig records the configuration parameter values |
| * @param cfg is the source of the configuration |
| */ |
| void readConfigFile(PluginConfig cfg) { |
| CopyrightPatterns.RuleSet.Builder builder = CopyrightPatterns.RuleSet.builder(); |
| // can be disabled in All-Projects and then enabled project-by-project |
| defaultEnable = cfg.getBoolean(KEY_ENABLE, defaultEnable); |
| fromAccountId = cfg.getInt(KEY_FROM, 0); |
| addStringList(cfg, reviewers, KEY_REVIEWER, "reviewer email address"); |
| addStringList(cfg, ccs, KEY_CC, "CC email address"); |
| String key = cfg.getString(KEY_REVIEW_LABEL); |
| if (!Strings.isNullOrEmpty(key)) { |
| reviewLabel = key; |
| } |
| addPatternList(cfg, alwaysReviewPath, KEY_ALWAYS_REVIEW_PATH, "path pattern"); |
| addPatternList(cfg, matchProjects, KEY_MATCH_PROJECTS, "project pattern"); |
| addPatternList(cfg, excludeProjects, KEY_EXCLUDE_PROJECTS, "project pattern"); |
| addPatternList( |
| cfg, thirdPartyAllowedProjects, KEY_THIRD_PARTY_ALLOWED_PROJECTS, "project pattern"); |
| // exclusions |
| addRule( |
| cfg, |
| rule -> { |
| builder.exclude(rule); |
| }, |
| KEY_EXCLUDE); |
| addRule( |
| cfg, |
| rule -> { |
| builder.excludePattern(rule); |
| }, |
| KEY_EXCLUDE_PATTERN); |
| // first-party |
| addRule( |
| cfg, |
| rule -> { |
| builder.addFirstParty(rule); |
| }, |
| KEY_FIRST_PARTY); |
| addRulePattern( |
| cfg, |
| pattern -> { |
| builder.addFirstPartyOwner(pattern); |
| }, |
| pattern -> { |
| builder.addFirstPartyLicense(pattern); |
| }, |
| KEY_FIRST_PARTY_PATTERN); |
| // third-party |
| addRule( |
| cfg, |
| rule -> { |
| builder.addThirdParty(rule); |
| }, |
| KEY_THIRD_PARTY); |
| addRulePattern( |
| cfg, |
| pattern -> { |
| builder.addThirdPartyOwner(pattern); |
| }, |
| pattern -> { |
| builder.addThirdPartyLicense(pattern); |
| }, |
| KEY_THIRD_PARTY_PATTERN); |
| // forbidden |
| addRule( |
| cfg, |
| rule -> { |
| builder.addForbidden(rule); |
| }, |
| KEY_FORBIDDEN); |
| addRulePattern( |
| cfg, |
| pattern -> { |
| builder.addForbiddenOwner(pattern); |
| }, |
| pattern -> { |
| builder.addForbiddenLicense(pattern); |
| }, |
| KEY_FORBIDDEN_PATTERN); |
| if (hasErrors()) { |
| // don't try to compile scanner if errors in configuration |
| return; |
| } |
| CopyrightPatterns.RuleSet rules = builder.build(); |
| patternSignature = rules.signature(); |
| scanner = |
| new CopyrightScanner( |
| rules.firstPartyLicenses, |
| rules.thirdPartyLicenses, |
| rules.forbiddenLicenses, |
| rules.firstPartyOwners, |
| rules.thirdPartyOwners, |
| rules.forbiddenOwners, |
| rules.excludePatterns); |
| } |
| |
| /** Returns true if {@code project} repository allows third-party code. */ |
| boolean isThirdPartyAllowed(String project) { |
| return matchesAny(project, thirdPartyAllowedProjects); |
| } |
| |
| /** Returns true if {@code fullPath} matches pattern always requiring review. e.g. PATENT */ |
| boolean isAlwaysReviewPath(String fullPath) { |
| return matchesAny(fullPath, alwaysReviewPath); |
| } |
| |
| /** Returns true if the configuration triggers any error messages. */ |
| boolean hasErrors() { |
| return messages.stream().anyMatch(m -> m.getType().equals(ValidationMessage.Type.ERROR)); |
| } |
| |
| /** Formats and appends config validation messages to {@code sb}. */ |
| void appendMessages(StringBuilder sb) { |
| for (CommitValidationMessage msg : messages) { |
| sb.append("\n\n"); |
| sb.append(msg.getType().toString()); |
| sb.append(" "); |
| sb.append(msg.getMessage()); |
| } |
| } |
| |
| /** Returns true if any pattern in {@code regexes} found in {@code text}. */ |
| static boolean matchesAny(String text, Collection<Pattern> regexes) { |
| requireNonNull(regexes); |
| for (Pattern pattern : regexes) { |
| if (pattern.matcher(text).find()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** Looks up {@code key} in {@code cfg} adding values to {@code dest} as rule names. */ |
| private void addRule(PluginConfig cfg, Consumer<String> dest, String key) { |
| for (String rule : cfg.getStringList(key)) { |
| rule = Strings.nullToEmpty(rule).trim(); |
| if (rule.isEmpty()) { |
| messages.add( |
| errorMessage(pluginKeyValueMessage(key, rule, "missing license or owner name"))); |
| continue; |
| } |
| try { |
| dest.accept(rule); |
| } catch (UnknownPatternName e) { |
| messages.add(errorMessage(pluginKeyValueMessage(key, rule, e.getMessage()))); |
| } |
| } |
| } |
| |
| /** |
| * Looks up {@code key} in {@code cfg} adding {@code owner} patterns to {@code ownerDest} and |
| * {@code license} patterns to {@code licenseDest}. |
| */ |
| private void addRulePattern( |
| PluginConfig cfg, Consumer<String> ownerDest, Consumer<String> licenseDest, String key) { |
| for (String cfgValue : cfg.getStringList(key)) { |
| cfgValue = Strings.nullToEmpty(cfgValue).trim(); |
| if (cfgValue.isEmpty()) { |
| messages.add( |
| errorMessage(pluginKeyValueMessage(key, cfgValue, "missing owner or license pattern"))); |
| continue; |
| } |
| if (cfgValue.toLowerCase().startsWith(OWNER)) { |
| String pattern = cfgValue.substring(OWNER.length()).trim(); |
| ownerDest.accept(pattern); |
| } else if (cfgValue.toLowerCase().startsWith(LICENSE)) { |
| String pattern = cfgValue.substring(LICENSE.length()).trim(); |
| licenseDest.accept(pattern); |
| } else { |
| messages.add( |
| errorMessage( |
| pluginKeyValueMessage( |
| key, cfgValue, "missing 'owner' or 'license' keyword in '" + cfgValue + "'"))); |
| } |
| } |
| } |
| |
| /** Looks up {@code key} in {@code cfg} adding values to {@code dest} as strings. */ |
| private void addStringList( |
| PluginConfig cfg, Collection<String> dest, String key, String shortDesc) { |
| for (String s : cfg.getStringList(key)) { |
| s = Strings.nullToEmpty(s).trim(); |
| if (s.isEmpty()) { |
| messages.add(errorMessage(pluginKeyValueMessage(key, s, "missing " + shortDesc))); |
| continue; |
| } |
| dest.add(s); |
| } |
| } |
| |
| /** Looks up {@code key} in {@code cfg} adding values to {@code dest} as regex patterns. */ |
| private void addPatternList( |
| PluginConfig cfg, Collection<Pattern> dest, String key, String shortDesc) { |
| for (String pattern : cfg.getStringList(key)) { |
| pattern = Strings.nullToEmpty(pattern).trim(); |
| if (pattern.isEmpty()) { |
| messages.add(errorMessage(pluginKeyValueMessage(key, pattern, "missing " + shortDesc))); |
| continue; |
| } |
| try { |
| dest.add(Pattern.compile(pattern)); |
| } catch (PatternSyntaxException e) { |
| messages.add(errorMessage(pluginKeyValueMessage(key, pattern, e.getMessage()))); |
| } |
| } |
| } |
| } |