Merge "Merge branch 'stable-2.14' into stable-2.15" into stable-2.15
diff --git a/BUILD b/BUILD
index 6086d5c..1ab91f7 100644
--- a/BUILD
+++ b/BUILD
@@ -1,4 +1,10 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "gerrit_plugin",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+)
 
 gerrit_plugin(
     name = "reviewers",
@@ -10,3 +16,21 @@
     ],
     resources = glob(["src/main/**/*"]),
 )
+
+junit_tests(
+    name = "reviewers_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    tags = ["reviewers"],
+    deps = [
+        ":reviewers__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "reviewers__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":reviewers__plugin",
+    ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewers.java
new file mode 100644
index 0000000..afbb377
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewers.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2018 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.reviewers;
+
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import java.util.ArrayList;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+abstract class AddReviewers implements Runnable {
+  private static final Logger log = LoggerFactory.getLogger(AddReviewers.class);
+
+  private final ThreadLocalRequestContext tl;
+  protected final GerritApi gApi;
+  protected final IdentifiedUser.GenericFactory identifiedUserFactory;
+  protected final SchemaFactory<ReviewDb> schemaFactory;
+  protected final ChangeInfo changeInfo;
+
+  private ReviewDb db = null;
+
+  AddReviewers(
+      ThreadLocalRequestContext tl,
+      GerritApi gApi,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ChangeInfo changeInfo) {
+    this.tl = tl;
+    this.gApi = gApi;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.schemaFactory = schemaFactory;
+    this.changeInfo = changeInfo;
+  }
+
+  abstract Set<Account.Id> getReviewers();
+
+  @Override
+  public void run() {
+    RequestContext old =
+        tl.setContext(
+            new RequestContext() {
+
+              @Override
+              public CurrentUser getUser() {
+                return identifiedUserFactory.create(new Account.Id(changeInfo.owner._accountId));
+              }
+
+              @Override
+              public Provider<ReviewDb> getReviewDbProvider() {
+                return new Provider<ReviewDb>() {
+                  @Override
+                  public ReviewDb get() {
+                    if (db == null) {
+                      try {
+                        db = schemaFactory.open();
+                      } catch (OrmException e) {
+                        throw new ProvisionException("Cannot open ReviewDb", e);
+                      }
+                    }
+                    return db;
+                  }
+                };
+              }
+            });
+    try {
+      addReviewers(getReviewers(), changeInfo);
+    } finally {
+      tl.setContext(old);
+      if (db != null) {
+        db.close();
+        db = null;
+      }
+    }
+  }
+
+  private void addReviewers(Set<Account.Id> reviewers, ChangeInfo changeInfo) {
+    try {
+      // TODO(davido): Switch back to using changes API again,
+      // when it supports batch mode for adding reviewers
+      ReviewInput in = new ReviewInput();
+      in.reviewers = new ArrayList<>(reviewers.size());
+      for (Account.Id account : reviewers) {
+        AddReviewerInput addReviewerInput = new AddReviewerInput();
+        addReviewerInput.reviewer = account.toString();
+        in.reviewers.add(addReviewerInput);
+      }
+      gApi.changes().id(changeInfo._number).current().review(in);
+    } catch (RestApiException e) {
+      log.error("Couldn't add reviewers to the change", e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewersByConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewersByConfiguration.java
new file mode 100644
index 0000000..a03c035
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/AddReviewersByConfiguration.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 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.reviewers;
+
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Set;
+
+class AddReviewersByConfiguration extends AddReviewers {
+  private final Set<Account.Id> reviewers;
+
+  interface Factory {
+    AddReviewersByConfiguration create(ChangeInfo changeInfo, Set<Account.Id> reviewers);
+  }
+
+  @Inject
+  AddReviewersByConfiguration(
+      ThreadLocalRequestContext tl,
+      GerritApi gApi,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      @Assisted ChangeInfo changeInfo,
+      @Assisted Set<Account.Id> reviewers) {
+    super(tl, gApi, identifiedUserFactory, schemaFactory, changeInfo);
+    this.reviewers = reviewers;
+  }
+
+  @Override
+  Set<Account.Id> getReviewers() {
+    return reviewers;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java
deleted file mode 100644
index 47916a0..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ChangeEventListener.java
+++ /dev/null
@@ -1,281 +0,0 @@
-// Copyright (C) 2013 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.reviewers;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.events.RevisionCreatedListener;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.group.GroupsCollection;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.List;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-class ChangeEventListener implements RevisionCreatedListener {
-  private static final Logger log = LoggerFactory.getLogger(ChangeEventListener.class);
-
-  private final AccountResolver accountResolver;
-  private final Provider<GroupsCollection> groupsCollection;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final DefaultReviewers.Factory reviewersFactory;
-  private final GitRepositoryManager repoManager;
-  private final WorkQueue workQueue;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final ThreadLocalRequestContext tl;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final ChangeData.Factory changeDataFactory;
-  private final ReviewersConfig.Factory configFactory;
-  private final Provider<CurrentUser> user;
-  private final ChangeQueryBuilder queryBuilder;
-
-  @Inject
-  ChangeEventListener(
-      final AccountResolver accountResolver,
-      final Provider<GroupsCollection> groupsCollection,
-      final GroupMembers.Factory groupMembersFactory,
-      final DefaultReviewers.Factory reviewersFactory,
-      final GitRepositoryManager repoManager,
-      final WorkQueue workQueue,
-      final IdentifiedUser.GenericFactory identifiedUserFactory,
-      final ThreadLocalRequestContext tl,
-      final SchemaFactory<ReviewDb> schemaFactory,
-      final ChangeData.Factory changeDataFactory,
-      final ReviewersConfig.Factory configFactory,
-      final Provider<CurrentUser> user,
-      final ChangeQueryBuilder queryBuilder,
-      final PluginConfigFactory cfgFactory,
-      @PluginName String pluginName) {
-    this.accountResolver = accountResolver;
-    this.groupsCollection = groupsCollection;
-    this.groupMembersFactory = groupMembersFactory;
-    this.reviewersFactory = reviewersFactory;
-    this.repoManager = repoManager;
-    this.workQueue = workQueue;
-    this.identifiedUserFactory = identifiedUserFactory;
-    this.tl = tl;
-    this.schemaFactory = schemaFactory;
-    this.changeDataFactory = changeDataFactory;
-    this.configFactory = configFactory;
-    this.user = user;
-    this.queryBuilder = queryBuilder;
-  }
-
-  @Override
-  public void onRevisionCreated(RevisionCreatedListener.Event event) {
-    ChangeInfo c = event.getChange();
-    onEvent(new Project.NameKey(c.project), c._number, event.getWho());
-  }
-
-  private void onEvent(Project.NameKey projectName, int changeNumber, AccountInfo uploader) {
-    // TODO(davido): we have to cache per project configuration
-    ReviewersConfig config = configFactory.create(projectName);
-    List<ReviewerFilterSection> sections = config.getReviewerFilterSections();
-
-    if (sections.isEmpty()) {
-      return;
-    }
-
-    try (Repository git = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(git);
-        ReviewDb reviewDb = schemaFactory.open()) {
-      ChangeData changeData =
-          changeDataFactory.create(reviewDb, projectName, new Change.Id(changeNumber));
-      Set<String> reviewers = findReviewers(sections, changeData);
-      if (reviewers.isEmpty()) {
-        return;
-      }
-
-      final Change change = changeData.change();
-      final Runnable task =
-          reviewersFactory.create(change, toAccounts(reviewers, projectName, uploader));
-
-      workQueue
-          .getDefaultQueue()
-          .submit(
-              new Runnable() {
-                ReviewDb db = null;
-
-                @Override
-                public void run() {
-                  RequestContext old =
-                      tl.setContext(
-                          new RequestContext() {
-
-                            @Override
-                            public CurrentUser getUser() {
-                              return identifiedUserFactory.create(change.getOwner());
-                            }
-
-                            @Override
-                            public Provider<ReviewDb> getReviewDbProvider() {
-                              return new Provider<ReviewDb>() {
-                                @Override
-                                public ReviewDb get() {
-                                  if (db == null) {
-                                    try {
-                                      db = schemaFactory.open();
-                                    } catch (OrmException e) {
-                                      throw new ProvisionException("Cannot open ReviewDb", e);
-                                    }
-                                  }
-                                  return db;
-                                }
-                              };
-                            }
-                          });
-                  try {
-                    task.run();
-                  } finally {
-                    tl.setContext(old);
-                    if (db != null) {
-                      db.close();
-                      db = null;
-                    }
-                  }
-                }
-              });
-    } catch (OrmException | IOException | QueryParseException x) {
-      log.error(x.getMessage(), x);
-    }
-  }
-
-  private Set<String> findReviewers(List<ReviewerFilterSection> sections, ChangeData changeData)
-      throws OrmException, QueryParseException {
-    ImmutableSet.Builder<String> reviewers = ImmutableSet.builder();
-    List<ReviewerFilterSection> found = findReviewerSections(sections, changeData);
-    for (ReviewerFilterSection s : found) {
-      reviewers.addAll(s.getReviewers());
-    }
-    return reviewers.build();
-  }
-
-  private List<ReviewerFilterSection> findReviewerSections(
-      List<ReviewerFilterSection> sections, ChangeData changeData)
-      throws OrmException, QueryParseException {
-    ImmutableList.Builder<ReviewerFilterSection> found = ImmutableList.builder();
-    for (ReviewerFilterSection s : sections) {
-      if (Strings.isNullOrEmpty(s.getFilter()) || s.getFilter().equals("*")) {
-        found.add(s);
-      } else if (filterMatch(s.getFilter(), changeData)) {
-        found.add(s);
-      }
-    }
-    return found.build();
-  }
-
-  boolean filterMatch(String filter, ChangeData changeData)
-      throws OrmException, QueryParseException {
-    Preconditions.checkNotNull(filter);
-    ChangeQueryBuilder qb = queryBuilder.asUser(user.get());
-    Predicate<ChangeData> filterPredicate = qb.parse(filter);
-    // TODO(davido): check that the potential review can see this change
-    // by adding AND is_visible() predicate? Or is it OK to assume
-    // that reviewers always can see it?
-    return filterPredicate.asMatchable().match(changeData);
-  }
-
-  private Set<Account> toAccounts(Set<String> in, Project.NameKey p, AccountInfo uploader) {
-    Set<Account> reviewers = Sets.newHashSetWithExpectedSize(in.size());
-    GroupMembers groupMembers = null;
-    for (String r : in) {
-      try {
-        Account account = accountResolver.find(r);
-        if (account != null) {
-          reviewers.add(account);
-          continue;
-        }
-      } catch (OrmException | IOException | ConfigInvalidException e) {
-        // If the account doesn't exist, find() will return null.  We only
-        // get here if something went wrong accessing the database
-        log.error("Failed to resolve account " + r, e);
-        continue;
-      }
-      if (groupMembers == null) {
-        // email is not unique to one account, try to locate the account using
-        // "Full name <email>" to increase chance of finding only one.
-        String uploaderNameEmail = String.format("%s <%s>", uploader.name, uploader.email);
-        try {
-          Account uploaderAccount = accountResolver.findByNameOrEmail(uploaderNameEmail);
-          if (uploaderAccount != null) {
-            groupMembers =
-                groupMembersFactory.create(identifiedUserFactory.create(uploaderAccount.getId()));
-          }
-        } catch (OrmException | IOException e) {
-          log.warn(
-              String.format(
-                  "Failed to list accounts for group %s, cannot retrieve uploader account %s",
-                  r, uploaderNameEmail),
-              e);
-        }
-
-        try {
-          if (groupMembers != null) {
-            reviewers.addAll(
-                groupMembers.listAccounts(groupsCollection.get().parse(r).getGroupUUID(), p));
-          } else {
-            log.warn(
-                String.format(
-                    "Failed to list accounts for group %s; cannot retrieve uploader account for %s",
-                    r, uploader.email));
-          }
-        } catch (UnprocessableEntityException | NoSuchGroupException e) {
-          log.warn(String.format("Reviewer %s is neither an account nor a group", r));
-        } catch (NoSuchProjectException e) {
-          log.warn(String.format("Failed to list accounts for group %s and project %s", r, p));
-        } catch (IOException | OrmException e) {
-          log.warn(String.format("Failed to list accounts for group %s", r), e);
-        }
-      }
-    }
-    return reviewers;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java
deleted file mode 100644
index 1d5dac4..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/DefaultReviewers.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2013 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.reviewers;
-
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.changes.AddReviewerInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.util.ArrayList;
-import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-class DefaultReviewers implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(DefaultReviewers.class);
-
-  private final GerritApi gApi;
-  private final Change change;
-  private final Set<Account> reviewers;
-
-  interface Factory {
-    DefaultReviewers create(Change change, Set<Account> reviewers);
-  }
-
-  @Inject
-  DefaultReviewers(GerritApi gApi, @Assisted Change change, @Assisted Set<Account> reviewers) {
-    this.gApi = gApi;
-    this.change = change;
-    this.reviewers = reviewers;
-  }
-
-  @Override
-  public void run() {
-    addReviewers(reviewers, change);
-  }
-
-  /**
-   * Append the reviewers to change#{@link Change}
-   *
-   * @param reviewers Set of reviewers to add
-   * @param change {@link Change} to add the reviewers to
-   */
-  private void addReviewers(Set<Account> reviewers, Change change) {
-    try {
-      // TODO(davido): Switch back to using changes API again,
-      // when it supports batch mode for adding reviewers
-      ReviewInput in = new ReviewInput();
-      in.reviewers = new ArrayList<>(reviewers.size());
-      for (Account account : reviewers) {
-        AddReviewerInput addReviewerInput = new AddReviewerInput();
-        if (account.isActive()) {
-          addReviewerInput.reviewer = account.getId().toString();
-          in.reviewers.add(addReviewerInput);
-        }
-      }
-      gApi.changes().id(change.getId().get()).current().review(in);
-    } catch (RestApiException e) {
-      log.error("Couldn't add reviewers to the change", e);
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/GetReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/GetReviewers.java
index b0e735f..7ee4c83 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/GetReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/GetReviewers.java
@@ -14,9 +14,7 @@
 
 package com.googlesource.gerrit.plugins.reviewers;
 
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
@@ -25,16 +23,15 @@
 
 @Singleton
 class GetReviewers implements RestReadView<ProjectResource> {
-  private final ReviewersConfig.Factory configFactory;
+  private final ReviewersConfig config;
 
   @Inject
-  GetReviewers(ReviewersConfig.Factory configFactory) {
-    this.configFactory = configFactory;
+  GetReviewers(ReviewersConfig config) {
+    this.config = config;
   }
 
   @Override
-  public List<ReviewerFilterSection> apply(ProjectResource resource)
-      throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    return configFactory.create(resource.getNameKey()).getReviewerFilterSections();
+  public List<ReviewerFilterSection> apply(ProjectResource resource) throws RestApiException {
+    return config.forProject(resource.getNameKey()).getReviewerFilterSections();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
index 98dad1b..3e949d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
-import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.RevisionCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -24,24 +24,26 @@
 import com.google.gerrit.extensions.webui.GwtPlugin;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.change.ReviewerSuggestion;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
 
 public class Module extends FactoryModule {
   private final boolean enableUI;
   private final boolean enableREST;
+  private final boolean suggestOnly;
 
   @Inject
-  public Module(@PluginName String pluginName, PluginConfigFactory pluginCfgFactory) {
-    Config c = pluginCfgFactory.getGlobalPluginConfig(pluginName);
-    this.enableREST = c.getBoolean("reviewers", null, "enableREST", true);
-    this.enableUI = enableREST ? c.getBoolean("reviewers", null, "enableUI", true) : false;
+  public Module(ReviewersConfig cfg) {
+    this.enableREST = cfg.enableREST();
+    this.enableUI = cfg.enableUI();
+    this.suggestOnly = cfg.suggestOnly();
   }
 
-  public Module(boolean enableUI, boolean enableREST) {
+  public Module(boolean enableUI, boolean enableREST, boolean suggestOnly) {
     this.enableUI = enableUI;
     this.enableREST = enableREST;
+    this.suggestOnly = suggestOnly;
   }
 
   @Override
@@ -51,9 +53,21 @@
       DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("reviewers"));
     }
 
-    DynamicSet.bind(binder(), RevisionCreatedListener.class).to(ChangeEventListener.class);
-    factory(DefaultReviewers.Factory.class);
-    factory(ReviewersConfig.Factory.class);
+    if (suggestOnly) {
+      install(
+          new AbstractModule() {
+            @Override
+            protected void configure() {
+              bind(ReviewerSuggestion.class)
+                  .annotatedWith(Exports.named("reviewer-suggest"))
+                  .to(Reviewers.class);
+            }
+          });
+    } else {
+      DynamicSet.bind(binder(), RevisionCreatedListener.class).to(Reviewers.class);
+    }
+
+    factory(AddReviewersByConfiguration.Factory.class);
 
     if (enableREST) {
       install(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
index 69eba6a..856a5cd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
@@ -51,7 +51,7 @@
   }
 
   private final String pluginName;
-  private final ReviewersConfig.Factory configFactory;
+  private final ReviewersConfig config;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final AccountResolver accountResolver;
@@ -60,14 +60,14 @@
   @Inject
   PutReviewers(
       @PluginName String pluginName,
-      ReviewersConfig.Factory configFactory,
+      ReviewersConfig config,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       AccountResolver accountResolver,
       Provider<GroupsCollection> groupsCollection,
       Provider<ReviewDb> reviewDbProvider) {
     this.pluginName = pluginName;
-    this.configFactory = configFactory;
+    this.config = config;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.accountResolver = accountResolver;
@@ -78,7 +78,7 @@
   public List<ReviewerFilterSection> apply(ProjectResource rsrc, Input input)
       throws RestApiException {
     Project.NameKey projectName = rsrc.getNameKey();
-    ReviewersConfig cfg = configFactory.create(projectName);
+    ReviewersConfig.ForProject cfg = config.forProject(projectName);
     if (!rsrc.getControl().isOwner() || cfg == null) {
       throw new ResourceNotFoundException("Project" + projectName.get() + " not found");
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewerFilterSection.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewerFilterSection.java
index 1112bdd..06e7387 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewerFilterSection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewerFilterSection.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.reviewers;
 
+import java.util.Objects;
 import java.util.Set;
 
 class ReviewerFilterSection {
@@ -32,4 +33,18 @@
   Set<String> getReviewers() {
     return reviewers;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof ReviewerFilterSection) {
+      ReviewerFilterSection other = ((ReviewerFilterSection) o);
+      return Objects.equals(filter, other.filter) && Objects.equals(reviewers, other.reviewers);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(filter, reviewers);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Reviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Reviewers.java
new file mode 100644
index 0000000..70c2cec
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Reviewers.java
@@ -0,0 +1,187 @@
+// Copyright (C) 2013 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.reviewers;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.events.RevisionEvent;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ReviewerSuggestion;
+import com.google.gerrit.server.change.SuggestedReviewer;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class Reviewers implements RevisionCreatedListener, ReviewerSuggestion {
+  private static final Logger log = LoggerFactory.getLogger(Reviewers.class);
+
+  private final ReviewersResolver resolver;
+  private final AddReviewersByConfiguration.Factory byConfigFactory;
+  private final WorkQueue workQueue;
+  private final ReviewersConfig config;
+  private final Provider<CurrentUser> user;
+  private final ChangeQueryBuilder queryBuilder;
+  private final Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
+  Reviewers(
+      ReviewersResolver resolver,
+      AddReviewersByConfiguration.Factory byConfigFactory,
+      WorkQueue workQueue,
+      ReviewersConfig config,
+      Provider<CurrentUser> user,
+      ChangeQueryBuilder queryBuilder,
+      Provider<InternalChangeQuery> queryProvider) {
+    this.resolver = resolver;
+    this.byConfigFactory = byConfigFactory;
+    this.workQueue = workQueue;
+    this.config = config;
+    this.user = user;
+    this.queryBuilder = queryBuilder;
+    this.queryProvider = queryProvider;
+  }
+
+  @Override
+  public void onRevisionCreated(RevisionCreatedListener.Event event) {
+    onEvent(event);
+  }
+
+  @Override
+  public Set<SuggestedReviewer> suggestReviewers(
+      Project.NameKey projectName,
+      @Nullable Change.Id changeId,
+      @Nullable String query,
+      Set<Account.Id> candidates) {
+    List<ReviewerFilterSection> sections = getSections(projectName);
+
+    if (sections.isEmpty() || changeId == null) {
+      return ImmutableSet.of();
+    }
+
+    try {
+      Set<String> reviewers = findReviewers(changeId.get(), sections);
+      if (!reviewers.isEmpty()) {
+        return resolver
+            .resolve(reviewers, projectName, changeId.get(), null)
+            .stream()
+            .map(a -> suggestedReviewer(a))
+            .collect(toSet());
+      }
+    } catch (OrmException | QueryParseException | IOException x) {
+      log.error(x.getMessage(), x);
+    }
+    return ImmutableSet.of();
+  }
+
+  private SuggestedReviewer suggestedReviewer(Account.Id account) {
+    SuggestedReviewer reviewer = new SuggestedReviewer();
+    reviewer.account = account;
+    reviewer.score = 1;
+    return reviewer;
+  }
+
+  private List<ReviewerFilterSection> getSections(Project.NameKey projectName) {
+    // TODO(davido): we have to cache per project configuration
+    return config.forProject(projectName).getReviewerFilterSections();
+  }
+
+  private void onEvent(RevisionEvent event) {
+    ChangeInfo c = event.getChange();
+    Project.NameKey projectName = new Project.NameKey(c.project);
+
+    List<ReviewerFilterSection> sections = getSections(projectName);
+
+    if (sections.isEmpty()) {
+      return;
+    }
+
+    AccountInfo uploader = event.getWho();
+    int changeNumber = c._number;
+    try {
+      Set<String> reviewers = findReviewers(changeNumber, sections);
+      if (reviewers.isEmpty()) {
+        return;
+      }
+      final Runnable task =
+          byConfigFactory.create(
+              c, resolver.resolve(reviewers, projectName, changeNumber, uploader));
+
+      workQueue.getDefaultQueue().submit(task);
+    } catch (QueryParseException | IOException e) {
+      log.warn(
+          "Could not add default reviewers for change {} of project {}, filter is invalid: {}",
+          changeNumber,
+          projectName.get(),
+          e.getMessage());
+    } catch (OrmException x) {
+      log.error(x.getMessage(), x);
+    }
+  }
+
+  private Set<String> findReviewers(int change, List<ReviewerFilterSection> sections)
+      throws OrmException, QueryParseException {
+    ImmutableSet.Builder<String> reviewers = ImmutableSet.builder();
+    List<ReviewerFilterSection> found = findReviewerSections(change, sections);
+    for (ReviewerFilterSection s : found) {
+      reviewers.addAll(s.getReviewers());
+    }
+    return reviewers.build();
+  }
+
+  private List<ReviewerFilterSection> findReviewerSections(
+      int change, List<ReviewerFilterSection> sections) throws OrmException, QueryParseException {
+    ImmutableList.Builder<ReviewerFilterSection> found = ImmutableList.builder();
+    for (ReviewerFilterSection s : sections) {
+      if (Strings.isNullOrEmpty(s.getFilter()) || s.getFilter().equals("*")) {
+        found.add(s);
+      } else if (filterMatch(change, s.getFilter())) {
+        found.add(s);
+      }
+    }
+    return found.build();
+  }
+
+  boolean filterMatch(int change, String filter) throws OrmException, QueryParseException {
+    Preconditions.checkNotNull(filter);
+    ChangeQueryBuilder qb = queryBuilder.asUser(user.get());
+    return !queryProvider
+        .get()
+        .noFields()
+        .query(qb.parse(String.format("change:%s %s", change, filter)))
+        .isEmpty();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfig.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfig.java
index 6ee15ce..569373c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfig.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -32,80 +32,129 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-class ReviewersConfig extends VersionedMetaData {
-  private static final String FILENAME = "reviewers.config";
-  private static final String FILTER = "filter";
-  private static final String REVIEWER = "reviewer";
-  private Config cfg;
+@Singleton
+class ReviewersConfig {
+  private static final Logger log = LoggerFactory.getLogger(ReviewersConfig.class);
 
-  interface Factory {
-    ReviewersConfig create(Project.NameKey projectName);
-  }
+  static final String FILENAME = "reviewers.config";
+  static final String SECTION_FILTER = "filter";
+  static final String KEY_REVIEWER = "reviewer";
+  private static final String KEY_IGNORE_DRAFTS = "ignoreDrafts";
+  private static final String KEY_ENABLE_REST = "enableREST";
+  private static final String KEY_ENABLE_UI = "enableUI";
+  private static final String KEY_SUGGEST_ONLY = "suggestOnly";
+
+  private final PluginConfigFactory cfgFactory;
+  private final String pluginName;
+
+  private final boolean enableUI;
+  private final boolean enableREST;
+  private final boolean suggestOnly;
+  private final boolean ignoreDrafts;
 
   @Inject
-  ReviewersConfig(
-      PluginConfigFactory cfgFactory,
-      @PluginName String pluginName,
-      @Assisted Project.NameKey projectName)
-      throws NoSuchProjectException {
-    cfg = cfgFactory.getProjectPluginConfigWithInheritance(projectName, pluginName);
+  ReviewersConfig(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
+    this.cfgFactory = cfgFactory;
+    this.pluginName = pluginName;
+    Config cfg = cfgFactory.getGlobalPluginConfig(pluginName);
+    this.ignoreDrafts = cfg.getBoolean(pluginName, null, KEY_IGNORE_DRAFTS, false);
+    this.enableREST = cfg.getBoolean(pluginName, null, KEY_ENABLE_REST, true);
+    this.enableUI = enableREST ? cfg.getBoolean(pluginName, null, KEY_ENABLE_UI, true) : false;
+    this.suggestOnly = cfg.getBoolean(pluginName, null, KEY_SUGGEST_ONLY, false);
   }
 
-  List<ReviewerFilterSection> getReviewerFilterSections() {
-    ImmutableList.Builder<ReviewerFilterSection> b = ImmutableList.builder();
-    for (String f : cfg.getSubsections(FILTER)) {
-      b.add(newReviewerFilterSection(f));
+  public ForProject forProject(Project.NameKey projectName) {
+    Config cfg;
+    try {
+      cfg = cfgFactory.getProjectPluginConfigWithMergedInheritance(projectName, pluginName);
+    } catch (NoSuchProjectException e) {
+      log.error("Unable to get config for project {}", projectName.get());
+      cfg = new Config();
     }
-    return b.build();
+    return new ForProject(cfg);
   }
 
-  void addReviewer(String filter, String reviewer) {
-    if (!newReviewerFilterSection(filter).getReviewers().contains(reviewer)) {
-      List<String> values =
-          new ArrayList<>(Arrays.asList(cfg.getStringList(FILTER, filter, REVIEWER)));
-      values.add(reviewer);
-      cfg.setStringList(FILTER, filter, REVIEWER, values);
+  public boolean ignoreDrafts() {
+    return ignoreDrafts;
+  }
+
+  public boolean enableREST() {
+    return enableREST;
+  }
+
+  public boolean enableUI() {
+    return enableUI;
+  }
+
+  public boolean suggestOnly() {
+    return suggestOnly;
+  }
+
+  static class ForProject extends VersionedMetaData {
+    private Config cfg;
+
+    ForProject(Config cfg) {
+      this.cfg = cfg;
     }
-  }
 
-  void removeReviewer(String filter, String reviewer) {
-    if (newReviewerFilterSection(filter).getReviewers().contains(reviewer)) {
-      List<String> values =
-          new ArrayList<>(Arrays.asList(cfg.getStringList(FILTER, filter, REVIEWER)));
-      values.remove(reviewer);
-      if (values.isEmpty()) {
-        cfg.unsetSection(FILTER, filter);
-      } else {
-        cfg.setStringList(FILTER, filter, REVIEWER, values);
+    List<ReviewerFilterSection> getReviewerFilterSections() {
+      ImmutableList.Builder<ReviewerFilterSection> b = ImmutableList.builder();
+      for (String f : cfg.getSubsections(SECTION_FILTER)) {
+        b.add(newReviewerFilterSection(f));
+      }
+      return b.build();
+    }
+
+    void addReviewer(String filter, String reviewer) {
+      if (!newReviewerFilterSection(filter).getReviewers().contains(reviewer)) {
+        List<String> values =
+            new ArrayList<>(Arrays.asList(cfg.getStringList(SECTION_FILTER, filter, KEY_REVIEWER)));
+        values.add(reviewer);
+        cfg.setStringList(SECTION_FILTER, filter, KEY_REVIEWER, values);
       }
     }
-  }
 
-  private ReviewerFilterSection newReviewerFilterSection(String filter) {
-    ImmutableSet.Builder<String> b = ImmutableSet.builder();
-    for (String reviewer : cfg.getStringList(FILTER, filter, REVIEWER)) {
-      b.add(reviewer);
+    void removeReviewer(String filter, String reviewer) {
+      if (newReviewerFilterSection(filter).getReviewers().contains(reviewer)) {
+        List<String> values =
+            new ArrayList<>(Arrays.asList(cfg.getStringList(SECTION_FILTER, filter, KEY_REVIEWER)));
+        values.remove(reviewer);
+        if (values.isEmpty()) {
+          cfg.unsetSection(SECTION_FILTER, filter);
+        } else {
+          cfg.setStringList(SECTION_FILTER, filter, KEY_REVIEWER, values);
+        }
+      }
     }
-    return new ReviewerFilterSection(filter, b.build());
-  }
 
-  @Override
-  protected String getRefName() {
-    return RefNames.REFS_CONFIG;
-  }
-
-  @Override
-  protected void onLoad() throws IOException, ConfigInvalidException {
-    cfg = readConfig(FILENAME);
-  }
-
-  @Override
-  protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
-    if (Strings.isNullOrEmpty(commit.getMessage())) {
-      commit.setMessage("Update reviewers configuration\n");
+    private ReviewerFilterSection newReviewerFilterSection(String filter) {
+      ImmutableSet.Builder<String> b = ImmutableSet.builder();
+      for (String reviewer : cfg.getStringList(SECTION_FILTER, filter, KEY_REVIEWER)) {
+        b.add(reviewer);
+      }
+      return new ReviewerFilterSection(filter, b.build());
     }
-    saveConfig(FILENAME, cfg);
-    return true;
+
+    @Override
+    protected String getRefName() {
+      return RefNames.REFS_CONFIG;
+    }
+
+    @Override
+    protected void onLoad() throws IOException, ConfigInvalidException {
+      cfg = readConfig(FILENAME);
+    }
+
+    @Override
+    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+      if (Strings.isNullOrEmpty(commit.getMessage())) {
+        commit.setMessage("Update reviewers configuration\n");
+      }
+      saveConfig(FILENAME, cfg);
+      return true;
+    }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolver.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolver.java
new file mode 100644
index 0000000..c3fb00c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolver.java
@@ -0,0 +1,185 @@
+// Copyright (C) 2018 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.reviewers;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/* Resolve account and group names to account ids */
+@Singleton
+class ReviewersResolver {
+  private static final Logger log = LoggerFactory.getLogger(ReviewersResolver.class);
+
+  private final AccountResolver accountResolver;
+  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  @Inject
+  ReviewersResolver(
+      AccountResolver accountResolver,
+      Provider<GroupsCollection> groupsCollection,
+      GroupMembers.Factory groupMembersFactory,
+      IdentifiedUser.GenericFactory identifiedUserFactory) {
+    this.accountResolver = accountResolver;
+    this.groupsCollection = groupsCollection;
+    this.groupMembersFactory = groupMembersFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
+  }
+
+  /**
+   * Resolve a set of account names to {@link com.google.gerrit.reviewdb.client.Account.Id}s. Group
+   * names are resolved to their account members.
+   *
+   * @param names the set of account names to convert
+   * @param project the project name
+   * @param changeNumber the change Id
+   * @param uploader account to use to look up groups, or null if groups are not needed
+   * @return set of {@link com.google.gerrit.reviewdb.client.Account.Id}s.
+   */
+  @VisibleForTesting
+  Set<Account.Id> resolve(
+      Set<String> names, Project.NameKey project, int changeNumber, @Nullable AccountInfo uploader)
+      throws IOException {
+    Set<Account.Id> reviewers = Sets.newHashSetWithExpectedSize(names.size());
+    GroupMembers groupMembers = null;
+    for (String name : names) {
+      if (resolveAccount(project, changeNumber, uploader, reviewers, name)) {
+        continue;
+      }
+
+      if (groupMembers == null && uploader != null) {
+        groupMembers = createGroupMembers(project, changeNumber, uploader, name);
+      }
+
+      if (groupMembers != null) {
+        resolveGroup(project, changeNumber, reviewers, groupMembers, name);
+      } else {
+        log.warn(
+            "For the change {} of project {}: failed to list accounts for group {}; cannot retrieve uploader account for {}.",
+            changeNumber,
+            project,
+            name,
+            uploader.email);
+      }
+    }
+    return reviewers;
+  }
+
+  private boolean resolveAccount(
+      Project.NameKey project,
+      int changeNumber,
+      AccountInfo uploader,
+      Set<Account.Id> reviewers,
+      String accountName)
+      throws IOException {
+    try {
+      Account account = accountResolver.find(accountName);
+      if (account != null && account.isActive()) {
+        if (uploader == null || uploader._accountId != account.getId().get()) {
+          reviewers.add(account.getId());
+        }
+        return true;
+      }
+    } catch (OrmException | IOException | ConfigInvalidException e) {
+      // If the account doesn't exist, find() will return null.  We only
+      // get here if something went wrong accessing the database
+      log.error(
+          "For the change {} of project {}: failed to resolve account {}.",
+          changeNumber,
+          project,
+          accountName,
+          e);
+      return true;
+    }
+    return false;
+  }
+
+  private void resolveGroup(
+      Project.NameKey project,
+      int changeNumber,
+      Set<Account.Id> reviewers,
+      GroupMembers groupMembers,
+      String group) {
+    try {
+      Set<Account.Id> accounts =
+          groupMembers
+              .listAccounts(groupsCollection.get().parse(group).getGroupUUID(), project)
+              .stream()
+              .filter(Account::isActive)
+              .map(Account::getId)
+              .collect(toSet());
+      reviewers.addAll(accounts);
+    } catch (UnprocessableEntityException | NoSuchGroupException e) {
+      log.warn(
+          "For the change {} of project {}: reviewer {} is neither an account nor a group.",
+          changeNumber,
+          project,
+          group,
+          e);
+    } catch (NoSuchProjectException | IOException | OrmException e) {
+      log.warn(
+          "For the change {} of project {}: failed to list accounts for group {}.",
+          changeNumber,
+          project,
+          group,
+          e);
+    }
+  }
+
+  private GroupMembers createGroupMembers(
+      Project.NameKey project, int changeNumber, AccountInfo uploader, String group) {
+    // email is not unique to one account, try to locate the account using
+    // "Full name <email>" to increase chance of finding only one.
+    String uploaderNameEmail = String.format("%s <%s>", uploader.name, uploader.email);
+    try {
+      Account uploaderAccount = accountResolver.findByNameOrEmail(uploaderNameEmail);
+      if (uploaderAccount != null) {
+        return groupMembersFactory.create(identifiedUserFactory.create(uploaderAccount.getId()));
+      }
+    } catch (OrmException | IOException e) {
+      log.warn(
+          "For the change {} of project {}: failed to list accounts for group {}, cannot retrieve uploader account {}.",
+          changeNumber,
+          project,
+          group,
+          uploaderNameEmail,
+          e);
+    }
+    return null;
+  }
+}
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 491e134..cbc531c 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -1,7 +1,7 @@
 Build
 =====
 
-This plugin can be built with Bazel or Maven.
+This plugin can be built with Bazel.
 
 Two build modes are supported: Standalone and in Gerrit tree.
 The standalone build mode is recommended, as this mode doesn't require
@@ -19,6 +19,12 @@
   bazel-genfiles/@PLUGIN@.jar
 ```
 
+To execute the tests run:
+
+```
+  bazel test //...
+```
+
 This project can be imported into the Eclipse IDE:
 
 ```
@@ -37,6 +43,12 @@
   bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
 ```
 
+To execute the tests run:
+
+```
+  bazel test --test_tag_filters=@PLUGIN@
+```
+
 This project can be imported into the Eclipse IDE.
 Add the plugin name to the `CUSTOM_PLUGINS` set in
 Gerrit core in `tools/bzl/plugins.bzl`, and execute:
@@ -45,20 +57,5 @@
   ./tools/eclipse/project.py
 ```
 
-Maven
------
-
-Note that the Maven build is provided for compatibility reasons, but
-it is considered to be deprecated and will be removed in a future
-version of this plugin.
-
-To build with Maven, run
-
-```
-  mvn clean package
-```
-
-When building with Maven, the Gerrit Plugin API must be available.
-
 How to build the Gerrit Plugin API is described in the [Gerrit
 documentation](../../../Documentation/dev-bazel.html#_extension_and_plugin_api_jar_files).
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e2112fb..eb77417 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -8,6 +8,7 @@
   [reviewers]
     enableREST = true
     enableUI = false
+    suggestOnly = false
 ```
 
 reviewers.enableREST
@@ -18,6 +19,13 @@
 :	Enable the UI.  When set to false, the 'Reviewers' menu is not displayed
 	on the project screen. Defaults to true, or false when `enableREST` is false.
 
+reviewers.suggestOnly
+:	Provide the configured reviewers as suggestions in the "Add Reviewer" dialog
+	instead of automatically adding them to the change. Only supports accounts;
+	groups are not suggested. Defaults to false. By default Gerrit will consider
+	the suggestions with a weight of 1. To force the suggestions higher in the
+	list, set a higher value (like 1000) in `addReviewer.@PLUGIN@-reviewer-suggestion.weight`
+	in `gerrit.config`.
 Per project configuration of the @PLUGIN@ plugin is done in the
 `reviewers.config` file of the project. Missing values are inherited
 from the parent projects. This means a global default configuration can
@@ -58,4 +66,4 @@
 ```
 
 1. Push a change for review involving file "build/modules/GLOBAL.pm".
-2. Both john.doe@example.com and jane.doe@example.com get added as reviewers.
+2. Both john.doe@example.com and jane.doe@example.com get added or suggested as reviewers.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfigIT.java
new file mode 100644
index 0000000..cf3db58
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersConfigIT.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2018 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.reviewers;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.FILENAME;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.KEY_REVIEWER;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.SECTION_FILTER;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+@TestPlugin(name = "reviewers", sysModule = "com.googlesource.gerrit.plugins.reviewers.Module")
+public class ReviewersConfigIT extends LightweightPluginDaemonTest {
+  private static final String BRANCH_MAIN = "branch:main";
+  private static final String NO_FILTER = "*";
+  private static final String JANE_DOE = "jane.doe@example.com";
+  private static final String JOHN_DOE = "john.doe@example.com";
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    testRepo.reset("refs/heads/config");
+  }
+
+  @Test
+  public void reviewersConfigSingle() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_FILTER, NO_FILTER, KEY_REVIEWER, JOHN_DOE);
+    cfg.setString(SECTION_FILTER, BRANCH_MAIN, KEY_REVIEWER, JANE_DOE);
+
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, "Add reviewers", FILENAME, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    assertThat(reviewersConfig().forProject(project).getReviewerFilterSections())
+        .containsExactlyElementsIn(
+            ImmutableList.of(
+                new ReviewerFilterSection(NO_FILTER, ImmutableSet.of(JOHN_DOE)),
+                new ReviewerFilterSection(BRANCH_MAIN, ImmutableSet.of(JANE_DOE))))
+        .inOrder();
+  }
+
+  @Test
+  public void reviewersConfigWithMergedInheritance() throws Exception {
+    Config parentCfg = new Config();
+    parentCfg.setString(SECTION_FILTER, NO_FILTER, KEY_REVIEWER, JOHN_DOE);
+    parentCfg.setString(SECTION_FILTER, BRANCH_MAIN, KEY_REVIEWER, JOHN_DOE);
+
+    pushFactory
+        .create(
+            db,
+            admin.getIdent(),
+            testRepo,
+            "Add reviewers parent project",
+            FILENAME,
+            parentCfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    Project.NameKey childProject = createProject("child", project);
+    TestRepository<?> childTestRepo = cloneProject(childProject);
+    fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    childTestRepo.reset("refs/heads/config");
+
+    Config cfg = new Config();
+    cfg.setString(SECTION_FILTER, NO_FILTER, KEY_REVIEWER, JANE_DOE);
+    cfg.setString(SECTION_FILTER, BRANCH_MAIN, KEY_REVIEWER, JANE_DOE);
+
+    pushFactory
+        .create(
+            db,
+            admin.getIdent(),
+            childTestRepo,
+            "Add reviewers child project",
+            FILENAME,
+            cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    assertThat(reviewersConfig().forProject(childProject).getReviewerFilterSections())
+        .containsExactlyElementsIn(
+            ImmutableList.of(
+                new ReviewerFilterSection(NO_FILTER, ImmutableSet.of(JOHN_DOE, JANE_DOE)),
+                new ReviewerFilterSection(BRANCH_MAIN, ImmutableSet.of(JOHN_DOE, JANE_DOE))))
+        .inOrder();
+  }
+
+  private ReviewersConfig reviewersConfig() {
+    return plugin.getSysInjector().getInstance(ReviewersConfig.class);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersIT.java b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersIT.java
new file mode 100644
index 0000000..f46ca74
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersIT.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2018 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.reviewers;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.FILENAME;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.KEY_REVIEWER;
+import static com.googlesource.gerrit.plugins.reviewers.ReviewersConfig.SECTION_FILTER;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
+import java.util.Collection;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+@TestPlugin(name = "reviewers", sysModule = "com.googlesource.gerrit.plugins.reviewers.Module")
+public class ReviewersIT extends LightweightPluginDaemonTest {
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    testRepo.reset("refs/heads/config");
+  }
+
+  @Test
+  public void addReviewers() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+    TestAccount user2 = accountCreator.user2();
+
+    Config cfg = new Config();
+    cfg.setStringList(SECTION_FILTER, "*", KEY_REVIEWER, ImmutableList.of(user.email, user2.email));
+
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, "Add reviewers", FILENAME, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    testRepo.reset(oldHead);
+    String changeId = createChange().getChangeId();
+
+    Collection<AccountInfo> reviewers;
+    // Wait for 100 ms until the create patch set event
+    // is processed by the reviewers plugin
+    long wait = 0;
+    do {
+      reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
+      if (reviewers == null) {
+        Thread.sleep(10);
+        wait += 10;
+        if (wait > 100) {
+          assert_().fail("Timeout of 100 ms exceeded");
+        }
+      }
+    } while (reviewers == null);
+
+    assertThat(reviewers.stream().map(a -> a._accountId).collect(toSet()))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id.get(), user.id.get(), user2.id.get()));
+  }
+
+  @Test
+  public void doNotAddReviewersFromNonMatchingFilters() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+
+    Config cfg = new Config();
+    cfg.setString(SECTION_FILTER, "branch:master", KEY_REVIEWER, user.email);
+
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, "Add reviewers", FILENAME, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    testRepo.reset(oldHead);
+
+    createBranch(new Branch.NameKey(project, "other-branch"));
+
+    // Create a change that matches the filter section.
+    createChange("refs/for/master");
+
+    // The actual change we want to test
+    String changeId = createChange("refs/for/other-branch").getChangeId();
+
+    Collection<AccountInfo> reviewers;
+    long wait = 0;
+    do {
+      Thread.sleep(10);
+      wait += 10;
+      reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
+    } while (reviewers == null && wait < 100);
+
+    assertThat(reviewers).isNull();
+  }
+
+  @Test
+  public void addReviewersFromMatchingFilters() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+
+    Config cfg = new Config();
+    cfg.setString(SECTION_FILTER, "branch:other-branch", KEY_REVIEWER, user.email);
+
+    pushFactory
+        .create(db, admin.getIdent(), testRepo, "Add reviewers", FILENAME, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    testRepo.reset(oldHead);
+
+    createBranch(new Branch.NameKey(project, "other-branch"));
+
+    // Create a change that doesn't match the filter section.
+    createChange("refs/for/master");
+
+    // The actual change we want to test
+    String changeId = createChange("refs/for/other-branch").getChangeId();
+
+    Collection<AccountInfo> reviewers;
+    long wait = 0;
+    do {
+      Thread.sleep(10);
+      wait += 10;
+      reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
+    } while (reviewers == null && wait < 100);
+
+    assertThat(reviewers.stream().map(a -> a._accountId).collect(toSet()))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.id.get(), user.id.get()));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolverIT.java b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolverIT.java
new file mode 100644
index 0000000..c5b89ed
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/reviewers/ReviewersResolverIT.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2018 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.reviewers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.inject.Inject;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class ReviewersResolverIT extends AbstractDaemonTest {
+
+  @Inject private ReviewersResolver resolver;
+  private int change;
+
+  @Before
+  public void setUp() {
+    change = 1;
+  }
+
+  @Test
+  public void testUploaderSkippedAsReviewer() throws Exception {
+    Set<Account.Id> reviewers =
+        resolver.resolve(
+            Collections.singleton(user.email),
+            project,
+            change,
+            gApi.accounts().id(user.id.get()).get());
+    assertThat(reviewers).isEmpty();
+  }
+
+  @Test
+  public void testAccountResolve() throws Exception {
+    Set<Account.Id> reviewers =
+        resolver.resolve(
+            ImmutableSet.of(user.email, admin.email),
+            project,
+            change,
+            gApi.accounts().id(admin.id.get()).get());
+    assertThat(reviewers).containsExactly(user.id);
+  }
+
+  @Test
+  public void testAccountGroupResolve() throws Exception {
+    String group1 = createGroup("group1");
+    TestAccount foo = createTestAccount("foo", group1);
+    TestAccount bar = createTestAccount("bar", group1);
+
+    String group2 = createGroup("group2");
+    TestAccount baz = createTestAccount("baz", group2);
+    TestAccount qux = createTestAccount("qux", group2);
+
+    TestAccount system = createTestAccount("system", "Administrators");
+
+    Set<Account.Id> reviewers =
+        resolver.resolve(
+            ImmutableSet.of(system.email, group1, group2),
+            project,
+            change,
+            gApi.accounts().id(admin.id.get()).get());
+    assertThat(reviewers).containsExactly(system.id, foo.id, bar.id, baz.id, qux.id);
+  }
+
+  private TestAccount createTestAccount(String name, String group) throws Exception {
+    name = name(name);
+    return accountCreator.create(name, name + "@example.com", name + " full name", group);
+  }
+}
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    "junit_tests",
+)
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 74d4ce1..2ee6681 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,5 +2,6 @@
     "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
     "gerrit_plugin",
     "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
     "GWT_PLUGIN_DEPS",
 )
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 25b845e..a6cc8cb 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -1,13 +1,13 @@
 load(
     "//tools/bzl:plugin.bzl",
-    "PLUGIN_DEPS",
     "GWT_PLUGIN_DEPS",
 )
 load("//tools/bzl:classpath.bzl", "classpath_collector")
 
 classpath_collector(
     name = "main_classpath_collect",
-    deps = PLUGIN_DEPS + GWT_PLUGIN_DEPS + [
-        "//:reviewers__plugin",
+    testonly = 1,
+    deps = GWT_PLUGIN_DEPS + [
+        "//:reviewers__plugin_test_deps",
     ],
 )