Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Upgrade bazlets to latest stable-2.14

Change-Id: I8040871bfb9f22fa66f90b585af29ff56a772153
diff --git a/WORKSPACE b/WORKSPACE
index 486abae..6ff88c5 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "ece89b00ac1049233d9f5bb471a8d37b57667d9e",
+    commit = "ae1cd231b0262b2738e6c0593eb3c504209ad4f5",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 5e88fc1..b3f9594 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,8 +3,8 @@
 def external_plugin_deps():
     maven_jar(
         name = "mockito",
-        artifact = "org.mockito:mockito-core:2.27.0",
-        sha1 = "835fc3283b481f4758b8ef464cd560c649c08b00",
+        artifact = "org.mockito:mockito-core:2.28.2",
+        sha1 = "91110215a8cb9b77a46e045ee758f77d79167cc0",
         deps = [
             "@byte-buddy//jar",
             "@byte-buddy-agent//jar",
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java
index d8e2aff..b457018 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java
@@ -18,12 +18,12 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ScheduledExecutorService;
 
 @Singleton
 public class EventCleanerQueue implements LifecycleListener {
   private final WorkQueue workQueue;
-  private WorkQueue.Executor pool;
+  private ScheduledExecutorService pool;
 
   @Inject
   public EventCleanerQueue(WorkQueue workQueue) {
@@ -38,12 +38,11 @@
   @Override
   public void stop() {
     if (pool != null) {
-      pool.unregisterWorkQueue();
       pool = null;
     }
   }
 
-  public ScheduledThreadPoolExecutor getPool() {
-    return pool;
+  ScheduledExecutorService getPool() {
+    return this.pool;
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java
index 262316c..15f830a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java
@@ -18,15 +18,15 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.inject.Inject;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ScheduledExecutorService;
 
 /** Listen to Events and store them into the EventStore */
 class EventHandler implements EventListener {
   private final EventStore store;
-  private final ScheduledThreadPoolExecutor pool;
+  private final ScheduledExecutorService pool;
 
   @Inject
-  EventHandler(EventStore store, @EventPool ScheduledThreadPoolExecutor pool) {
+  EventHandler(EventStore store, @EventPool ScheduledExecutorService pool) {
     this.store = store;
     this.pool = pool;
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java
index 56f8802..b888425 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java
@@ -23,7 +23,7 @@
 import com.google.inject.Provides;
 import com.google.inject.Scopes;
 import com.google.inject.internal.UniqueAnnotations;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ScheduledExecutorService;
 
 /** Configures handling for an event queue while providing its pool. */
 public class EventModule extends AbstractModule {
@@ -42,13 +42,13 @@
 
   @Provides
   @EventPool
-  ScheduledThreadPoolExecutor provideEventPool(EventQueue queue) {
+  ScheduledExecutorService provideEventPool(EventQueue queue) {
     return queue.getPool();
   }
 
   @Provides
   @EventCleanerPool
-  ScheduledThreadPoolExecutor provideEventCleanerPool(EventCleanerQueue queue) {
+  ScheduledExecutorService provideEventCleanerPool(EventCleanerQueue queue) {
     return queue.getPool();
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java
index 32e9465..e4646b0 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java
@@ -19,7 +19,7 @@
 import com.google.inject.BindingAnnotation;
 import java.lang.annotation.Retention;
 
-/** Annotation applied to a ScheduledThreadPoolExecutor. */
+/** Annotation applied to a ScheduledExecutorService. */
 @Retention(RUNTIME)
 @BindingAnnotation
 public @interface EventPool {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java
index eca051b..72c3c5a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java
@@ -17,12 +17,12 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ScheduledExecutorService;
 
 /** A queue for events to store. */
 public class EventQueue implements LifecycleListener {
   private final WorkQueue workQueue;
-  private WorkQueue.Executor pool;
+  private ScheduledExecutorService pool;
 
   @Inject
   EventQueue(WorkQueue workQueue) {
@@ -38,12 +38,11 @@
   @Override
   public void stop() {
     if (pool != null) {
-      pool.unregisterWorkQueue();
       pool = null;
     }
   }
 
-  ScheduledThreadPoolExecutor getPool() {
+  ScheduledExecutorService getPool() {
     return this.pool;
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java
index 3c152e8..b092829 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java
@@ -22,7 +22,7 @@
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
 @Singleton
@@ -32,11 +32,10 @@
 
   private final SQLClient eventsDb;
 
-  private ScheduledThreadPoolExecutor pool;
+  private ScheduledExecutorService pool;
 
   @Inject
-  EventsLogCleaner(
-      @EventsDb SQLClient eventsDb, @EventCleanerPool ScheduledThreadPoolExecutor pool) {
+  EventsLogCleaner(@EventsDb SQLClient eventsDb, @EventCleanerPool ScheduledExecutorService pool) {
     this.eventsDb = eventsDb;
     this.pool = pool;
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java
index 05f663f..9e538e6 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java
@@ -48,7 +48,7 @@
 class SQLClient {
   private static final Logger log = LoggerFactory.getLogger(SQLClient.class);
   private final Gson gson;
-  private final boolean isPostgresql;
+  private final SQLDialect databaseDialect;
 
   private HikariDataSource ds;
 
@@ -56,7 +56,8 @@
     ds = new HikariDataSource(config);
 
     gson = new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
-    isPostgresql = config.getJdbcUrl().contains("postgresql");
+
+    databaseDialect = SQLDialect.fromJdbcUrl(config.getJdbcUrl());
   }
 
   /**
@@ -65,8 +66,8 @@
    * @throws SQLException If there was a problem with the database
    */
   void createDBIfNotCreated() throws SQLException {
-    execute(SQLTable.createTableQuery(isPostgresql));
-    execute(SQLTable.createIndexes(isPostgresql));
+    execute(SQLTable.createTableQuery(databaseDialect));
+    execute(SQLTable.createIndexes(databaseDialect));
   }
 
   /**
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java
new file mode 100644
index 0000000..10df147
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.eventslog.sql;
+
+/** SQLDialect provides enumerations for the various supported dialects of SQL. */
+public enum SQLDialect {
+  H2,
+  MYSQL,
+  POSTGRESQL;
+
+  /**
+   * This attempts to determine the SQL dialect from the JDBC URL. If the URL does not match one of
+   * the supported dialects, then H2 will be returned by default.
+   *
+   * @param jdbcUrl The JDBC URL.
+   * @return The dialect for the JDBC URL.
+   */
+  public static SQLDialect fromJdbcUrl(String jdbcUrl) {
+    if (jdbcUrl.contains("postgresql")) {
+      return POSTGRESQL;
+    } else if (jdbcUrl.contains("mysql")) {
+      return MYSQL;
+    }
+    return H2;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java
index 8e2ab52..8a06904 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java
@@ -24,11 +24,13 @@
 import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.events.ProjectEvent;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,8 +43,8 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map.Entry;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -52,7 +54,6 @@
   private static final Logger log = LoggerFactory.getLogger(SQLStore.class);
   private static final String H2_DB_SUFFIX = ".h2.db";
 
-  private final ProjectControl.GenericFactory projectControlFactory;
   private final Provider<CurrentUser> userProvider;
   private final EventsLogCleaner eventsLogCleaner;
   private SQLClient eventsDb;
@@ -63,30 +64,31 @@
   private final int connectTime;
   private boolean online = true;
   private boolean copyLocal;
-  private final ScheduledThreadPoolExecutor pool;
+  private final ScheduledExecutorService pool;
+  private final PermissionBackend permissionBackend;
   private ScheduledFuture<?> checkConnTask;
   private Path localPath;
 
   @Inject
   SQLStore(
-      ProjectControl.GenericFactory projectControlFactory,
       Provider<CurrentUser> userProvider,
       EventsLogConfig cfg,
       @EventsDb SQLClient eventsDb,
       @LocalEventsDb SQLClient localEventsDb,
-      @EventPool ScheduledThreadPoolExecutor pool,
+      @EventPool ScheduledExecutorService pool,
+      PermissionBackend permissionBackend,
       EventsLogCleaner eventsLogCleaner) {
     this.maxAge = cfg.getMaxAge();
     this.maxTries = cfg.getMaxTries();
     this.waitTime = cfg.getWaitTime();
     this.connectTime = cfg.getConnectTime();
     this.copyLocal = cfg.getCopyLocal();
-    this.projectControlFactory = projectControlFactory;
     this.userProvider = userProvider;
     this.eventsDb = eventsDb;
     this.localEventsDb = localEventsDb;
     this.eventsLogCleaner = eventsLogCleaner;
     this.pool = pool;
+    this.permissionBackend = permissionBackend;
     this.localPath = cfg.getLocalStorePath();
   }
 
@@ -118,18 +120,15 @@
     for (Entry<String, Collection<SQLEntry>> entry : eventsDb.getEvents(query).asMap().entrySet()) {
       String projectName = entry.getKey();
       try {
-        if (projectControlFactory
-            .controlFor(new Project.NameKey(projectName), userProvider.get())
-            .isVisible()) {
-          entries.addAll(entry.getValue());
-        }
-      } catch (NoSuchProjectException e) {
-        log.warn(
-            "Database contains a non-existing project, {}; removing project from database",
-            projectName);
-        eventsLogCleaner.removeProjectEventsAsync(projectName);
-      } catch (IOException e) {
-        log.warn("Cannot get project visibility info for {} from cache", projectName, e);
+        permissionBackend
+            .user(userProvider)
+            .project(new Project.NameKey(projectName))
+            .check(ProjectPermission.ACCESS);
+        entries.addAll(entry.getValue());
+      } catch (AuthException e) {
+        // Ignore
+      } catch (PermissionBackendException e) {
+        log.warn("Cannot check project access permission", e);
       }
     }
     return entries.stream().sorted().map(SQLEntry::getEvent).collect(toList());
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java
index 0db07a2..6093a38 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java
@@ -23,9 +23,28 @@
   static final String DATE_ENTRY = "date_created";
   static final String EVENT_ENTRY = "event_info";
 
+  /** This is the name of the index that tracks the created date. */
   private static final String CREATED_INDEX = "created_idx";
+  /** This is the name of the index that tracks the project. */
   private static final String PROJECT_INDEX = "project_idx";
+  /**
+   * This is the H2 idempotent index-creation query format. Inputs, in order: index-name,
+   * table-name, index-column
+   */
   private static final String H2_INDEX_CREATION_FORMAT = "CREATE INDEX IF NOT EXISTS %s ON %s (%s)";
+  /**
+   * This is the MySQL idempotent index-creation query format. Inputs, in order: table-name,
+   * index-name, table-name, index-name, index-column
+   */
+  private static final String MYSQL_INDEX_CREATION_FORMAT =
+      "SET @x := (SELECT COUNT(*) FROM information_schema.statistics WHERE table_name = '%s' AND index_name = '%s' AND table_schema = DATABASE());\n"
+          + "SET @sql := IF( @x > 0, 'SELECT ''Index exists.''', 'ALTER TABLE %s ADD INDEX %s (%s);');\n"
+          + "PREPARE stmt FROM @sql;\n"
+          + "EXECUTE stmt";
+  /**
+   * This is the Postgres idempotent index-creation query format. Inputs, in order: index-name,
+   * index-name, table-name, index-column
+   */
   private static final String POSTGRESQL_INDEX_CREATION_FORMAT =
       "DO $$\n"
           + "BEGIN\n"
@@ -42,13 +61,17 @@
 
   private SQLTable() {}
 
-  static String createTableQuery(boolean postgresql) {
+  static String createTableQuery(SQLDialect databaseDialect) {
     StringBuilder query = new StringBuilder(140);
     query.append(format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME));
-    if (postgresql) {
-      query.append(format("%s SERIAL PRIMARY KEY,", PRIMARY_ENTRY));
-    } else {
-      query.append(format("%s INT AUTO_INCREMENT PRIMARY KEY,", PRIMARY_ENTRY));
+    switch (databaseDialect) {
+      case POSTGRESQL:
+        query.append(format("%s SERIAL PRIMARY KEY,", PRIMARY_ENTRY));
+        break;
+      case MYSQL:
+      case H2:
+      default:
+        query.append(format("%s INT AUTO_INCREMENT PRIMARY KEY,", PRIMARY_ENTRY));
     }
     query.append(format("%s VARCHAR(255),", PROJECT_ENTRY));
     query.append(format("%s TIMESTAMP DEFAULT NOW(),", DATE_ENTRY));
@@ -56,11 +79,19 @@
     return query.toString();
   }
 
-  static String createIndexes(boolean postgresql) {
-    return postgresql ? getPostgresqlQuery() : getH2Query();
+  static String createIndexes(SQLDialect databaseDialect) {
+    switch (databaseDialect) {
+      case POSTGRESQL:
+        return getPostgresqlIndexQuery();
+      case MYSQL:
+        return getMysqlIndexQuery();
+      case H2:
+      default:
+        return getH2IndexQuery();
+    }
   }
 
-  private static String getPostgresqlQuery() {
+  private static String getPostgresqlIndexQuery() {
     StringBuilder query = new StringBuilder(540);
     query.append(
         format(
@@ -80,7 +111,29 @@
     return query.toString();
   }
 
-  private static String getH2Query() {
+  private static String getMysqlIndexQuery() {
+    StringBuilder query = new StringBuilder();
+    query.append(
+        format(
+            MYSQL_INDEX_CREATION_FORMAT,
+            TABLE_NAME,
+            CREATED_INDEX,
+            TABLE_NAME,
+            CREATED_INDEX,
+            DATE_ENTRY));
+    query.append(";");
+    query.append(
+        format(
+            MYSQL_INDEX_CREATION_FORMAT,
+            TABLE_NAME,
+            PROJECT_INDEX,
+            TABLE_NAME,
+            PROJECT_INDEX,
+            PROJECT_ENTRY));
+    return query.toString();
+  }
+
+  private static String getH2IndexQuery() {
     StringBuilder query = new StringBuilder();
     query.append(format(H2_INDEX_CREATION_FORMAT, CREATED_INDEX, TABLE_NAME, DATE_ENTRY));
     query.append(";");
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 48ee3c6..130cc2a 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -78,7 +78,7 @@
 To execute the tests run:
 
 ```
-  bazel test plugins/@PLUGIN@:events_log_tests
+  bazel test plugins/@PLUGIN@:events-log_tests
 ```
 
 More information about Buck can be found in the [Gerrit
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 4421212..1c74a20 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -26,6 +26,10 @@
 plugin.@PLUGIN@.storeUrl
 :    Specify the path to the directory in which to keep the database. When not
      specified, the default path is jdbc:h2:\<gerrit_site>/data/db.
+     Supported database engines:
+* h2 (default)
+* postgresql
+* mysql
 
 plugin.@PLUGIN@.localStorePath
 :    Specify the path to the directory in which to keep the back up database.
@@ -42,8 +46,12 @@
 plugin.@PLUGIN@.urlOptions
 :    Options to append to the database url. Each option should be specified in a
      separate line using the option=value format. For example:
-       urlOptions = loglevel=INFO
-       urlOptions = logUnclosedConnections=true
+* `urlOptions = loglevel=INFO`
+* `urlOptions = logUnclosedConnections=true`
+
+When using `mysql`, this option must be specified:
+
+* `urlOptions = allowMultiQueries=true`
 
 plugin.@PLUGIN@.maxTries
 :    Maximum number of times the plugin should attempt to store the event if a
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java
index f3eb207..0007ab8 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java
@@ -21,6 +21,7 @@
 
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.Event;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.junit.Before;
 import org.junit.Test;
@@ -35,7 +36,7 @@
 
   @Before
   public void setUp() {
-    ScheduledThreadPoolExecutor poolMock = new PoolMock();
+    ScheduledExecutorService poolMock = new PoolMock();
     eventHandler = new EventHandler(storeMock, poolMock);
   }
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java
new file mode 100644
index 0000000..60b7cb1
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.eventslog.sql;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SQLDialectTest {
+  @Test
+  public void defaultIsH2() throws Exception {
+    assertThat(SQLDialect.fromJdbcUrl("")).isEqualTo(SQLDialect.H2);
+    assertThat(SQLDialect.fromJdbcUrl("jdbc:")).isEqualTo(SQLDialect.H2);
+    assertThat(SQLDialect.fromJdbcUrl("jdbc:whatever://")).isEqualTo(SQLDialect.H2);
+  }
+
+  @Test
+  public void mysqlIsParsed() throws Exception {
+    assertThat(SQLDialect.fromJdbcUrl("jdbc:mysql://")).isEqualTo(SQLDialect.MYSQL);
+  }
+
+  @Test
+  public void postgresqlIsParsed() throws Exception {
+    assertThat(SQLDialect.fromJdbcUrl("jdbc:postgresql://")).isEqualTo(SQLDialect.POSTGRESQL);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java
index bbc4d05..96ce35d 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java
@@ -17,6 +17,7 @@
 import static com.ericsson.gerrit.plugins.eventslog.sql.SQLTable.TABLE_NAME;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -28,15 +29,14 @@
 import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.events.ProjectEvent;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gson.Gson;
 import com.google.inject.Provider;
 import com.zaxxer.hikari.HikariConfig;
-import java.io.IOException;
 import java.net.ConnectException;
 import java.sql.Connection;
 import java.sql.DriverManager;
@@ -45,9 +45,11 @@
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,92 +69,68 @@
   private static final String TERM_CONN_MSG = "terminating connection";
   private static final String MSG = "message";
   private static final String GENERIC_QUERY = "SELECT * FROM " + TABLE_NAME;
-  private static final boolean PROJECT_VISIBLE_TO_USER = true;
-  private static final boolean PROJECT_NOT_VISIBLE_TO_USER = false;
 
-  @Mock private ProjectControl.GenericFactory pcFactoryMock;
   @Mock private Provider<CurrentUser> userProviderMock;
   @Mock private EventsLogConfig cfgMock;
+  @Mock private PermissionBackend permissionBackendMock;
+  @Mock private PermissionBackend.ForProject forProjectMock;
+  @Mock private PermissionBackend.WithUser withUserMock;
   @Mock private EventsLogCleaner logCleanerMock;
 
   private SQLClient eventsDb;
   private SQLClient localEventsDb;
   private SQLStore store;
+  private ScheduledExecutorService poolMock;
   private HikariConfig config;
-  private ScheduledThreadPoolExecutor poolMock;
 
   private Statement stat;
+  private MockEvent mockEvent;
 
   @Rule public TemporaryFolder testFolder = new TemporaryFolder();
 
   @Before
   public void setUp() throws SQLException {
     config = new HikariConfig();
+    config.setJdbcUrl(TEST_URL);
     config.addDataSourceProperty("DB_CLOSE_DELAY", "-1");
     config.addDataSourceProperty("DATABASE_TO_UPPER", "false");
     Connection conn = DriverManager.getConnection(TEST_URL + ";" + TEST_OPTIONS);
+    mockEvent = new MockEvent();
     stat = conn.createStatement();
     poolMock = new PoolMock();
     when(cfgMock.getMaxAge()).thenReturn(5);
     when(cfgMock.getLocalStorePath()).thenReturn(testFolder.getRoot().toPath());
   }
 
-  private void tearDown() throws Exception {
-    stat.execute("DROP TABLE " + TABLE_NAME);
+  @After
+  public void tearDown() throws Exception {
+    stat.execute("DROP TABLE IF EXISTS " + TABLE_NAME);
     store.stop();
   }
 
-  private void setUpClient() {
-    config.setJdbcUrl(TEST_URL);
-    eventsDb = new SQLClient(config);
-    config.setJdbcUrl(TEST_LOCAL_URL);
-    localEventsDb = new SQLClient(config);
-    store =
-        new SQLStore(
-            pcFactoryMock,
-            userProviderMock,
-            cfgMock,
-            eventsDb,
-            localEventsDb,
-            poolMock,
-            logCleanerMock);
-    store.start();
-  }
-
-  private void setUpClientMock() throws SQLException {
-    eventsDb = mock(SQLClient.class);
-    localEventsDb = mock(SQLClient.class);
-    when(localEventsDb.dbExists()).thenReturn(true);
-  }
-
   @Test
   public void storeThenQueryVisible() throws Exception {
-    MockEvent mockEvent = setUpMocks(PROJECT_VISIBLE_TO_USER);
+    when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock);
+    when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock);
+    doNothing().when(forProjectMock).check(ProjectPermission.ACCESS);
+    setUpClient();
     store.storeEvent(mockEvent);
     List<String> events = store.queryChangeEvents(GENERIC_QUERY);
     String json = new Gson().toJson(mockEvent);
     assertThat(events).containsExactly(json).inOrder();
-    tearDown();
   }
 
   @Test
   public void storeThenQueryNotVisible() throws Exception {
-    MockEvent mockEvent = setUpMocks(PROJECT_NOT_VISIBLE_TO_USER);
+    when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock);
+    when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock);
+    doThrow(new PermissionBackendException(""))
+        .when(forProjectMock)
+        .check(ProjectPermission.ACCESS);
+    setUpClient();
     store.storeEvent(mockEvent);
     List<String> events = store.queryChangeEvents(GENERIC_QUERY);
     assertThat(events).isEmpty();
-    tearDown();
-  }
-
-  private MockEvent setUpMocks(boolean isVisible) throws NoSuchProjectException, IOException {
-    MockEvent mockEvent = new MockEvent();
-    ProjectControl pcMock = mock(ProjectControl.class);
-    CurrentUser userMock = mock(CurrentUser.class);
-    when(userProviderMock.get()).thenReturn(userMock);
-    when(pcFactoryMock.controlFor(mockEvent.getProjectNameKey(), userMock)).thenReturn(pcMock);
-    when(pcMock.isVisible()).thenReturn(isVisible);
-    setUpClient();
-    return mockEvent;
   }
 
   @Test(expected = MalformedQueryException.class)
@@ -163,40 +141,20 @@
   }
 
   @Test
-  public void notReturnEventOfNonExistingProject() throws Exception {
-    MockEvent mockEvent = new MockEvent();
-    Project.NameKey projectMock = mock(Project.NameKey.class);
-    CurrentUser userMock = mock(CurrentUser.class);
-    when(userProviderMock.get()).thenReturn(userMock);
-    NameKey projectNameKey = mockEvent.getProjectNameKey();
-    doThrow(new NoSuchProjectException(projectMock))
-        .when(pcFactoryMock)
-        .controlFor(projectNameKey, userMock);
-    setUpClient();
-    store.storeEvent(mockEvent);
-    List<String> events = store.queryChangeEvents(GENERIC_QUERY);
-    assertThat(events).isEmpty();
-    verify(logCleanerMock).removeProjectEventsAsync(mockEvent.project);
-    tearDown();
-  }
-
-  @Test
   public void notReturnEventWithNoVisibilityInfo() throws Exception {
-    MockEvent mockEvent = new MockEvent();
-    CurrentUser userMock = mock(CurrentUser.class);
-    when(userProviderMock.get()).thenReturn(userMock);
-    NameKey projectNameKey = mockEvent.getProjectNameKey();
-    doThrow(new IOException()).when(pcFactoryMock).controlFor(projectNameKey, userMock);
+    when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock);
+    when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock);
+    doThrow(new PermissionBackendException(""))
+        .when(forProjectMock)
+        .check(ProjectPermission.ACCESS);
     setUpClient();
     store.storeEvent(mockEvent);
     List<String> events = store.queryChangeEvents(GENERIC_QUERY);
     assertThat(events).isEmpty();
-    tearDown();
   }
 
   @Test
   public void retryOnConnectException() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     when(cfgMock.getMaxTries()).thenReturn(3);
     Throwable[] exceptions = new Throwable[3];
     Arrays.fill(exceptions, new SQLException(new ConnectException()));
@@ -205,13 +163,14 @@
     doThrow(exceptions).doNothing().when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(eventsDb, times(3)).storeEvent(mockEvent);
@@ -220,7 +179,6 @@
 
   @Test
   public void retryOnMessage() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     when(cfgMock.getMaxTries()).thenReturn(3);
     Throwable[] exceptions = new Throwable[3];
     Arrays.fill(exceptions, new SQLException(TERM_CONN_MSG));
@@ -229,13 +187,14 @@
     doThrow(exceptions).doNothing().when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(eventsDb, times(3)).storeEvent(mockEvent);
@@ -244,19 +203,19 @@
 
   @Test
   public void noRetryOnMessage() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     when(cfgMock.getMaxTries()).thenReturn(3);
     setUpClientMock();
     doThrow(new SQLException(MSG)).when(eventsDb).storeEvent(mockEvent);
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(eventsDb, times(1)).storeEvent(mockEvent);
@@ -264,7 +223,6 @@
 
   @Test
   public void noRetryOnZeroMaxTries() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     when(cfgMock.getMaxTries()).thenReturn(0);
     Throwable[] exceptions = new Throwable[3];
     Arrays.fill(exceptions, new SQLException(new ConnectException()));
@@ -273,13 +231,14 @@
     doThrow(exceptions).doNothing().when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(eventsDb, times(1)).storeEvent(mockEvent);
@@ -287,38 +246,31 @@
 
   @Test(expected = ServiceUnavailableException.class)
   public void throwSQLExceptionIfNotOnline() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     setUpClientMock();
     doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
     doThrow(new SQLException()).when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     store.queryChangeEvents(GENERIC_QUERY);
   }
 
   @Test
-  public void restoreFromLocalAndRemoveUnfoundProjectEvents() throws Exception {
+  public void restoreEventsFromLocalDb() throws Exception {
     MockEvent mockEvent = new MockEvent();
     MockEvent mockEvent2 = new MockEvent("proj");
-    MockEvent mockEvent3 = new MockEvent("unfound");
-
-    ProjectControl pc = mock(ProjectControl.class);
-    NoSuchProjectException e = mock(NoSuchProjectException.class);
-    CurrentUser userMock = mock(CurrentUser.class);
-    when(userProviderMock.get()).thenReturn(userMock);
-    when(pcFactoryMock.controlFor((mockEvent.getProjectNameKey()), userMock)).thenReturn(pc);
-    when(pcFactoryMock.controlFor((mockEvent2.getProjectNameKey()), userMock)).thenReturn(pc);
-    when(pc.isVisible()).thenReturn(true);
-    doThrow(e).when(pcFactoryMock).controlFor((mockEvent3.getProjectNameKey()), userMock);
+    when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock);
+    when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock);
+    doNothing().when(forProjectMock).check(ProjectPermission.ACCESS);
 
     config.setJdbcUrl(TEST_URL);
     eventsDb = new SQLClient(config);
@@ -326,25 +278,24 @@
     localEventsDb = new SQLClient(config);
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
 
     localEventsDb.createDBIfNotCreated();
     localEventsDb.storeEvent(mockEvent);
     localEventsDb.storeEvent(mockEvent2);
-    localEventsDb.storeEvent(mockEvent3);
     store.start();
+
     List<String> events = store.queryChangeEvents(GENERIC_QUERY);
     Gson gson = new Gson();
     String json = gson.toJson(mockEvent);
     String json2 = gson.toJson(mockEvent2);
     assertThat(events).containsExactly(json, json2).inOrder();
-    tearDown();
   }
 
   @Test
@@ -354,32 +305,33 @@
     doThrow(new SQLException()).when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     verify(localEventsDb).createDBIfNotCreated();
   }
 
   @Test
   public void storeLocalOffline() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     setUpClientMock();
     doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
     doThrow(new SQLException()).when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(localEventsDb).storeEvent(mockEvent);
@@ -387,25 +339,46 @@
 
   @Test
   public void storeLocalOfflineAfterNoRetry() throws Exception {
-    MockEvent mockEvent = new MockEvent();
     setUpClientMock();
     when(cfgMock.getMaxTries()).thenReturn(0);
     doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated();
     doThrow(new SQLException()).when(eventsDb).queryOne();
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     store.storeEvent(mockEvent);
     verify(localEventsDb).storeEvent(mockEvent);
   }
 
+  private void setUpClient() {
+    eventsDb = new SQLClient(config);
+    localEventsDb = new SQLClient(config);
+    store =
+        new SQLStore(
+            userProviderMock,
+            cfgMock,
+            eventsDb,
+            localEventsDb,
+            poolMock,
+            permissionBackendMock,
+            logCleanerMock);
+    store.start();
+  }
+
+  private void setUpClientMock() throws SQLException {
+    eventsDb = mock(SQLClient.class);
+    localEventsDb = mock(SQLClient.class);
+    when(localEventsDb.dbExists()).thenReturn(true);
+  }
+
   /**
    * For this test we expect that if we can connect to main database, then we should come back
    * online and try setting up again. We just want to make sure that restoreEventsFromLocal gets
@@ -420,13 +393,14 @@
     when(localEventsDb.getAll()).thenReturn(ImmutableList.of(mock(SQLEntry.class)));
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     poolMock.scheduleWithFixedDelay(store.new CheckConnectionTask(), 0, 0, TimeUnit.MILLISECONDS);
     verify(localEventsDb, times(2)).removeOldEvents(0);
@@ -443,7 +417,6 @@
   }
 
   private void checkConnectionAndRestore(boolean copy) throws Exception {
-    MockEvent mockEvent = new MockEvent();
     eventsDb = mock(SQLClient.class);
     config.setJdbcUrl(TEST_LOCAL_URL);
     localEventsDb = new SQLClient(config);
@@ -460,13 +433,14 @@
 
     store =
         new SQLStore(
-            pcFactoryMock,
             userProviderMock,
             cfgMock,
             eventsDb,
             localEventsDb,
             poolMock,
+            permissionBackendMock,
             logCleanerMock);
+
     store.start();
     verify(eventsDb).queryOne();
     verify(eventsDb).storeEvent(any(String.class), any(Timestamp.class), any(String.class));