Java tutorial
// Copyright (C) 2017 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.findowners; import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS; import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS_FILE_NAME; import static com.googlesource.gerrit.plugins.findowners.Config.REJECT_ERROR_IN_OWNERS; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Multimap; import com.google.gerrit.extensions.annotations.Exports; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.account.Emails; import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.ProjectConfigEntry; import com.google.gerrit.server.events.CommitReceivedEvent; import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidationListener; import com.google.gerrit.server.git.validators.CommitValidationMessage; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.inject.AbstractModule; import com.google.inject.Inject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; /** Check syntax of changed OWNERS files. */ public class OwnersValidator implements CommitValidationListener { private interface TreeWalkVisitor { void onVisit(TreeWalk tw); } public static AbstractModule module() { return new AbstractModule() { @Override protected void configure() { DynamicSet.bind(binder(), CommitValidationListener.class).to(OwnersValidator.class); bind(ProjectConfigEntry.class).annotatedWith(Exports.named(REJECT_ERROR_IN_OWNERS)) .toInstance(new ProjectConfigEntry("Reject OWNERS Files With Errors", null, ProjectConfigEntryType.BOOLEAN, null, false, "Pushes of commits with errors in OWNERS files will be rejected.")); } }; } private final String pluginName; private final PluginConfigFactory cfgFactory; private final Emails emails; @Inject OwnersValidator(@PluginName String pluginName, PluginConfigFactory cfgFactory, Emails emails) { this.pluginName = pluginName; this.cfgFactory = cfgFactory; this.emails = emails; } public static String getOwnersFileName(PluginConfig cfg) { return getOwnersFileName(cfg, OWNERS); } public static String getOwnersFileName(PluginConfig cfg, String defaultName) { return cfg.getString(OWNERS_FILE_NAME, defaultName); } public String getOwnersFileName(Project.NameKey project) { String name = getOwnersFileName(cfgFactory.getFromGerritConfig(pluginName, true)); try { return getOwnersFileName(cfgFactory.getFromProjectConfigWithInheritance(project, pluginName), name); } catch (NoSuchProjectException e) { return name; } } @VisibleForTesting static boolean isActive(PluginConfig cfg) { return cfg.getBoolean(REJECT_ERROR_IN_OWNERS, false); } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { List<CommitValidationMessage> messages = new LinkedList<>(); try { Project.NameKey project = receiveEvent.project.getNameKey(); PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(project, pluginName); if (isActive(cfg)) { String name = getOwnersFileName(project); messages = performValidation(receiveEvent.commit, receiveEvent.revWalk, name, false); } } catch (NoSuchProjectException | IOException e) { throw new CommitValidationException("failed to check owners files", e); } if (hasError(messages)) { throw new CommitValidationException("found invalid owners file", messages); } return messages; } @VisibleForTesting List<CommitValidationMessage> performValidation(RevCommit c, RevWalk revWalk, String ownersFileName, boolean verbose) throws IOException { // Collect all messages from all files. List<CommitValidationMessage> messages = new LinkedList<>(); // Collect all email addresses from all files and check each address only once. Map<String, Set<String>> email2lines = new HashMap<>(); Map<String, ObjectId> content = getChangedOwners(c, revWalk, ownersFileName); for (String path : content.keySet()) { ObjectLoader ol = revWalk.getObjectReader().open(content.get(path)); try (InputStream in = ol.openStream()) { if (RawText.isBinary(in)) { add(messages, path + " is a binary file", true); // OWNERS files cannot be binary continue; } } checkFile(messages, email2lines, path, ol, verbose); } checkEmails(messages, emails, email2lines, verbose); return messages; } private static void checkEmails(List<CommitValidationMessage> messages, Emails emails, Map<String, Set<String>> email2lines, boolean verbose) { List<String> owners = new ArrayList<>(email2lines.keySet()); if (verbose) { for (String owner : owners) { add(messages, "owner: " + owner, false); } } if (emails == null || owners.isEmpty()) { return; } String[] ownerEmailsAsArray = new String[owners.size()]; owners.toArray(ownerEmailsAsArray); try { Multimap<String, Account.Id> email2ids = emails.getAccountsFor(ownerEmailsAsArray); for (String owner : ownerEmailsAsArray) { boolean wrongEmail = (email2ids == null); if (!wrongEmail) { try { Collection<Account.Id> ids = email2ids.get(owner); wrongEmail = (ids == null || ids.isEmpty()); } catch (Exception e) { wrongEmail = true; } } if (wrongEmail) { String locations = String.join(" ", email2lines.get(owner)); add(messages, "unknown: " + owner + " at " + locations, true); } } } catch (Exception e) { add(messages, "checkEmails failed.", true); } } private static void checkFile(List<CommitValidationMessage> messages, Map<String, Set<String>> email2lines, String path, ObjectLoader ol, boolean verbose) throws IOException { if (verbose) { add(messages, "validate: " + path, false); } try (BufferedReader br = new BufferedReader( new InputStreamReader(ol.openStream(), StandardCharsets.UTF_8))) { int line = 0; for (String l = br.readLine(); l != null; l = br.readLine()) { line++; checkLine(messages, email2lines, path, line, l); } } } private static void collectEmail(Map<String, Set<String>> map, String email, String file, int lineNumber) { if (!email.equals("*")) { map.computeIfAbsent(email, (String k) -> new HashSet<>()); map.get(email).add(file + ":" + lineNumber); } } private static boolean hasError(List<CommitValidationMessage> messages) { for (CommitValidationMessage m : messages) { if (m.isError()) { return true; } } return false; } private static void add(List<CommitValidationMessage> messages, String msg, boolean error) { messages.add(new CommitValidationMessage(msg, error)); } private static void checkLine(List<CommitValidationMessage> messages, Map<String, Set<String>> email2lines, String path, int lineNumber, String line) { String email; String[] emails; if (Parser.isComment(line) || Parser.isNoParent(line)) { // no email address to check } else if ((email = Parser.parseEmail(line)) != null) { collectEmail(email2lines, email, path, lineNumber); } else if ((emails = Parser.parsePerFileEmails(line)) != null) { for (String e : emails) { collectEmail(email2lines, e, path, lineNumber); } } else if (Parser.isInclude(line)) { // Included "OWNERS" files will be checked by themselves. // TODO: Check if the include file path is valid and existence of the included file. // TODO: Check an included file syntax if it is not named as the project ownersFileName. add(messages, "unchecked: " + path + ":" + lineNumber + ": " + line, false); } else if (Parser.isFile(line)) { add(messages, "ignored: " + path + ":" + lineNumber + ": " + line, true); } else { add(messages, "syntax: " + path + ":" + lineNumber + ": " + line, true); } } /** * Find all changed OWNERS files which differ between the commit and its parents. Return a map * from "Path to the changed file" to "ObjectId of the file". */ private static Map<String, ObjectId> getChangedOwners(RevCommit c, RevWalk revWalk, String ownersFileName) throws IOException { final Map<String, ObjectId> content = new HashMap<>(); visitChangedEntries(c, revWalk, new TreeWalkVisitor() { @Override public void onVisit(TreeWalk tw) { if (isFile(tw) && ownersFileName.equals(tw.getNameString())) { content.put(tw.getPathString(), tw.getObjectId(0)); } } }); return content; } private static boolean isFile(TreeWalk tw) { return FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)) || FileMode.REGULAR_FILE.equals(tw.getRawMode(0)); } /** * Find all TreeWalk entries which differ between the commit and its parents. If a TreeWalk entry * is found this method calls the onVisit() method of the class TreeWalkVisitor. */ private static void visitChangedEntries(RevCommit c, RevWalk revWalk, TreeWalkVisitor visitor) throws IOException { try (TreeWalk tw = new TreeWalk(revWalk.getObjectReader())) { tw.setRecursive(true); tw.setFilter(TreeFilter.ANY_DIFF); tw.addTree(c.getTree()); if (c.getParentCount() > 0) { for (RevCommit p : c.getParents()) { if (p.getTree() == null) { revWalk.parseHeaders(p); } tw.addTree(p.getTree()); } while (tw.next()) { if (isDifferentToAllParents(c, tw)) { visitor.onVisit(tw); } } } else { while (tw.next()) { visitor.onVisit(tw); } } } } private static boolean isDifferentToAllParents(RevCommit c, TreeWalk tw) { if (c.getParentCount() > 1) { for (int p = 1; p <= c.getParentCount(); p++) { if (tw.getObjectId(0).equals(tw.getObjectId(p))) { return false; } } } return true; } }