Merge branch 'stable-2.14'

* stable-2.14:
  Improve wording about invalid/missing rate limit config
  Update bazlets to latest stable-2.14 to build with 2.14.14 API
  Remove default rate limits
  Update Mockito to 2.22.0
  Format external_plugin_dependencies.bzl with buildifier
  Fix Eclipse preferences to use Java 8 compliance
  Migrate `tools/bazel.rc` to `.bazelrc`
  Add .mailmap
  ListQuotes: Format with google-java-format
  Update bazlets to latest stable-2.14 to build with 2.14.13 API
  Update bazlets to latest stable-2.14 to use 2.14.12 API
  Update bazlets to latest stable-2.14 to use 2.14.11 API
  Format build files with buildifier 0.12.0
  Format Java files with google-java-format 1.6
  Update bazlets to latest revision on stable-2.14
  Add REST API rate limits
  Add tests for RateLimitUploadListener
  Fix bug in message formatting
  Refactor limit exceeded messaging
  Fix documentation and prepare for extension w.r.t. new rate limiter types
  Format all Java files with google-java-format
  Prevent NullPointerException for default configuration

Change-Id: I51036fc85a5deab86a95b30c48bf37920e73649f
diff --git a/tools/bazel.rc b/.bazelrc
similarity index 100%
rename from tools/bazel.rc
rename to .bazelrc
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..2dfeff7
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1 @@
+Viktor Kaufmann <viktor.kaufman@sap.com> d048645 <viktor.kaufman@sap.com>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 2a585e4..e123c28 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -4,8 +4,8 @@
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
 org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
 org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
diff --git a/BUILD b/BUILD
index c8a682f..112baa9 100644
--- a/BUILD
+++ b/BUILD
@@ -1,9 +1,9 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 load(
     "//tools/bzl:plugin.bzl",
-    "gerrit_plugin",
     "PLUGIN_DEPS",
     "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
 )
 
 gerrit_plugin(
@@ -31,5 +31,6 @@
     visibility = ["//visibility:public"],
     exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
         ":quota__plugin",
+        "@mockito//jar",
     ],
 )
diff --git a/WORKSPACE b/WORKSPACE
index c3c4540..b08627e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -24,3 +24,7 @@
 
 # Load release Plugin API
 # gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
index e14e488..f97b72c 100644
--- a/bazlets.bzl
+++ b/bazlets.bzl
@@ -1,17 +1,16 @@
 NAME = "com_googlesource_gerrit_bazlets"
 
 def load_bazlets(
-    commit,
-    local_path = None
-  ):
-  if not local_path:
-      native.git_repository(
-          name = NAME,
-          remote = "https://gerrit.googlesource.com/bazlets",
-          commit = commit,
-      )
-  else:
-      native.local_repository(
-          name = NAME,
-          path = local_path,
-      )
+        commit,
+        local_path = None):
+    if not local_path:
+        native.git_repository(
+            name = NAME,
+            remote = "https://gerrit.googlesource.com/bazlets",
+            commit = commit,
+        )
+    else:
+        native.local_repository(
+            name = NAME,
+            path = local_path,
+        )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..b5f4f2a
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,33 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+    maven_jar(
+        name = "mockito",
+        artifact = "org.mockito:mockito-core:2.22.0",
+        sha1 = "73d21198eea9e20af8e55260ec131b6fea9de917",
+        deps = [
+            "@byte_buddy//jar",
+            "@byte_buddy_agent//jar",
+            "@objenesis//jar",
+        ],
+    )
+
+    BYTE_BUDDY_VER = "1.8.21"
+
+    maven_jar(
+        name = "byte_buddy",
+        artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VER,
+        sha1 = "3589ecd78aa4b1e1c1e1505d0321e93a9b73ca54",
+    )
+
+    maven_jar(
+        name = "byte_buddy_agent",
+        artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VER,
+        sha1 = "5b652c6c6645dfb27fdf96bf3f6d12b7b3818344",
+    )
+
+    maven_jar(
+        name = "objenesis",
+        artifact = "org.objenesis:objenesis:2.6",
+        sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+    )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java
index 2475d21..81dbcd8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java
@@ -14,6 +14,9 @@
 
 package com.googlesource.gerrit.plugins.quota;
 
+import static com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type.RESTAPI;
+import static com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type.UPLOADPACK;
+
 import com.google.common.collect.ArrayTable;
 import com.google.common.collect.Table;
 import java.text.MessageFormat;
@@ -32,12 +35,9 @@
 import org.slf4j.LoggerFactory;
 
 public class AccountLimitsConfig {
-  private static final int DEFAULT_BURST_COUNT = 30;
-  private static final int DEFAULT_INTERVAL_SECONDS = 60;
   private static final Pattern PATTERN =
       Pattern.compile("^\\s*(\\d+)\\s*/\\s*(.*)\\s*burst\\s*(\\d+)$");
-  private static final Logger log =
-      LoggerFactory.getLogger(AccountLimitsConfig.class);
+  private static final Logger log = LoggerFactory.getLogger(AccountLimitsConfig.class);
   static final String GROUP_SECTION = "group";
   static final SectionParser<AccountLimitsConfig> KEY =
       new SectionParser<AccountLimitsConfig>() {
@@ -72,7 +72,8 @@
   }
 
   public static enum Type implements ConfigEnum {
-    UPLOADPACK;
+    UPLOADPACK,
+    RESTAPI;
 
     @Override
     public String toConfigValue() {
@@ -94,41 +95,44 @@
     }
     rateLimits = ArrayTable.create(Arrays.asList(Type.values()), groups);
     for (String groupName : groups) {
-      Type type = Type.UPLOADPACK;
-      rateLimits.put(type, groupName,
-          parseRateLimit(c, groupName, type));
+      parseRateLimit(c, groupName, UPLOADPACK);
+      parseRateLimit(c, groupName, RESTAPI);
     }
   }
 
-  RateLimit parseRateLimit(Config c, String groupName, Type type) {
+  void parseRateLimit(Config c, String groupName, Type type) {
     String name = type.toConfigValue();
-    String value = c.getString(GROUP_SECTION, groupName, name).trim();
+    String value = c.getString(GROUP_SECTION, groupName, name);
     if (value == null) {
-      return defaultRateLimit(type);
+      return;
     }
+    value = value.trim();
 
     Matcher m = PATTERN.matcher(value);
     if (!m.matches()) {
-      log.warn(
-          "Invalid ''{}'' ratelimit configuration ''{}'', use default ratelimit {}/hour",
-          name, value, 3600.0D / DEFAULT_INTERVAL_SECONDS);
-      return defaultRateLimit(type);
+      log.error(
+          "Invalid ''{}'' ratelimit configuration ''{}''; ignoring the configuration entry",
+          name,
+          value);
+      return;
     }
 
     String digits = m.group(1);
     String unitName = m.group(2).trim();
     String storeCountString = m.group(3).trim();
-    long burstCount = DEFAULT_BURST_COUNT;
+    long burstCount;
     try {
       burstCount = Long.parseLong(storeCountString);
     } catch (NumberFormatException e) {
-      log.warn(
-          "Invalid ''{}'' ratelimit store configuration ''{}'', use default burst count ''{}''",
-          name, storeCountString, burstCount);
+      log.error(
+          "Invalid ''{}'' ratelimit store configuration ''{}''; ignoring the configuration entry",
+          name,
+          storeCountString);
+      return;
     }
 
     TimeUnit inputUnit = TimeUnit.HOURS;
-    double ratePerSecond = 1.0D / DEFAULT_INTERVAL_SECONDS;
+    double ratePerSecond;
     if (match(unitName, "s", "sec", "second")) {
       inputUnit = TimeUnit.SECONDS;
     } else if (match(unitName, "m", "min", "minute")) {
@@ -139,16 +143,17 @@
       inputUnit = TimeUnit.DAYS;
     } else {
       logNotRateUnit(GROUP_SECTION, groupName, name, value);
+      return;
     }
     try {
-      ratePerSecond = 1.0D * Long.parseLong(digits)
-          / TimeUnit.SECONDS.convert(1, inputUnit);
+      ratePerSecond = 1.0D * Long.parseLong(digits) / TimeUnit.SECONDS.convert(1, inputUnit);
     } catch (NumberFormatException nfe) {
       logNotRateUnit(GROUP_SECTION, groupName, unitName, value);
+      return;
     }
 
     int maxBurstSeconds = (int) (burstCount / ratePerSecond);
-    return new RateLimit(type, ratePerSecond, maxBurstSeconds);
+    rateLimits.put(type, groupName, new RateLimit(type, ratePerSecond, maxBurstSeconds));
   }
 
   private static boolean match(final String a, final String... cases) {
@@ -160,22 +165,20 @@
     return false;
   }
 
-  private void logNotRateUnit(String section, String subsection, String name,
-      String valueString) {
+  private void logNotRateUnit(String section, String subsection, String name, String valueString) {
     if (subsection != null) {
-      log.error(MessageFormat.format("Invalid rate unit value: {0}.{1}.{2}={3}",
-          section, subsection, name, valueString));
+      log.error(
+          MessageFormat.format(
+              "Invalid rate unit value: {0}.{1}.{2}={3}; ignoring the configuration entry",
+              section, subsection, name, valueString));
     } else {
-      log.error(MessageFormat.format("Invalid rate unit value: {0}.{1}={2}",
-          section, name, valueString));
+      log.error(
+          MessageFormat.format(
+              "Invalid rate unit value: {0}.{1}={2}; ignoring the configuration entry",
+              section, name, valueString));
     }
   }
 
-  private RateLimit defaultRateLimit(Type type) {
-    return new RateLimit(type, 1.0D / DEFAULT_INTERVAL_SECONDS,
-        DEFAULT_INTERVAL_SECONDS * DEFAULT_BURST_COUNT);
-  }
-
   /**
    * @param type type of rate limit
    * @return map of rate limits per group name
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java
index 751735c..1961efb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java
@@ -21,27 +21,22 @@
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-
 import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.RateLimit;
 import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type;
-
+import java.util.Map;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Map;
-import java.util.Optional;
-
 public class AccountLimitsFinder {
-  private static final Logger log =
-      LoggerFactory.getLogger(AccountLimitsFinder.class);
+  private static final Logger log = LoggerFactory.getLogger(AccountLimitsFinder.class);
 
   private final ProjectCache projectCache;
   private final GroupsCollection groupsCollection;
 
   @Inject
-  AccountLimitsFinder(ProjectCache projectCache,
-      GroupsCollection groupsCollection) {
+  AccountLimitsFinder(ProjectCache projectCache, GroupsCollection groupsCollection) {
     this.projectCache = projectCache;
     this.groupsCollection = groupsCollection;
   }
@@ -49,20 +44,16 @@
   /**
    * @param type type of rate limit
    * @param user identified user
-   * @return the rate limit matching the first configured group limit the given
-   *         user is a member of
+   * @return the rate limit matching the first configured group limit the given user is a member of
    */
-  public Optional<RateLimit> firstMatching(AccountLimitsConfig.Type type,
-      IdentifiedUser user) {
-    Optional<Map<String, AccountLimitsConfig.RateLimit>> limits =
-        getRatelimits(type);
+  public Optional<RateLimit> firstMatching(AccountLimitsConfig.Type type, IdentifiedUser user) {
+    Optional<Map<String, AccountLimitsConfig.RateLimit>> limits = getRatelimits(type);
     if (limits.isPresent()) {
       GroupMembership memberShip = user.getEffectiveGroups();
       for (String groupName : limits.get().keySet()) {
         GroupDescription.Basic d = groupsCollection.parseId(groupName);
         if (d == null) {
-          log.error("Ignoring limits for unknown group ''{}'' in quota.config",
-              groupName);
+          log.error("Ignoring limits for unknown group ''{}'' in quota.config", groupName);
         } else if (memberShip.contains(d.getGroupUUID())) {
           return Optional.ofNullable(limits.get().get(groupName));
         }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/DeletionListener.java b/src/main/java/com/googlesource/gerrit/plugins/quota/DeletionListener.java
index 47fbafd..e625187 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/DeletionListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/DeletionListener.java
@@ -14,12 +14,10 @@
 
 package com.googlesource.gerrit.plugins.quota;
 
-
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
-
 public class DeletionListener implements ProjectDeletedListener {
 
   private final RepoSizeCache repoSizeCache;
@@ -33,4 +31,4 @@
   public void onProjectDeleted(Event event) {
     repoSizeCache.evict(new Project.NameKey(event.getProjectName()));
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/GCListener.java b/src/main/java/com/googlesource/gerrit/plugins/quota/GCListener.java
index 887f117..ca2e933 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/GCListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/GCListener.java
@@ -17,9 +17,8 @@
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.inject.Singleton;
 import com.google.inject.Inject;
-
+import com.google.inject.Singleton;
 import java.util.Properties;
 
 @Singleton
@@ -38,11 +37,9 @@
     Properties statistics = event.getStatistics();
     if (statistics != null) {
       Number sizeOfLooseObjects = (Number) statistics.get("sizeOfLooseObjects");
-      Number sizeOfPackedObjects =
-          (Number) statistics.get("sizeOfPackedObjects");
+      Number sizeOfPackedObjects = (Number) statistics.get("sizeOfPackedObjects");
       if (sizeOfLooseObjects != null && sizeOfPackedObjects != null) {
-        repoSizeCache.set(key, sizeOfLooseObjects.longValue()
-            + sizeOfPackedObjects.longValue());
+        repoSizeCache.set(key, sizeOfLooseObjects.longValue() + sizeOfPackedObjects.longValue());
         return;
       }
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuota.java b/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuota.java
index 19aebe3..854d9ef 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuota.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuota.java
@@ -23,9 +23,8 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
-
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class GetQuota implements RestReadView<ProjectResource> {
 
@@ -34,7 +33,9 @@
   private final LoadingCache<Project.NameKey, AtomicLong> repoSizeCache;
 
   @Inject
-  public GetQuota(ProjectCache projectCache, QuotaFinder quotaFinder,
+  public GetQuota(
+      ProjectCache projectCache,
+      QuotaFinder quotaFinder,
       @Named(REPO_SIZE_CACHE) LoadingCache<Project.NameKey, AtomicLong> repoSizeCache) {
     this.projectCache = projectCache;
     this.quotaFinder = quotaFinder;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuotas.java b/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuotas.java
index 971c414..b24eec4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuotas.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/GetQuotas.java
@@ -26,22 +26,19 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class GetQuotas implements
-    ChildCollection<ConfigResource, QuotaResource> {
+public class GetQuotas implements ChildCollection<ConfigResource, QuotaResource> {
 
   private final Provider<ListQuotas> list;
   private final DynamicMap<RestView<QuotaResource>> views;
 
   @Inject
-  public GetQuotas(Provider<ListQuotas> list,
-      DynamicMap<RestView<QuotaResource>> views) {
+  public GetQuotas(Provider<ListQuotas> list, DynamicMap<RestView<QuotaResource>> views) {
     this.list = list;
     this.views = views;
   }
 
   @Override
-  public RestView<ConfigResource> list() throws ResourceNotFoundException,
-      AuthException {
+  public RestView<ConfigResource> list() throws ResourceNotFoundException, AuthException {
     return list.get();
   }
 
@@ -55,5 +52,4 @@
   public DynamicMap<RestView<QuotaResource>> views() {
     return views;
   }
-
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/quota/HttpModule.java
new file mode 100644
index 0000000..5a2148e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/HttpModule.java
@@ -0,0 +1,75 @@
+// 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.quota;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type;
+
+class HttpModule extends CacheModule {
+  static final String CACHE_NAME_RESTAPI_ACCOUNTID = "restapi_rate_limits_by_account";
+  static final String CACHE_NAME_RESTAPI_REMOTEHOST = "restapi_rate_limits_by_ip";
+
+  private final String restapiLimitExceededMsg;
+
+  @Inject
+  HttpModule(PluginConfigFactory pluginCF, @PluginName String pluginName) {
+    final PluginConfig pc = pluginCF.getFromGerritConfig(pluginName);
+    restapiLimitExceededMsg =
+        new RateMsgHelper(
+                Type.RESTAPI, pc.getString(RateMsgHelper.RESTAPI_CONFIGURABLE_MSG_ANNOTATION))
+            .getMessageFormatMsgWithBursts();
+  }
+
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), AllRequestFilter.class).to(RestApiRateLimiter.class);
+    bindConstant()
+        .annotatedWith(Names.named(RateMsgHelper.RESTAPI_CONFIGURABLE_MSG_ANNOTATION))
+        .to(restapiLimitExceededMsg);
+  }
+
+  @Provides
+  @Named(CACHE_NAME_RESTAPI_ACCOUNTID)
+  @Singleton
+  public LoadingCache<Account.Id, Module.Holder> getRestApiLoadingCacheByAccountId(
+      GenericFactory userFactory, AccountLimitsFinder finder) {
+    return CacheBuilder.newBuilder()
+        .build(new Module.HolderCacheLoaderByAccountId(Type.RESTAPI, userFactory, finder));
+  }
+
+  @Provides
+  @Named(CACHE_NAME_RESTAPI_REMOTEHOST)
+  @Singleton
+  public LoadingCache<String, Module.Holder> getRestApiLoadingCacheByRemoteHost(
+      SystemGroupBackend systemGroupBackend, AccountLimitsFinder finder) {
+    return CacheBuilder.newBuilder()
+        .build(new Module.HolderCacheLoaderByRemoteHost(Type.RESTAPI, systemGroupBackend, finder));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/ListQuotas.java b/src/main/java/com/googlesource/gerrit/plugins/quota/ListQuotas.java
index 06d9cc1..1d8061c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/ListQuotas.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/ListQuotas.java
@@ -24,12 +24,9 @@
 import com.google.gerrit.server.project.ListProjects;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-
 import com.googlesource.gerrit.plugins.quota.GetQuota.QuotaInfo;
-
-import org.kohsuke.args4j.Option;
-
 import java.util.Map;
+import org.kohsuke.args4j.Option;
 
 public class ListQuotas implements RestReadView<ConfigResource> {
 
@@ -43,7 +40,10 @@
     this.listProjects = listProjects;
   }
 
-  @Option(name = "--prefix", aliases = {"-p"}, metaVar = "PREFIX",
+  @Option(
+      name = "--prefix",
+      aliases = {"-p"},
+      metaVar = "PREFIX",
       usage = "match project prefix")
   public void setMatchPrefix(String matchPrefix) {
     this.matchPrefix = matchPrefix;
@@ -51,8 +51,7 @@
 
   @Override
   public Map<String, QuotaInfo> apply(ConfigResource resource)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      Exception {
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
     Map<String, QuotaInfo> result = Maps.newTreeMap();
     ListProjects lister = listProjects.get();
     lister.setMatchPrefix(matchPrefix);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositoriesQuotaValidator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositoriesQuotaValidator.java
index 5135b52..16499c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositoriesQuotaValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositoriesQuotaValidator.java
@@ -23,14 +23,12 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class MaxRepositoriesQuotaValidator implements
-    ProjectCreationValidationListener {
+public class MaxRepositoriesQuotaValidator implements ProjectCreationValidationListener {
   private final QuotaFinder quotaFinder;
   private final ProjectCache projectCache;
 
   @Inject
-  MaxRepositoriesQuotaValidator(QuotaFinder quotaFinder,
-      ProjectCache projectCache) {
+  MaxRepositoriesQuotaValidator(QuotaFinder quotaFinder, ProjectCache projectCache) {
     this.quotaFinder = quotaFinder;
     this.projectCache = projectCache;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositorySizeQuota.java b/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositorySizeQuota.java
index 4388cd9..5e923ad 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositorySizeQuota.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/MaxRepositorySizeQuota.java
@@ -28,18 +28,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-
-import org.apache.commons.lang.mutable.MutableLong;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.GC;
-import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.PostReceiveHook;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceivePack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.FileVisitResult;
@@ -51,11 +39,20 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.commons.lang.mutable.MutableLong;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 class MaxRepositorySizeQuota implements ReceivePackInitializer, PostReceiveHook, RepoSizeCache {
-  private static final Logger log = LoggerFactory
-      .getLogger(MaxRepositorySizeQuota.class);
+  private static final Logger log = LoggerFactory.getLogger(MaxRepositorySizeQuota.class);
 
   static final String REPO_SIZE_CACHE = "repo_size";
 
@@ -77,9 +74,11 @@
   private final ProjectNameResolver projectNameResolver;
 
   @Inject
-  MaxRepositorySizeQuota(QuotaFinder quotaFinder,
+  MaxRepositorySizeQuota(
+      QuotaFinder quotaFinder,
       @Named(REPO_SIZE_CACHE) LoadingCache<Project.NameKey, AtomicLong> cache,
-      ProjectCache projectCache, ProjectNameResolver projectNameResolver) {
+      ProjectCache projectCache,
+      ProjectNameResolver projectNameResolver) {
     this.quotaFinder = quotaFinder;
     this.cache = cache;
     this.projectCache = projectCache;
@@ -116,12 +115,10 @@
         maxPackSize2 = Math.max(0, maxTotalSize - totalSize);
       }
 
-      long maxPackSize = Ordering.<Long> natural().nullsLast().min(
-          maxPackSize1, maxPackSize2);
+      long maxPackSize = Ordering.<Long>natural().nullsLast().min(maxPackSize1, maxPackSize2);
       rp.setMaxPackSizeLimit(maxPackSize);
     } catch (ExecutionException e) {
-      log.warn("Couldn't setMaxPackSizeLimit on receive-pack for "
-          + project.get(), e);
+      log.warn("Couldn't setMaxPackSizeLimit on receive-pack for " + project.get(), e);
     }
   }
 
@@ -153,12 +150,11 @@
     private final boolean useGitObjectCount;
 
     @Inject
-    Loader(GitRepositoryManager gitManager,
-        PluginConfigFactory cfg,
-        @PluginName String pluginName) {
+    Loader(
+        GitRepositoryManager gitManager, PluginConfigFactory cfg, @PluginName String pluginName) {
       this.gitManager = gitManager;
-      this.useGitObjectCount = cfg.getFromGerritConfig(pluginName)
-          .getBoolean("useGitObjectCount", false);
+      this.useGitObjectCount =
+          cfg.getFromGerritConfig(pluginName).getBoolean("useGitObjectCount", false);
     }
 
     @Override
@@ -173,21 +169,22 @@
 
     private static long getDiskUsage(File dir) throws IOException {
       final MutableLong size = new MutableLong();
-      Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
-        @Override
-        public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
-            throws IOException {
-          if (attrs.isRegularFile()) {
-            size.add(attrs.size());
-          }
-          return FileVisitResult.CONTINUE;
-        }
-      });
+      Files.walkFileTree(
+          dir.toPath(),
+          new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
+                throws IOException {
+              if (attrs.isRegularFile()) {
+                size.add(attrs.size());
+              }
+              return FileVisitResult.CONTINUE;
+            }
+          });
       return size.longValue();
     }
 
-    private long getDiskUsageByGitObjectCount(Repository repo)
-        throws IOException {
+    private long getDiskUsageByGitObjectCount(Repository repo) throws IOException {
       RepoStatistics stats = new GC((FileRepository) repo).getStatistics();
       return stats.sizeOfLooseObjects + stats.sizeOfPackedObjects;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
index 935498a..a762579 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
@@ -15,12 +15,14 @@
 package com.googlesource.gerrit.plugins.quota;
 
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
-import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 import static com.googlesource.gerrit.plugins.quota.QuotaResource.QUOTA_KIND;
 
+import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.common.util.concurrent.RateLimiter;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -31,115 +33,189 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.inject.Inject;
+import com.google.inject.Provides;
 import com.google.inject.Scopes;
+import com.google.inject.Singleton;
 import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
 import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.RateLimit;
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.transport.PostReceiveHook;
 
 class Module extends CacheModule {
   static final String CACHE_NAME_ACCOUNTID = "rate_limits_by_account";
   static final String CACHE_NAME_REMOTEHOST = "rate_limits_by_ip";
 
+  private final String uploadpackLimitExceededMsg;
+
+  @Inject
+  Module(PluginConfigFactory plugincf, @PluginName String pluginName) {
+    PluginConfig pc = plugincf.getFromGerritConfig(pluginName);
+    uploadpackLimitExceededMsg =
+        new RateMsgHelper(
+                Type.UPLOADPACK, pc.getString(RateMsgHelper.UPLOADPACK_CONFIGURABLE_MSG_ANNOTATION))
+            .getMessageFormatMsg();
+  }
+
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
         .to(MaxRepositoriesQuotaValidator.class);
-    DynamicSet.bind(binder(), ReceivePackInitializer.class)
-        .to(MaxRepositorySizeQuota.class);
-    DynamicSet.bind(binder(), PostReceiveHook.class)
-        .to(MaxRepositorySizeQuota.class);
-    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(
-        DeletionListener.class);
-    DynamicSet.bind(binder(), GarbageCollectorListener.class).to(
-        GCListener.class);
+    DynamicSet.bind(binder(), ReceivePackInitializer.class).to(MaxRepositorySizeQuota.class);
+    DynamicSet.bind(binder(), PostReceiveHook.class).to(MaxRepositorySizeQuota.class);
+    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(DeletionListener.class);
+    DynamicSet.bind(binder(), GarbageCollectorListener.class).to(GCListener.class);
     DynamicSet.setOf(binder(), UsageDataEventCreator.class);
     install(MaxRepositorySizeQuota.module());
-    install(new RestApiModule() {
-      @Override
-      protected void configure() {
-        DynamicMap.mapOf(binder(), QUOTA_KIND);
-        get(PROJECT_KIND, "quota").to(GetQuota.class);
-        child(CONFIG_KIND, "quota").to(GetQuotas.class);
-      }
-    });
+    install(
+        new RestApiModule() {
+          @Override
+          protected void configure() {
+            DynamicMap.mapOf(binder(), QUOTA_KIND);
+            get(PROJECT_KIND, "quota").to(GetQuota.class);
+            child(CONFIG_KIND, "quota").to(GetQuotas.class);
+          }
+        });
     bind(Publisher.class).in(Scopes.SINGLETON);
     bind(PublisherScheduler.class).in(Scopes.SINGLETON);
     bind(ProjectNameResolver.class).in(Scopes.SINGLETON);
     bind(LifecycleListener.class)
-      .annotatedWith(UniqueAnnotations.create())
-      .to(PublisherScheduler.class);
+        .annotatedWith(UniqueAnnotations.create())
+        .to(PublisherScheduler.class);
 
-    DynamicSet.bind(binder(), UploadValidationListener.class)
-        .to(RateLimitUploadListener.class);
-    cache(CACHE_NAME_ACCOUNTID, Account.Id.class, Holder.class)
-        .loader(LoaderAccountId.class);
-    cache(CACHE_NAME_REMOTEHOST, String.class, Holder.class)
-        .loader(LoaderRemoteHost.class);
+    DynamicSet.bind(binder(), UploadValidationListener.class).to(RateLimitUploadListener.class);
+    bindConstant()
+        .annotatedWith(Names.named(RateMsgHelper.UPLOADPACK_CONFIGURABLE_MSG_ANNOTATION))
+        .to(uploadpackLimitExceededMsg);
   }
 
   static class Holder {
-    public static final Holder EMPTY = new Holder(null);
+    static final Holder EMPTY = new Holder(null);
+    private int burstPermits;
+    private AtomicInteger gracePermits = new AtomicInteger(0);
     private RateLimiter l;
 
-    public Holder(RateLimiter l) {
+    Holder(RateLimiter l) {
       this.l = l;
     }
 
-    public RateLimiter get() {
+    private Holder(RateLimiter l, int burstPermits) {
+      this(l);
+      this.burstPermits = burstPermits;
+      gracePermits.set(burstPermits);
+    }
+
+    RateLimiter get() {
       return l;
     }
+
+    int getBurstPermits() {
+      return burstPermits;
+    }
+
+    /**
+     * The grace permits ensure that a burst of requests can be served as the first interaction with
+     * Gerrit. Without the extra booked burst, particularly the Gerrit web interface would display
+     * an unexpected error, except for inappropriately lax rate limits.
+     *
+     * @return false, once the grace permits have been spent
+     */
+    boolean hasGracePermits() {
+      if (gracePermits.get() <= 0) return false;
+      return gracePermits.getAndDecrement() > 0;
+    }
+
+    private static final Holder createWithBurstyRateLimiter(Optional<RateLimit> limit) {
+      return new Holder(
+          RateLimitUploadListener.createSmoothBurstyRateLimiter(
+              limit.get().getRatePerSecond(), limit.get().getMaxBurstSeconds()),
+          (int) (limit.get().getMaxBurstSeconds() * limit.get().getRatePerSecond()));
+    }
   }
 
-  private static class LoaderAccountId extends CacheLoader<Account.Id, Holder> {
-    private GenericFactory userFactory;
-    private AccountLimitsFinder finder;
+  private abstract static class AbstractHolderCacheLoader<Key> extends CacheLoader<Key, Holder> {
+    protected AccountLimitsFinder finder;
+    protected Type limitsConfigType;
 
-    @Inject
-    LoaderAccountId(IdentifiedUser.GenericFactory userFactory,
-        AccountLimitsFinder finder) {
-      this.userFactory = userFactory;
+    protected AbstractHolderCacheLoader(Type limitsConfigType, AccountLimitsFinder finder) {
+      this.limitsConfigType = limitsConfigType;
       this.finder = finder;
     }
 
-    @Override
-    public Holder load(Account.Id key) throws Exception {
-      IdentifiedUser user = userFactory.create(key);
-      Optional<RateLimit> limit =
-          finder.firstMatching(AccountLimitsConfig.Type.UPLOADPACK, user);
+    protected final Holder createWithBurstyRateLimiter(Optional<RateLimit> limit) throws Exception {
       if (limit.isPresent()) {
-        return new Holder(RateLimitUploadListener.createSmoothBurstyRateLimiter(
-            limit.get().getRatePerSecond(), limit.get().getMaxBurstSeconds()));
+        return Holder.createWithBurstyRateLimiter(limit);
       }
       return Holder.EMPTY;
     }
   }
 
-  private static class LoaderRemoteHost extends CacheLoader<String, Holder> {
-    private AccountLimitsFinder finder;
+  static class HolderCacheLoaderByAccountId extends AbstractHolderCacheLoader<Account.Id> {
+    private GenericFactory userFactory;
+
+    protected HolderCacheLoaderByAccountId(
+        Type limitsConfigType,
+        IdentifiedUser.GenericFactory userFactory,
+        AccountLimitsFinder finder) {
+      super(limitsConfigType, finder);
+      this.userFactory = userFactory;
+    }
+
+    private final Holder createWithBurstyRateLimiter(Account.Id key) throws Exception {
+      return createWithBurstyRateLimiter(
+          finder.firstMatching(limitsConfigType, userFactory.create(key)));
+    }
+
+    @Override
+    public final Holder load(Account.Id key) throws Exception {
+      return createWithBurstyRateLimiter(key);
+    }
+  }
+
+  static class HolderCacheLoaderByRemoteHost extends AbstractHolderCacheLoader<String> {
     private String anonymous;
 
-    @Inject
-    LoaderRemoteHost(SystemGroupBackend systemGroupBackend,
-        AccountLimitsFinder finder) {
-      this.finder = finder;
-      this.anonymous = systemGroupBackend.get(ANONYMOUS_USERS).getName();
+    protected HolderCacheLoaderByRemoteHost(
+        Type limitsConfigType, SystemGroupBackend systemGroupBackend, AccountLimitsFinder finder) {
+      super(limitsConfigType, finder);
+      this.anonymous = systemGroupBackend.get(SystemGroupBackend.ANONYMOUS_USERS).getName();
+    }
+
+    private final Holder createWithBurstyRateLimiter() throws Exception {
+      return createWithBurstyRateLimiter(finder.getRateLimit(limitsConfigType, anonymous));
     }
 
     @Override
-    public Holder load(String key) throws Exception {
-      Optional<RateLimit> limit =
-          finder.getRateLimit(AccountLimitsConfig.Type.UPLOADPACK, anonymous);
-      if (limit.isPresent()) {
-        return new Holder(RateLimitUploadListener.createSmoothBurstyRateLimiter(
-            limit.get().getRatePerSecond(), limit.get().getMaxBurstSeconds()));
-      }
-      return Holder.EMPTY;
+    public final Holder load(String key) throws Exception {
+      return createWithBurstyRateLimiter();
     }
   }
+
+  @Provides
+  @Named(CACHE_NAME_ACCOUNTID)
+  @Singleton
+  public LoadingCache<Account.Id, Module.Holder> getLoadingCacheByAccountId(
+      GenericFactory userFactory, AccountLimitsFinder finder) {
+    return CacheBuilder.newBuilder()
+        .build(new HolderCacheLoaderByAccountId(Type.UPLOADPACK, userFactory, finder));
+  }
+
+  @Provides
+  @Named(CACHE_NAME_REMOTEHOST)
+  @Singleton
+  public LoadingCache<String, Module.Holder> getLoadingCacheByRemoteHost(
+      SystemGroupBackend systemGroupBackend, AccountLimitsFinder finder) {
+    return CacheBuilder.newBuilder()
+        .build(new HolderCacheLoaderByRemoteHost(Type.UPLOADPACK, systemGroupBackend, finder));
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Namespace.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Namespace.java
index 8a2c6f8..9b018ec 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Namespace.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Namespace.java
@@ -31,8 +31,7 @@
   public boolean matches(Project.NameKey project) {
     String p = project.get();
     if (namespace.endsWith("/*")) {
-      return p.startsWith(
-          namespace.substring(0, namespace.length() - 1));
+      return p.startsWith(namespace.substring(0, namespace.length() - 1));
     } else if (namespace.startsWith("^")) {
       return p.matches(namespace.substring(1));
     } else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/ProjectNameResolver.java b/src/main/java/com/googlesource/gerrit/plugins/quota/ProjectNameResolver.java
index 5d4fbcd..038a201 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/ProjectNameResolver.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/ProjectNameResolver.java
@@ -19,25 +19,21 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-
+import java.nio.file.Path;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.nio.file.Path;
-
 @Singleton
 class ProjectNameResolver {
 
-  private static final Logger log = LoggerFactory
-      .getLogger(ProjectNameResolver.class);
+  private static final Logger log = LoggerFactory.getLogger(ProjectNameResolver.class);
   private final Path basePath;
 
   @Inject
   ProjectNameResolver(SitePaths site, @GerritServerConfig final Config cfg) {
-    this.basePath =
-        site.resolve(cfg.getString("gerrit", null, "basePath"));
+    this.basePath = site.resolve(cfg.getString("gerrit", null, "basePath"));
   }
 
   Project.NameKey projectName(Repository repo) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
index 71181e4..319c2eb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
@@ -19,12 +19,10 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.util.ArrayList;
 import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Publisher implements Runnable {
@@ -44,7 +42,7 @@
 
   @Override
   public void run() {
-    if(!listeners.iterator().hasNext()) {
+    if (!listeners.iterator().hasNext()) {
       return;
     }
 
@@ -68,5 +66,4 @@
       }
     }
   }
-
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/PublisherScheduler.java b/src/main/java/com/googlesource/gerrit/plugins/quota/PublisherScheduler.java
index 1f17110..bfa95f3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/PublisherScheduler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/PublisherScheduler.java
@@ -21,13 +21,11 @@
 import com.google.gerrit.server.config.ScheduleConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
-
+import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.concurrent.TimeUnit;
-
 public class PublisherScheduler implements LifecycleListener {
 
   private static final Logger log = LoggerFactory.getLogger(PublisherScheduler.class);
@@ -36,11 +34,11 @@
   private final ScheduleConfig scheduleConfig;
 
   @Inject
-  PublisherScheduler(WorkQueue workQueue, Publisher publisher,  @GerritServerConfig Config cfg) {
+  PublisherScheduler(WorkQueue workQueue, Publisher publisher, @GerritServerConfig Config cfg) {
     this.workQueue = workQueue;
     this.publisher = publisher;
-    scheduleConfig = new ScheduleConfig(cfg, "plugin", "quota", "publicationInterval",
-        "publicationStartTime");
+    scheduleConfig =
+        new ScheduleConfig(cfg, "plugin", "quota", "publicationInterval", "publicationStartTime");
   }
 
   @Override
@@ -52,13 +50,12 @@
     } else if (delay < 0 || interval <= 0) {
       log.warn("Ignoring invalid schedule configuration");
     } else {
-      workQueue.getDefaultQueue().scheduleAtFixedRate(publisher, delay,
-          interval, TimeUnit.MILLISECONDS);
+      workQueue
+          .getDefaultQueue()
+          .scheduleAtFixedRate(publisher, delay, interval, TimeUnit.MILLISECONDS);
     }
   }
 
   @Override
-  public void stop() {
-  }
-
+  public void stop() {}
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
index f748e64..f32e1ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
@@ -17,12 +17,10 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
-
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
 
 public class QuotaFinder {
   private final ProjectCache projectCache;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
index 6419258..07e6e20 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.quota;
 
 import com.google.gerrit.reviewdb.client.Project;
-
 import org.eclipse.jgit.lib.Config;
 
 public class QuotaSection {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java
index 8d5a9b9..0b36ebb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java
@@ -14,90 +14,70 @@
 
 package com.googlesource.gerrit.plugins.quota;
 
-import static com.googlesource.gerrit.plugins.quota.Module.CACHE_NAME_ACCOUNTID;
-import static com.googlesource.gerrit.plugins.quota.Module.CACHE_NAME_REMOTEHOST;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.LoadingCache;
 import com.google.common.util.concurrent.RateLimiter;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
-
 import com.googlesource.gerrit.plugins.quota.Module.Holder;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.UploadPack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.text.MessageFormat;
 import java.util.Collection;
 import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class RateLimitUploadListener implements UploadValidationListener {
   private static final int SECONDS_PER_HOUR = 3600;
-  private static final Logger log =
-      LoggerFactory.getLogger(RateLimitUploadListener.class);
+  private static final Logger log = LoggerFactory.getLogger(RateLimitUploadListener.class);
   private static final Method createStopwatchMethod;
   private static final Constructor<?> constructor;
-  private static final String RATE_LIMIT_TOKEN = "${rateLimit}";
-  private static final String DEFAULT_RATE_LIMIT_EXCEEDED_MSG =
-      "Exceeded rate limit of " + RATE_LIMIT_TOKEN + " fetch requests/hour";
 
   static {
     try {
-      Class<?> sleepingStopwatchClass = Class.forName(
-          "com.google.common.util.concurrent.RateLimiter$SleepingStopwatch");
-      createStopwatchMethod =
-          sleepingStopwatchClass.getDeclaredMethod("createFromSystemTimer");
+      Class<?> sleepingStopwatchClass =
+          Class.forName("com.google.common.util.concurrent.RateLimiter$SleepingStopwatch");
+      createStopwatchMethod = sleepingStopwatchClass.getDeclaredMethod("createFromSystemTimer");
       createStopwatchMethod.setAccessible(true);
-      Class<?> burstyRateLimiterClass = Class.forName(
-          "com.google.common.util.concurrent.SmoothRateLimiter$SmoothBursty");
+      Class<?> burstyRateLimiterClass =
+          Class.forName("com.google.common.util.concurrent.SmoothRateLimiter$SmoothBursty");
       constructor = burstyRateLimiterClass.getDeclaredConstructors()[0];
       constructor.setAccessible(true);
     } catch (ClassNotFoundException | NoSuchMethodException e) {
       // shouldn't happen
-      throw new RuntimeException(
-          "Failed to prepare loading RateLimiter via reflection", e);
+      throw new RuntimeException("Failed to prepare loading RateLimiter via reflection", e);
     }
   }
 
   /**
-   * Create a custom instance of RateLimiter by accessing the non-public
-   * constructor of the implementation class SmoothRateLimiter.SmoothBursty
-   * through reflection.
+   * Create a custom instance of RateLimiter by accessing the non-public constructor of the
+   * implementation class SmoothRateLimiter.SmoothBursty through reflection.
    *
-   * <p>
-   * RateLimiter's implementation class SmoothRateLimiter.SmoothBursty allows to
-   * collect permits during idle times which can be used to send bursts of
-   * requests exceeding the average rate until the stored permits are consumed.
-   * If the rate per second is 0.2 and you wait 20 seconds you can acquire 4
-   * permits which in average matches the configured rate limit of 0.2
-   * requests/second. If the permitted rate is smaller than 1 per second the
-   * standard implementation doesn't allow any bursts since it hard-codes the
-   * maximum time which can be used to collect stored permits to 1 second.
+   * <p>RateLimiter's implementation class SmoothRateLimiter.SmoothBursty allows to collect permits
+   * during idle times which can be used to send bursts of requests exceeding the average rate until
+   * the stored permits are consumed. If the rate per second is 0.2 and you wait 20 seconds you can
+   * acquire 4 permits which in average matches the configured rate limit of 0.2 requests/second. If
+   * the permitted rate is smaller than 1 per second the standard implementation doesn't allow any
+   * bursts since it hard-codes the maximum time which can be used to collect stored permits to 1
+   * second.
    *
-   * <p>
-   * Build jobs fetching updates from Gerrit are typically triggered by events
-   * which can arrive in bursts. Hence the standard RateLimiter seems not to be
-   * the right choice at least for fetch requests where we probably want to
-   * limit the rate to less than 1 request per second per user.
+   * <p>Build jobs fetching updates from Gerrit are typically triggered by events which can arrive
+   * in bursts. Hence the standard RateLimiter seems not to be the right choice at least for fetch
+   * requests where we probably want to limit the rate to less than 1 request per second per user.
    *
-   * <p>
-   * The used constructor can't be accessed through a public method yet hence
-   * use reflection to instantiate it.
+   * <p>The used constructor can't be accessed through a public method yet hence use reflection to
+   * instantiate it.
    *
    * @see "https://github.com/google/guava/issues/1974"
    * @param permitsPerSecond the new stable rate of this {@code RateLimiter}
@@ -105,15 +85,14 @@
    * @return a new RateLimiter
    */
   @VisibleForTesting
-  static RateLimiter createSmoothBurstyRateLimiter(double permitsPerSecond,
-      double maxBurstSeconds) {
+  static RateLimiter createSmoothBurstyRateLimiter(
+      double permitsPerSecond, double maxBurstSeconds) {
     RateLimiter rl;
     try {
       Object stopwatch = createStopwatchMethod.invoke(null);
       rl = (RateLimiter) constructor.newInstance(stopwatch, maxBurstSeconds);
       rl.setRate(permitsPerSecond);
-    } catch (InvocationTargetException | IllegalAccessException
-        | InstantiationException e) {
+    } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) {
       // shouldn't happen
       throw new RuntimeException(e);
     }
@@ -126,23 +105,26 @@
   private final String limitExceededMsg;
 
   @Inject
-  RateLimitUploadListener(Provider<CurrentUser> user,
-      @Named(CACHE_NAME_ACCOUNTID) LoadingCache<Account.Id, Holder> limitsPerAccount,
-      @Named(CACHE_NAME_REMOTEHOST) LoadingCache<String, Holder> limitsPerRemoteHost,
-      PluginConfigFactory cfg,
-      @PluginName String pluginName) {
+  RateLimitUploadListener(
+      Provider<CurrentUser> user,
+      @Named(Module.CACHE_NAME_ACCOUNTID) LoadingCache<Account.Id, Holder> limitsPerAccount,
+      @Named(Module.CACHE_NAME_REMOTEHOST) LoadingCache<String, Holder> limitsPerRemoteHost,
+      @Named(RateMsgHelper.UPLOADPACK_CONFIGURABLE_MSG_ANNOTATION) String limitExceededMsg) {
     this.user = user;
     this.limitsPerAccount = limitsPerAccount;
     this.limitsPerRemoteHost = limitsPerRemoteHost;
-    String msg = cfg.getFromGerritConfig(pluginName).getString(
-        "uploadpackLimitExceededMsg", DEFAULT_RATE_LIMIT_EXCEEDED_MSG);
-    limitExceededMsg = msg.replace(RATE_LIMIT_TOKEN, "{0,number,##.##}");
+    this.limitExceededMsg = limitExceededMsg;
   }
 
   @Override
-  public void onBeginNegotiate(Repository repository, Project project,
-      String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
-      int cntOffered) throws ValidationException {
+  public void onBeginNegotiate(
+      Repository repository,
+      Project project,
+      String remoteHost,
+      UploadPack up,
+      Collection<? extends ObjectId> wants,
+      int cntOffered)
+      throws ValidationException {
     RateLimiter limiter = null;
     CurrentUser u = user.get();
     if (u.isIdentifiedUser()) {
@@ -150,30 +132,32 @@
       try {
         limiter = limitsPerAccount.get(accountId).get();
       } catch (ExecutionException e) {
-        String msg = MessageFormat
-            .format("Cannot get rate limits for account ''{}''", accountId);
+        String msg = MessageFormat.format("Cannot get rate limits for account ''{0}''", accountId);
         log.warn(msg, e);
       }
     } else {
       try {
         limiter = limitsPerRemoteHost.get(remoteHost).get();
       } catch (ExecutionException e) {
-        String msg = MessageFormat.format(
-            "Cannot get rate limits for anonymous access from remote host ''{0}''",
-            remoteHost);
+        String msg =
+            MessageFormat.format(
+                "Cannot get rate limits for anonymous access from remote host ''{0}''", remoteHost);
         log.warn(msg, e);
       }
     }
     if (limiter != null && !limiter.tryAcquire()) {
       throw new RateLimitException(
-          MessageFormat.format(limitExceededMsg,
-              limiter.getRate() * SECONDS_PER_HOUR));
+          MessageFormat.format(limitExceededMsg, limiter.getRate() * SECONDS_PER_HOUR));
     }
   }
 
   @Override
-  public void onPreUpload(Repository repository, Project project,
-      String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
-      Collection<? extends ObjectId> haves) throws ValidationException {
-  }
+  public void onPreUpload(
+      Repository repository,
+      Project project,
+      String remoteHost,
+      UploadPack up,
+      Collection<? extends ObjectId> wants,
+      Collection<? extends ObjectId> haves)
+      throws ValidationException {}
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RateMsgHelper.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RateMsgHelper.java
new file mode 100644
index 0000000..9640d06
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RateMsgHelper.java
@@ -0,0 +1,70 @@
+// 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.quota;
+
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type;
+
+public class RateMsgHelper {
+  public static final String UPLOADPACK_CONFIGURABLE_MSG_ANNOTATION = "uploadpackLimitExceededMsg";
+  public static final String RESTAPI_CONFIGURABLE_MSG_ANNOTATION = "restapiLimitExceededMsg";
+  private static final String RATE_LIMIT_TOKEN = "${rateLimit}";
+  private static final String BURSTS_LIMIT_TOKEN = "${burstsLimit}";
+  private static final String RATE_LIMIT_FORMAT_DOUBLE = "{0,number,##.##}";
+  private static final String RATE_LIMIT_FORMAT_INT = "{1,number,###}";
+  private static final String UPLOADPACK_INLINE_NAME = "fetch";
+  private static final String RESTAPI_INLINE_NAME = "REST API";
+
+  private static String getDefaultTemplateMsg(String rateLimitTypeName) {
+    return "Exceeded rate limit of "
+        + RATE_LIMIT_TOKEN
+        + " "
+        + rateLimitTypeName
+        + " requests/hour";
+  }
+
+  private static String getDefaultTemplateMsgWithBursts(String rateLimitTypeName) {
+    return "Exceeded rate limit of "
+        + RATE_LIMIT_TOKEN
+        + " "
+        + rateLimitTypeName
+        + " requests/hour (or idle time used up in bursts of max "
+        + BURSTS_LIMIT_TOKEN
+        + " requests)";
+  }
+
+  private String messageFormatMsg;
+  private String messageFormatMsgWithBursts;
+
+  public RateMsgHelper(Type limitsConfigType, String templateMsg) {
+    String rateLimitTypeName =
+        limitsConfigType == Type.UPLOADPACK ? UPLOADPACK_INLINE_NAME : RESTAPI_INLINE_NAME;
+    messageFormatMsg = templateMsg == null ? getDefaultTemplateMsg(rateLimitTypeName) : templateMsg;
+    messageFormatMsg = messageFormatMsg.replace(RATE_LIMIT_TOKEN, RATE_LIMIT_FORMAT_DOUBLE);
+    messageFormatMsgWithBursts =
+        templateMsg == null ? getDefaultTemplateMsgWithBursts(rateLimitTypeName) : templateMsg;
+    messageFormatMsgWithBursts =
+        messageFormatMsgWithBursts
+            .replace(RATE_LIMIT_TOKEN, RATE_LIMIT_FORMAT_DOUBLE)
+            .replace(BURSTS_LIMIT_TOKEN, RATE_LIMIT_FORMAT_INT);
+  }
+
+  public String getMessageFormatMsg() {
+    return messageFormatMsg;
+  }
+
+  public String getMessageFormatMsgWithBursts() {
+    return messageFormatMsgWithBursts;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java
index 9baf36a..08a9959 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java
@@ -24,15 +24,14 @@
 @Singleton
 public class RepoSizeEventCreator implements UsageDataEventCreator {
 
-  private static final MetaData REPO_SIZE = new MetaDataImpl("repoSize", "byte", "B",
-      "total file size of the repository");
+  private static final MetaData REPO_SIZE =
+      new MetaDataImpl("repoSize", "byte", "B", "total file size of the repository");
 
   private final ProjectCache projectCache;
   private final RepoSizeCache repoSizeCache;
 
   @Inject
-  public RepoSizeEventCreator(ProjectCache projectCache,
-      RepoSizeCache repoSizeCache) {
+  public RepoSizeEventCreator(ProjectCache projectCache, RepoSizeCache repoSizeCache) {
     this.projectCache = projectCache;
     this.repoSizeCache = repoSizeCache;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiter.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiter.java
new file mode 100644
index 0000000..eb9c493
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiter.java
@@ -0,0 +1,118 @@
+// 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.quota;
+
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.quota.Module.Holder;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Pattern;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class RestApiRateLimiter extends AllRequestFilter {
+  private static final Logger log = LoggerFactory.getLogger(RestApiRateLimiter.class);
+  private static final int SECONDS_PER_HOUR = 3600;
+
+  static final int SC_TOO_MANY_REQUESTS = 429;
+
+  private final Provider<CurrentUser> user;
+  private final LoadingCache<Account.Id, Holder> limitsPerAccount;
+  private final LoadingCache<String, Holder> limitsPerRemoteHost;
+
+  private final Pattern resturi =
+      Pattern.compile(
+          "^/(?:a/)?"
+              + "(access|accounts|changes|config|groups|plugins|projects|Documentation|tools)/(.*)$");
+
+  private final String limitExceededMsg;
+
+  @Inject
+  RestApiRateLimiter(
+      Provider<CurrentUser> user,
+      @Named(HttpModule.CACHE_NAME_RESTAPI_ACCOUNTID)
+          LoadingCache<Account.Id, Holder> limitsPerAccount,
+      @Named(HttpModule.CACHE_NAME_RESTAPI_REMOTEHOST)
+          LoadingCache<String, Holder> limitsPerRemoteHost,
+      @Named(RateMsgHelper.RESTAPI_CONFIGURABLE_MSG_ANNOTATION) String limitExceededMsg) {
+    this.user = user;
+    this.limitsPerAccount = limitsPerAccount;
+    this.limitsPerRemoteHost = limitsPerRemoteHost;
+    this.limitExceededMsg = limitExceededMsg;
+  }
+
+  @Override
+  public void doFilter(ServletRequest req, ServletResponse res, final FilterChain chain)
+      throws IOException, ServletException {
+    if (isRest(req)) {
+      Holder rateLimiterHolder;
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        Account.Id accountId = u.asIdentifiedUser().getAccountId();
+        try {
+          rateLimiterHolder = limitsPerAccount.get(accountId);
+        } catch (ExecutionException e) {
+          rateLimiterHolder = Holder.EMPTY;
+          String msg =
+              MessageFormat.format("Cannot get rate limits for account ''{0}''", accountId);
+          log.warn(msg, e);
+        }
+      } else {
+        try {
+          rateLimiterHolder = limitsPerRemoteHost.get(req.getRemoteHost());
+        } catch (ExecutionException e) {
+          rateLimiterHolder = Holder.EMPTY;
+          String msg =
+              MessageFormat.format(
+                  "Cannot get rate limits for anonymous access from remote host ''{0}''",
+                  req.getRemoteHost());
+          log.warn(msg, e);
+        }
+      }
+      if (!rateLimiterHolder.hasGracePermits()
+          && rateLimiterHolder.get() != null
+          && !rateLimiterHolder.get().tryAcquire()) {
+        String msg =
+            MessageFormat.format(
+                limitExceededMsg,
+                rateLimiterHolder.get().getRate() * SECONDS_PER_HOUR,
+                rateLimiterHolder.getBurstPermits());
+        ((HttpServletResponse) res).sendError(SC_TOO_MANY_REQUESTS, msg);
+        return;
+      }
+    }
+    chain.doFilter(req, res);
+  }
+
+  boolean isRest(ServletRequest req) {
+    return req instanceof HttpServletRequest
+        && resturi.matcher(((HttpServletRequest) req).getRequestURI()).matches();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java
index d37709c..3dc86de 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.Data;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.MetaData;
-
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
@@ -35,18 +34,19 @@
   }
 
   void addData(final long value, final String projectName) {
-    Data dataRow = new Data() {
+    Data dataRow =
+        new Data() {
 
-      @Override
-      public long getValue() {
-        return value;
-      }
+          @Override
+          public long getValue() {
+            return value;
+          }
 
-      @Override
-      public String getProjectName() {
-        return projectName;
-      }
-    };
+          @Override
+          public String getProjectName() {
+            return projectName;
+          }
+        };
 
     data.add(dataRow);
   }
@@ -58,11 +58,11 @@
 
   @Override
   public Timestamp getInstant() {
-    return timestamp  ;
+    return timestamp;
   }
 
   @Override
   public List<Data> getData() {
     return data;
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java
index 56db9b3..4a42ead 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java
@@ -21,5 +21,4 @@
   String getName();
 
   UsageDataPublishedListener.Event create();
-
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index a20a452..fda7ec7 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -9,13 +9,15 @@
 
 * Maximum number of projects in a namespace
 * The maximum total file size of a repository in a namespace
+* The maximum total file size of all repositories in a namespace
 
 The measured repository sizes can be published periodically to registered
 UsageDataPublishedListeners.
 
-The @PLUGIN@ plugin supports the following  rate limits:
+The @PLUGIN@ plugin supports the following rate limits:
 
 * `uploadpack` requests which are executed when a client runs a fetch command.
+* Maximum number of REST API calls
 
 Rate limits define the maximum request rate for users in a given group
 for a given request type.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 61e4a77..161013f 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,11 +22,11 @@
     maxTotalSize = 200 m
 ```
 
-<a id="maxProjects">
+<a id="maxProjects" />
 `quota.<namespace>.maxProjects`
 : The maximum number of projects that can be created in this namespace.
 
-<a id="maxRepoSize">
+<a id="maxRepoSize" />
 `quota.<namespace>.maxRepoSize`
 : The maximum total file size of a repository in this namespace. This is
 the sum of sizes of all files in a Git repository where the size is
@@ -34,7 +34,7 @@
 reference file is counted as 41 bytes although it typically occupies a
 block of 4K in the file system.
 
-<a id="maxTotalSize">
+<a id="maxTotalSize" />
 `quota.<namespace>.maxTotalSize`
 : The maximum total file size of all repositories in this namespace.
 This is the sum of sizes of all files in all Git repositories in this
@@ -58,11 +58,11 @@
 namespace matching the regular expression.
 
 * for-each-pattern (`?/*`): Defines the same quota for each
-subfolder. `?` is a placeholder for any name and '?/*' with
+subfolder. `?` is a placeholder for any name and `?/*` with
 'maxProjects = 3' means that for every subfolder 3 projects are
-allowed. Hence '?/*' is a shortcut for having n explicit quotas:
-  '<name1>/*' with 'maxProjects = 3'
-  '<name2>/*' with 'maxProjects = 3'
+allowed. Hence `?/*` is a shortcut for having n explicit quotas:<br />
+  `<name1>/*` with 'maxProjects = 3'<br />
+  `<name2>/*` with 'maxProjects = 3'<br />
   ...
 
 
@@ -90,7 +90,7 @@
     maxProjects = 5
 ```
 
-Example: Allow the creation of 10 projects in folder 'test/*' and set
+Example: Allow the creation of 10 projects in folder `test/*` and set
 the quota of 2m for each of them
 
 ```
@@ -99,7 +99,7 @@
     maxRepoSize = 2 m
 ```
 
-Example: Allow the creation of 10 projects in folder 'test/*' and set
+Example: Allow the creation of 10 projects in folder `test/*` and set
 a quota of 20m for the total size of all repositories
 
 ```
@@ -108,7 +108,7 @@
     maxTotalSize = 20 m
 ```
 
-Example: Allow the creation of 10 projects in folder 'test/*' and set
+Example: Allow the creation of 10 projects in folder `test/*` and set
 a quota of 20m for the total size of all repositories. In addition make
 sure that each individual repository cannot exceed 3m
 
@@ -127,7 +127,7 @@
     useGitObjectCount = true
 ```
 
-<a id="useGitObjectCount">
+<a id="useGitObjectCount" />
 `plugin.quota.useGitObjectCount`
 : Use git object count. If true, *repoSize = looseObjectsSize +
 packedObjectsSize*, where *looseObjectsSize* and *packedObjectsSize* are given
@@ -136,9 +136,9 @@
 Rate Limits
 -----------
 
-The defined rate limits are stored in a `quota.config` file in the
+The defined rate limits are stored in the `quota.config` file in the
 `refs/meta/config` branch of the `All-Projects` root project. Rate
-limits are defined per user group and rate limit type:
+limits are defined per user group and rate limit type.
 
 Example:
 
@@ -146,11 +146,15 @@
   [group "buildserver"]
     uploadpack = 10 / min burst 500
 
+  [group "app"]
+    restapi = 12 / min burst 60
+
   [group "Registered Users"]
     uploadpack = 1 /min burst 180
 
   [group "Anonymous Users"]
     uploadpack = 6/h burst 12
+    restapi = 30/m burst 200
 ```
 
 For logged in users rate limits are associated to their accountId. For
@@ -161,50 +165,52 @@
 If a user is a member of multiple groups mentioned in `quota.config`
 the limit applies that is defined first in the `quota.config` file.
 This resolves ambiguity in case the user is a member of multiple groups
-used in the configuration.
+used in the configuration. Note, all users are members of "Anonymous Users".
 
 Use group "Anonymous Users" to define the rate limit for anonymous users.
 Use group "Registered Users" to define the default rate limit for all logged
 in users.
 
 Format of the rate limit entries in `quota.config`:
+
 ```
   [group "<groupName>"]
     <rateLimitType> = <rateLimit> <rateUnit> burst <storedRequests>
 ```
 
-<a id="rateLimitType>">
+The group can be defined by its name or UUID.
+
+<a id="rateLimitType" />
 `group.<groupName>.<rateLimitType>`
 : identifies which request type is limited by this configuration.
 The following rate limit types are supported:
 * `uploadpack`: rate limit for uploadpack (fetch) requests
-The group can be defined by its name or UUID.
+for the given group
+* `restapi`: rate limit for REST API requests
 
-<a id="uploadpack">
-`group.<groupName>.uploadpack`
-: rate limit for uploadpack (fetch) requests for the given group. The
-group can be defined by its name or UUID.
-
-<a id="rateLimit">
+<a id="rateLimit" />
+`group.<groupName>.<rateLimit>`
 : The rate limit (first parameter) defines the maximum allowed request rate.
 
-<a id="rateUnit">
-: Rate limits can be defined using the following rate units:
-`/s`, `/sec`, `/second`: requests per second
-`/m`, `/min`, `/minute`: requests per minute
-`/h`, `/hr`, `/hour`: requests per hour
-`/d`, `/day`: requests per day
+<a id="rateUnit" />
+`group.<groupName>.<rateUnit>`
+: Rate limits can be defined using the following rate units:<br />
+`/s`, `/sec`, `/second`: requests per second<br />
+`/m`, `/min`, `/minute`: requests per minute<br />
+`/h`, `/hr`, `/hour`: requests per hour<br />
+`/d`, `/day`: requests per day<br />
 
-The default unit used if no unit is configured is `/hour`.
-
-<a id="burst">
-The `burst` parameter allows to define how many unused requests can be
+<a id="burst" />
+`group.<groupName>.<storedRequests>`
+: The `burst` parameter allows to define how many unused requests can be
 stored for later use during idle times. This allows clients to send
 bursts of requests exceeding their rate limit until all their stored
-requests are consumed.
+requests are consumed. For `restapi`, `burst` requests can be served
+at the very beginning of a client interaction with the back-end server,
+as if idle time would already have been accumulated.
 
-If a rate limit configuration value is invalid a default rate limit of 1
-request per minute with 30 stored requests is assumed.
+If a rate limit configuration value is invalid or missing for a group,
+that value is ignored and a warning is logged.
 
 Example:
 
@@ -217,12 +223,22 @@
   [group "Registered Users"]
     uploadpack = 30/hour burst 60
 ```
-The rate limit exceeded message can be configured by setting parameter
+The rate limit exceeded message can be configured.
+
+For `uploadpack`, by setting parameter
 `uploadpackLimitExceededMsg` in the `plugin.quota` subsection of the
 `gerrit.config` file. `${rateLimit}` token is supported in the message and
 will be replaced by effective rate limit per hour.
+Defaults to `Exceeded rate limit of ${rateLimit} fetch requests/hour` .
 
-Defaults to `Exceeded rate limit of ${rateLimit} fetch requests/hour`
+For `restapi`, configure the message by setting the parameter
+`restapiLimitExceededMsg` in the `plugin.quota` subsection of the
+`gerrit.config` file. `${rateLimit}` and `${burstsLimit}` tokens
+are supported in the message and will be replaced by the effective rate
+limit per hour and the effective number of burst permits, correspondingly.
+The default message reads:
+`Exceeded rate limit of ${rateLimit} REST API requests/hour (or idle `
+`time used up in bursts of max ${burstsLimit} requests)` .
 
 Publication Schedule
 --------------------
diff --git a/src/main/resources/Documentation/rest-api-projects.md b/src/main/resources/Documentation/rest-api-projects.md
index e0fdacc..5f96f9e 100644
--- a/src/main/resources/Documentation/rest-api-projects.md
+++ b/src/main/resources/Documentation/rest-api-projects.md
@@ -7,10 +7,12 @@
 Please also take note of the general information on the
 [REST API](../../../Documentation/rest-api.html).
 
-<a id="project-endpoints"> Quota Endpoints
+<a id="project-endpoints" />
+Quota Endpoints
 ------------------------------------------
 
-### <a id="get-quota"> Get Quota
+<a id="get-quota" />
+### Get Quota
 _GET /projects/\{project\}/@PLUGIN@~quota/_
 
 Get quota for a project.
@@ -43,7 +45,8 @@
   }
 ```
 
-### <a id="get-quotas"> Get Quotas for Multiple Projects
+<a id="get-quotas" />
+### Get Quotas for Multiple Projects
 _GET /config/server/@PLUGIN@~quota/_
 
 Get quota for the projects accessible by the caller.
@@ -118,10 +121,12 @@
   }
 ```
 
-<a id="json-entities">JSON Entities
+<a id="json-entities" />
+JSON Entities
 -----------------------------------
 
-### <a id="quota-info"></a>QuotaInfo
+<a id="quota-info" />
+### QuotaInfo
 
 The `QuotaInfo` entity contains information about a project's quota.
 It has the following fields:
@@ -130,8 +135,8 @@
 * _max\_repo\_size_: The max allowed size of this project's Git repositoriy on the disk.
 * _namespace_: [NamespaceInfo](#namespace-info)
 
-
-### <a id="namespace-info"></a>NamespaceInfo
+<a id="namespace-info" />
+### NamespaceInfo
 
 The 'NamespaceInfo' entity contains the quota information for the whole namespace.
 This means that the sum of sizes of all repositories under that namespace is not
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/DeletionListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/DeletionListenerTest.java
index 4620323..034f3b2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/DeletionListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/DeletionListenerTest.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
-
 import org.junit.Test;
 
 public class DeletionListenerTest {
@@ -42,20 +41,20 @@
     DeletionListener classUnderTest = new DeletionListener(repoSizeCache);
     replay(repoSizeCache);
 
-    ProjectDeletedListener.Event event = new ProjectDeletedListener.Event() {
-      @Override
-      public String getProjectName() {
-        return MY_PROJECT;
-      }
+    ProjectDeletedListener.Event event =
+        new ProjectDeletedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return MY_PROJECT;
+          }
 
-      @Override
-      public NotifyHandling getNotify() {
-        return NotifyHandling.ALL;
-      }
-    };
+          @Override
+          public NotifyHandling getNotify() {
+            return NotifyHandling.ALL;
+          }
+        };
     classUnderTest.onProjectDeleted(event);
 
     verify(repoSizeCache);
   }
-
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/GCListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/GCListenerTest.java
index f8df792..4e397ba 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/GCListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/GCListenerTest.java
@@ -24,10 +24,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
-
-import org.junit.Test;
-
 import java.util.Properties;
+import org.junit.Test;
 
 public class GCListenerTest {
   static {
@@ -104,5 +102,4 @@
         };
     return event;
   }
-
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java
index 28d2ef7..f2d84c5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.events.UsageDataPublishedListener;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
 import com.google.gerrit.extensions.registration.DynamicSet;
-
 import org.apache.log4j.Appender;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
@@ -77,7 +76,7 @@
     assertTrue(captor.hasCaptured());
     LoggingEvent event = captor.getValue();
     assertEquals(Level.WARN, event.getLevel());
-    assertTrue(((String)event.getMessage()).contains(CREATOR_NAME));
+    assertTrue(((String) event.getMessage()).contains(CREATOR_NAME));
   }
 
   @Test
@@ -139,5 +138,4 @@
 
     verify(listener, good, creator, appender);
   }
-
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java
index fd41489..b86e87d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.events.UsageDataPublishedListener;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
 import com.google.gerrit.extensions.registration.DynamicSet;
-
 import org.junit.Test;
 
 public class PublisherTest {
@@ -42,8 +41,7 @@
     creators.add(c1);
     creators.add(c2);
 
-    UsageDataPublishedListener listener =
-        createMock(UsageDataPublishedListener.class);
+    UsageDataPublishedListener listener = createMock(UsageDataPublishedListener.class);
     listener.onUsageDataPublished(e1);
     expectLastCall();
     listener.onUsageDataPublished(e2);
@@ -67,13 +65,11 @@
     DynamicSet<UsageDataEventCreator> creators = DynamicSet.emptySet();
     creators.add(creator);
 
-    UsageDataPublishedListener l1 =
-        createMock(UsageDataPublishedListener.class);
+    UsageDataPublishedListener l1 = createMock(UsageDataPublishedListener.class);
     l1.onUsageDataPublished(event);
     expectLastCall();
 
-    UsageDataPublishedListener l2 =
-        createMock(UsageDataPublishedListener.class);
+    UsageDataPublishedListener l2 = createMock(UsageDataPublishedListener.class);
     l2.onUsageDataPublished(event);
     expectLastCall();
 
@@ -102,5 +98,4 @@
 
     verify(creator);
   }
-
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListenerTest.java
new file mode 100644
index 0000000..a6f2a47
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListenerTest.java
@@ -0,0 +1,127 @@
+// 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.quota;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.RateLimiter;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.quota.Module.Holder;
+import java.util.concurrent.ExecutionException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RateLimitUploadListenerTest {
+  private static final String LIMIT_EXCEEDED_MSG = "test exceeded message: {0,number,##.##}";
+  private static final String REMOTE_HOST = "host";
+  private RateLimitUploadListener uploadHook;
+  @Mock private Provider<CurrentUser> user;
+  @Mock private LoadingCache<Account.Id, Holder> limitsPerAccount;
+  @Mock private LoadingCache<String, Holder> limitsPerRemoteHost;
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private CurrentUser currentUser;
+
+  @Mock private Account.Id accountId;
+  @Mock private Holder holder;
+  @Mock private RateLimiter limiter;
+
+  @Before
+  public void setUp() {
+    uploadHook =
+        spy(
+            new RateLimitUploadListener(
+                user, limitsPerAccount, limitsPerRemoteHost, LIMIT_EXCEEDED_MSG));
+    when(user.get()).thenReturn(currentUser);
+  }
+
+  private void setUpRegisteredUser() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(true);
+    when(currentUser.asIdentifiedUser().getAccountId()).thenReturn(accountId);
+    when(limitsPerAccount.get(accountId)).thenReturn(holder);
+    when(holder.get()).thenReturn(limiter);
+  }
+
+  private void setUpRegisteredUserExecutionException() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(true);
+    when(currentUser.asIdentifiedUser().getAccountId()).thenReturn(accountId);
+    when(limitsPerAccount.get(accountId)).thenThrow(new ExecutionException(null));
+    when(accountId.toString()).thenReturn("123");
+  }
+
+  private void setUpAnonymous() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(false);
+    when(limitsPerRemoteHost.get(REMOTE_HOST)).thenReturn(holder);
+    when(holder.get()).thenReturn(limiter);
+  }
+
+  private void setUpNoQuotaViolation() {
+    when(limiter.tryAcquire()).thenReturn(true);
+  }
+
+  private void setUpQuotaViolation() {
+    when(limiter.tryAcquire()).thenReturn(false);
+  }
+
+  @Test(expected = RateLimitException.class)
+  public void testNegotiationQuotaViolation() throws ExecutionException, ValidationException {
+    setUpRegisteredUser();
+    setUpQuotaViolation();
+    uploadHook.onBeginNegotiate(null, null, REMOTE_HOST, null, null, 0);
+  }
+
+  @Test
+  public void testNegotiationNoQuotaViolation() throws ExecutionException, ValidationException {
+    setUpRegisteredUser();
+    setUpNoQuotaViolation();
+    uploadHook.onBeginNegotiate(null, null, REMOTE_HOST, null, null, 0);
+    verify(limiter, times(0)).getRate();
+  }
+
+  @Test
+  public void testNegotiationCacheMiss() throws ExecutionException, ValidationException {
+    setUpRegisteredUserExecutionException();
+    uploadHook.onBeginNegotiate(null, null, REMOTE_HOST, null, null, 0);
+    verify(limiter, times(0)).getRate();
+  }
+
+  @Test(expected = RateLimitException.class)
+  public void testNegotiationAnonymQuotaViolation() throws ExecutionException, ValidationException {
+    setUpAnonymous();
+    setUpQuotaViolation();
+    uploadHook.onBeginNegotiate(null, null, REMOTE_HOST, null, null, 0);
+  }
+
+  @Test
+  public void testNegotiationAnonymNoQuotaViolation()
+      throws ExecutionException, ValidationException {
+    setUpAnonymous();
+    setUpNoQuotaViolation();
+    uploadHook.onBeginNegotiate(null, null, REMOTE_HOST, null, null, 0);
+    verify(limiter, times(0)).getRate();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java
index fc72e9a..1da13f1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java
@@ -27,13 +27,11 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
-
-import org.junit.Before;
-import org.junit.Test;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
 
 public class RepoSizeEventCreatorTest {
 
@@ -62,7 +60,6 @@
     classUnderTest = new RepoSizeEventCreator(projectCache, repoSizeCache);
   }
 
-
   @Test
   public void testEmpty() {
     replay(repoSizeCache);
@@ -86,5 +83,4 @@
     assertEquals("p1", dataPoint.getProjectName());
     assertEquals(100L, dataPoint.getValue());
   }
-
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiterTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiterTest.java
new file mode 100644
index 0000000..64fe0c7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/RestApiRateLimiterTest.java
@@ -0,0 +1,162 @@
+// 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.quota;
+
+import static com.googlesource.gerrit.plugins.quota.RestApiRateLimiter.SC_TOO_MANY_REQUESTS;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.RateLimiter;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.quota.Module.Holder;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RestApiRateLimiterTest {
+  private static final String LIMIT_EXCEEDED_MSG =
+      "test exceeded message: {0,number,##.##}, {1,number,###}";
+  @Mock private HttpServletRequest req;
+  @Mock private HttpServletResponse res;
+  @Mock private FilterChain chain;
+  @Mock private Provider<CurrentUser> user;
+  @Mock private LoadingCache<Account.Id, Holder> limitsPerAccount;
+  @Mock private LoadingCache<String, Holder> limitsPerRemoteHost;
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private CurrentUser currentUser;
+
+  @Mock private Account.Id accountId;
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private Holder holder;
+
+  @Mock private RateLimiter rateLimiter;
+  private RestApiRateLimiter restReqFilter;
+
+  @Before
+  public void setUp() throws IOException, ServletException {
+    restReqFilter =
+        spy(
+            new RestApiRateLimiter(
+                user, limitsPerAccount, limitsPerRemoteHost, LIMIT_EXCEEDED_MSG));
+    doReturn(true).when(restReqFilter).isRest(req);
+    when(user.get()).thenReturn(currentUser);
+    doNothing().when(chain).doFilter(req, res);
+  }
+
+  private void setUpRegisteredUser() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(true);
+    when(currentUser.asIdentifiedUser().getAccountId()).thenReturn(accountId);
+    when(limitsPerAccount.get(accountId)).thenReturn(holder);
+  }
+
+  private void setUpRegisteredUserExecutionException() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(true);
+    when(currentUser.asIdentifiedUser().getAccountId()).thenReturn(accountId);
+    when(limitsPerAccount.get(accountId)).thenThrow(new ExecutionException(null));
+    when(accountId.toString()).thenReturn("123");
+  }
+
+  private void setUpAnonymous() throws ExecutionException {
+    when(currentUser.isIdentifiedUser()).thenReturn(false);
+    when(req.getRemoteHost()).thenReturn("host");
+    when(limitsPerRemoteHost.get("host")).thenReturn(holder);
+  }
+
+  private void setUpNoQuotaViolation1() {
+    when(holder.hasGracePermits()).thenReturn(false);
+    when(holder.get()).thenReturn(rateLimiter);
+    when(holder.get().tryAcquire()).thenReturn(true);
+  }
+
+  private void setUpNoQuotaViolation2() {
+    when(holder.hasGracePermits()).thenReturn(true);
+  }
+
+  private void setUpQuotaViolation() {
+    when(holder.hasGracePermits()).thenReturn(false);
+    when(holder.get()).thenReturn(rateLimiter);
+    when(holder.get().tryAcquire()).thenReturn(false);
+  }
+
+  @Test
+  public void testDoFilterQuotaViolation()
+      throws IOException, ServletException, ExecutionException {
+    setUpRegisteredUser();
+    setUpQuotaViolation();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+  }
+
+  @Test
+  public void testDoFilterNoQuotaViolation()
+      throws IOException, ServletException, ExecutionException {
+    setUpRegisteredUser();
+    setUpNoQuotaViolation1();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res, times(0)).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+    setUpNoQuotaViolation2();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res, times(0)).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+  }
+
+  @Test
+  public void testDoFilterCacheMiss() throws IOException, ServletException, ExecutionException {
+    setUpRegisteredUserExecutionException();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res, times(0)).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+  }
+
+  @Test
+  public void testDoFilterAnonymQuotaViolation()
+      throws IOException, ServletException, ExecutionException {
+    setUpAnonymous();
+    setUpQuotaViolation();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+  }
+
+  @Test
+  public void testDoFilterAnonymNoQuotaViolation()
+      throws IOException, ServletException, ExecutionException {
+    setUpAnonymous();
+    setUpNoQuotaViolation1();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res, times(0)).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+    setUpNoQuotaViolation2();
+    restReqFilter.doFilter(req, res, chain);
+    verify(res, times(0)).sendError(eq(SC_TOO_MANY_REQUESTS), anyString());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/TestNamespaceMatching.java b/src/test/java/com/googlesource/gerrit/plugins/quota/TestNamespaceMatching.java
index e233252..53253ee 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/quota/TestNamespaceMatching.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/TestNamespaceMatching.java
@@ -18,7 +18,6 @@
 import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.reviewdb.client.Project;
-
 import org.junit.Test;
 
 public class TestNamespaceMatching {
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index dfcbe9c..d5764f7 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,2 +1,4 @@
-load("@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
-     "classpath_collector")
+load(
+    "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+    "classpath_collector",
+)
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index a2e438f..0b25d23 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,6 +1,6 @@
 load(
     "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
-    "gerrit_plugin",
     "PLUGIN_DEPS",
     "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
 )