blob: 9573f99bcbae6aa1d91b4ec8d2876340301ee463 [file] [log] [blame]
// Copyright (C) 2016 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.google.gerrit.server.schema;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.Ints;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.change.AccountPatchReviewStore;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.inject.Singleton;
@Singleton
public class H2AccountPatchReviewStore
implements AccountPatchReviewStore, LifecycleListener {
private static final Logger log =
LoggerFactory.getLogger(H2AccountPatchReviewStore.class);
public static class Module extends LifecycleModule {
@Override
protected void configure() {
DynamicItem.bind(binder(), AccountPatchReviewStore.class)
.to(H2AccountPatchReviewStore.class);
listener().to(H2AccountPatchReviewStore.class);
}
}
@VisibleForTesting
public static class InMemoryModule extends LifecycleModule {
@Override
protected void configure() {
H2AccountPatchReviewStore inMemoryStore = new H2AccountPatchReviewStore();
DynamicItem.bind(binder(), AccountPatchReviewStore.class)
.toInstance(inMemoryStore);
listener().toInstance(inMemoryStore);
}
}
private final String url;
@Inject
H2AccountPatchReviewStore(@GerritServerConfig Config cfg,
SitePaths sitePaths) {
this.url = H2.appendUrlOptions(cfg, getUrl(sitePaths));
}
public static String getUrl(SitePaths sitePaths) {
return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews"));
}
/**
* Creates an in-memory H2 database to store the reviewed flags.
* This should be used for tests only.
*/
@VisibleForTesting
private H2AccountPatchReviewStore() {
// DB_CLOSE_DELAY=-1: By default the content of an in-memory H2 database is
// lost at the moment the last connection is closed. This option keeps the
// content as long as the vm lives.
this.url = "jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1";
}
@Override
public void start() {
try {
createTableIfNotExists(url);
} catch (OrmException e) {
log.error("Failed to create table to store account patch reviews", e);
}
}
public static void createTableIfNotExists(String url) throws OrmException {
try (Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement()) {
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ACCOUNT_PATCH_REVIEWS ("
+ "ACCOUNT_ID INTEGER DEFAULT 0 NOT NULL, "
+ "CHANGE_ID INTEGER DEFAULT 0 NOT NULL, "
+ "PATCH_SET_ID INTEGER DEFAULT 0 NOT NULL, "
+ "FILE_NAME VARCHAR(255) DEFAULT '' NOT NULL, "
+ "CONSTRAINT PRIMARY_KEY_ACCOUNT_PATCH_REVIEWS "
+ "PRIMARY KEY (ACCOUNT_ID, CHANGE_ID, PATCH_SET_ID, FILE_NAME)"
+ ")");
} catch (SQLException e) {
throw convertError("create", e);
}
}
public static void dropTableIfExists(String url) throws OrmException {
try (Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement()) {
stmt.executeUpdate("DROP TABLE IF EXISTS ACCOUNT_PATCH_REVIEWS");
} catch (SQLException e) {
throw convertError("create", e);
}
}
@Override
public void stop() {
}
@Override
public boolean markReviewed(PatchSet.Id psId, Account.Id accountId,
String path) throws OrmException {
try (Connection con = DriverManager.getConnection(url);
PreparedStatement stmt =
con.prepareStatement("INSERT INTO ACCOUNT_PATCH_REVIEWS "
+ "(ACCOUNT_ID, CHANGE_ID, PATCH_SET_ID, FILE_NAME) VALUES "
+ "(?, ?, ?, ?)")) {
stmt.setInt(1, accountId.get());
stmt.setInt(2, psId.getParentKey().get());
stmt.setInt(3, psId.get());
stmt.setString(4, path);
stmt.executeUpdate();
return true;
} catch (SQLException e) {
OrmException ormException = convertError("insert", e);
if (ormException instanceof OrmDuplicateKeyException) {
return false;
}
throw ormException;
}
}
@Override
public void markReviewed(PatchSet.Id psId, Account.Id accountId,
Collection<String> paths) throws OrmException {
if (paths == null || paths.isEmpty()) {
return;
}
try (Connection con = DriverManager.getConnection(url);
PreparedStatement stmt =
con.prepareStatement("INSERT INTO ACCOUNT_PATCH_REVIEWS "
+ "(ACCOUNT_ID, CHANGE_ID, PATCH_SET_ID, FILE_NAME) VALUES "
+ "(?, ?, ?, ?)")) {
for (String path : paths) {
stmt.setInt(1, accountId.get());
stmt.setInt(2, psId.getParentKey().get());
stmt.setInt(3, psId.get());
stmt.setString(4, path);
stmt.addBatch();
}
stmt.executeBatch();
} catch (SQLException e) {
throw convertError("insert", e);
}
}
@Override
public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path)
throws OrmException {
try (Connection con = DriverManager.getConnection(url);
PreparedStatement stmt =
con.prepareStatement("DELETE FROM ACCOUNT_PATCH_REVIEWS "
+ "WHERE ACCOUNT_ID = ? AND CHANGE_ID + ? AND "
+ "PATCH_SET_ID = ? AND FILE_NAME = ?")) {
stmt.setInt(1, accountId.get());
stmt.setInt(2, psId.getParentKey().get());
stmt.setInt(3, psId.get());
stmt.setString(4, path);
stmt.executeUpdate();
} catch (SQLException e) {
throw convertError("delete", e);
}
}
@Override
public void clearReviewed(PatchSet.Id psId) throws OrmException {
try (Connection con = DriverManager.getConnection(url);
PreparedStatement stmt =
con.prepareStatement("DELETE FROM ACCOUNT_PATCH_REVIEWS "
+ "WHERE CHANGE_ID + ? AND PATCH_SET_ID = ?")) {
stmt.setInt(1, psId.getParentKey().get());
stmt.setInt(2, psId.get());
stmt.executeUpdate();
} catch (SQLException e) {
throw convertError("delete", e);
}
}
@Override
public Collection<String> findReviewed(PatchSet.Id psId, Account.Id accountId)
throws OrmException {
try (Connection con = DriverManager.getConnection(url);
PreparedStatement stmt =
con.prepareStatement("SELECT FILE_NAME FROM ACCOUNT_PATCH_REVIEWS "
+ "WHERE ACCOUNT_ID = ? AND CHANGE_ID = ? AND PATCH_SET_ID = ?")) {
stmt.setInt(1, accountId.get());
stmt.setInt(2, psId.getParentKey().get());
stmt.setInt(3, psId.get());
try (ResultSet rs = stmt.executeQuery()) {
List<String> files = new ArrayList<>();
while (rs.next()) {
files.add(rs.getString("FILE_NAME"));
}
return files;
}
} catch (SQLException e) {
throw convertError("select", e);
}
}
public static OrmException convertError(String op, SQLException err) {
switch (getSQLStateInt(err)) {
case 23001: // UNIQUE CONSTRAINT VIOLATION
case 23505: // DUPLICATE_KEY_1
return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
default:
if (err.getCause() == null && err.getNextException() != null) {
err.initCause(err.getNextException());
}
return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
}
}
private static String getSQLState(SQLException err) {
String ec;
SQLException next = err;
do {
ec = next.getSQLState();
next = next.getNextException();
} while (ec == null && next != null);
return ec;
}
private static int getSQLStateInt(SQLException err) {
String s = getSQLState(err);
if (s != null) {
Integer i = Ints.tryParse(s);
return i != null ? i : -1;
}
return 0;
}
}