initial commit
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..729c54f
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+ ci = //:ci
+ plugin = //:ci
+
+[java]
+ src_roots = java, resources
+
+[project]
+ ignore = .git
+
+[cache]
+ mode = dir
+ dir = buck-out/cache
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa42023
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+/target
+/.classpath
+/.project
+/.settings
+/.buckconfig.local
+/.buckd
+/buck-cache
+/buck-out
+/local.properties
+*.pyc
+.buckversion
+/bucklets
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..a557b76
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,35 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
+gerrit_plugin(
+ name = 'ci',
+ srcs = glob(['src/main/java/**/*.java']),
+ resources = glob(['src/main/**/*']),
+ manifest_entries = [
+ 'Gerrit-PluginName: ci',
+ 'Gerrit-Module: com.googlesource.gerrit.plugins.ci.GlobalModule',
+ 'Gerrit-SshModule: com.googlesource.gerrit.plugins.ci.SshModule',
+ 'Implementation-Title: CI plugin',
+ 'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/ci',
+ ],
+ provided_deps = [
+ '//lib/commons:dbcp',
+ '//lib:gson',
+ ]
+)
+
+java_test(
+ name = 'ci_tests',
+ srcs = glob(['src/test/java/**/*IT.java']),
+ labels = ['ci-plugin'],
+ source_under_test = [':ci__plugin'],
+ deps = GERRIT_PLUGIN_API + GERRIT_TESTS + [
+ ':ci__plugin',
+ ],
+)
+
+java_library(
+ name = 'classpath',
+ deps = GERRIT_PLUGIN_API + GERRIT_TESTS + [
+ ':ci__plugin'
+ ],
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1e33399
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+Gerrit CI reporting and visualization plugin
+============================================
+
+This plugin allow CI system to report result outcome to Gerrit. The result are visualized on change screen. Reporting can be done through SSH commands or REST API.
+
+Database schema doesn't created automatically for now. The scripts are povided separately:
+Prebuilt artifacts
+
+Persistense
+-----------
+
+CI data is stored in seperate database (not review DB used by Gerrit
+itself). The following database dialects are currently supported:
+
+* H2
+* MySQL
+* Oracle
+* PostgreSQL
+
+Database schema doesn't created automatically for now. The script is povided for H2 only for now.
+
+```
+CREATE TABLE PATCH_SET_VERIFICATIONS(
+VALUE SMALLINT DEFAULT 0 NOT NULL,
+GRANTED TIMESTAMP NOT NULL,
+URL VARCHAR(255),
+VERIFIER VARCHAR(255),
+COMMENT VARCHAR(255),
+CHANGE_ID INTEGER DEFAULT 0 NOT NULL,
+PATCH_SET_ID INTEGER DEFAULT 0 NOT NULL,
+CATEGORY_ID VARCHAR(255) DEFAULT '' NOT NULL,
+PRIMARY KEY(CHANGE_ID, PATCH_SET_ID, CATEGORY_ID));
+```
+
+Example for SSH command
+-----------------------
+
+```
+ssh gerritd ci verify --verification "'category=gate-horizon-pep8|value=1|url=https://ci.host.com/jobs/pep8/4711|verifier=Jenkins|comment=Non Voting'" a14825a6e9c75b68c6be486ec2b8b6fed43b8858
+```
+
+Example for REST API
+--------------------
+
+```
+curl -X POST --digest --user jow:secret --data-binary
+@post-verify.txt --header "Content-Type: application/json;
+charset=UTF-8"
+http://localhost:8080/a/changes/1/revisions/4d5fda7e653534b1709883d96264910fab03ddbb/verify
+
+$ cat post-verify.txt
+{
+ "verifications": {
+ "gate-puma-pep8": {
+ "value": -1,
+ "url": "https://ci.host.com/jobs/pep8/1711",
+ "comment": "Failed",
+ "verifier": "Jenkins"
+ }
+ }
+}
+
+```
+
+TODO
+----
+
+* Documentation
+* Schema initialization and upgrade
+* UI integration
+
+License
+-------
+
+Apache License 2.0
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/GlobalModule.java b/src/main/java/com/googlesource/gerrit/plugins/ci/GlobalModule.java
new file mode 100644
index 0000000..4dcb816
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/GlobalModule.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2015 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.ci;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.name.Names;
+
+import com.googlesource.gerrit.plugins.ci.server.schema.CiDataSourceModule;
+import com.googlesource.gerrit.plugins.ci.server.schema.CiDataSourceProvider;
+import com.googlesource.gerrit.plugins.ci.server.schema.CiDataSourceType;
+import com.googlesource.gerrit.plugins.ci.server.schema.CiDataSourceTypeGuesser;
+import com.googlesource.gerrit.plugins.ci.server.schema.CiDatabaseModule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+public class GlobalModule extends AbstractModule {
+
+ private final Injector injector;
+
+ @Inject
+ GlobalModule(Injector injector) {
+ this.injector = injector;
+ }
+
+ @Override
+ protected void configure() {
+ List<Module> modules = new ArrayList<>();
+ modules.add(new LifecycleModule() {
+ @Override
+ protected void configure() {
+ // For bootstrap we need to retrieve the ds type first
+ CiDataSourceTypeGuesser guesser =
+ injector.createChildInjector(
+ new CiDataSourceModule()).getInstance(
+ Key.get(CiDataSourceTypeGuesser.class));
+
+ // For the ds type we retrieve the underlying implementation
+ CiDataSourceType dst = injector.createChildInjector(
+ new CiDataSourceModule()).getInstance(
+ Key.get(CiDataSourceType.class,
+ Names.named(guesser.guessDataSourceType())));
+ // Bind the type to the retrieved instance
+ bind(CiDataSourceType.class).toInstance(dst);
+ bind(CiDataSourceProvider.Context.class).toInstance(
+ CiDataSourceProvider.Context.MULTI_USER);
+ bind(Key.get(DataSource.class, Names.named("CiDb"))).toProvider(
+ CiDataSourceProvider.class).in(SINGLETON);
+ listener().to(CiDataSourceProvider.class);
+ }
+ });
+ modules.add(new CiDatabaseModule());
+ for (Module module : modules) {
+ install(module);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/InitPlugin.java b/src/main/java/com/googlesource/gerrit/plugins/ci/InitPlugin.java
new file mode 100644
index 0000000..8ce45a3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/InitPlugin.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 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.ci;
+
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+public class InitPlugin implements InitStep {
+
+ // TODO(davido): Add site initialization logic
+ @SuppressWarnings("unused")
+ private final CiDb ciDb;
+
+ @Inject
+ InitPlugin(CiDb ciDb) {
+ this.ciDb = ciDb;
+ }
+
+ @Override
+ public void run() throws Exception {
+ }
+
+ @Override
+ public void postRun() throws Exception {
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/ci/SshModule.java
new file mode 100644
index 0000000..2d7d16c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/SshModule.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.ci;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+import com.googlesource.gerrit.plugins.ci.commands.CiAdminQueryShell;
+import com.googlesource.gerrit.plugins.ci.commands.CiQueryShell;
+import com.googlesource.gerrit.plugins.ci.commands.VerifyCommand;
+
+public class SshModule extends PluginCommandModule {
+ @Override
+ protected void configureCommands() {
+ command(CiAdminQueryShell.class);
+ command(VerifyCommand.class);
+ factory(CiQueryShell.Factory.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiAdminQueryShell.java b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiAdminQueryShell.java
new file mode 100644
index 0000000..74636c9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiAdminQueryShell.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2015 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.ci.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+/** Opens a query processor. */
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ACCESS_DATABASE)
+@CommandMetaData(name = "cisql", description = "Administrative interface to CI database")
+public class CiAdminQueryShell extends SshCommand {
+ @Inject
+ private CiQueryShell.Factory factory;
+
+ @Inject
+ private IdentifiedUser currentUser;
+
+ @Option(name = "--format", usage = "Set output format")
+ private CiQueryShell.OutputFormat format = CiQueryShell.OutputFormat.PRETTY;
+
+ @Option(name = "-c", metaVar = "SQL QUERY", usage = "Query to execute")
+ private String query;
+
+ @Override
+ protected void run() throws Failure {
+ try {
+ checkPermission();
+
+ final CiQueryShell shell = factory.create(in, out);
+ shell.setOutputFormat(format);
+ if (query != null) {
+ shell.execute(query);
+ } else {
+ shell.run();
+ }
+ } catch (PermissionDeniedException err) {
+ throw new UnloggedFailure("fatal: " + err.getMessage());
+ }
+ }
+
+ /**
+ * Assert that the current user is permitted to perform raw queries.
+ * <p>
+ * As the @RequireCapability guards at various entry points of internal
+ * commands implicitly add administrators (which we want to avoid), we also
+ * check permissions within QueryShell and grant access only to those who
+ * canPerformRawQuery, regardless of whether they are administrators or not.
+ *
+ * @throws PermissionDeniedException
+ */
+ private void checkPermission() throws PermissionDeniedException {
+ if (!currentUser.getCapabilities().canAccessDatabase()) {
+ throw new PermissionDeniedException(String.format(
+ "%s does not have \"Access Database\" capability.",
+ currentUser.getUserName()));
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiQueryShell.java b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiQueryShell.java
new file mode 100644
index 0000000..1c4dd45
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/CiQueryShell.java
@@ -0,0 +1,771 @@
+// Copyright (C) 2015 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.ci.commands;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.Version;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** Simple interactive SQL query tool. */
+public class CiQueryShell {
+ public interface Factory {
+ CiQueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
+ }
+
+ public static enum OutputFormat {
+ PRETTY, JSON, JSON_SINGLE
+ }
+
+ private final BufferedReader in;
+ private final PrintWriter out;
+ private final SchemaFactory<CiDb> dbFactory;
+ private OutputFormat outputFormat = OutputFormat.PRETTY;
+
+ private CiDb db;
+ private Connection connection;
+ private Statement statement;
+
+ @Inject
+ CiQueryShell(SchemaFactory<CiDb> dbFactory,
+ @Assisted InputStream in,
+ @Assisted OutputStream out) {
+ this.dbFactory = dbFactory;
+ this.in = new BufferedReader(new InputStreamReader(in, UTF_8));
+ this.out = new PrintWriter(new OutputStreamWriter(out, UTF_8));
+ }
+
+ public void setOutputFormat(OutputFormat fmt) {
+ outputFormat = fmt;
+ }
+
+ public void run() {
+ try {
+ db = dbFactory.open();
+ try {
+ connection = ((JdbcSchema) db).getConnection();
+ connection.setAutoCommit(true);
+
+ statement = connection.createStatement();
+ try {
+ showBanner();
+ readEvalPrintLoop();
+ } finally {
+ statement.close();
+ statement = null;
+ }
+ } finally {
+ db.close();
+ db = null;
+ }
+ } catch (OrmException | SQLException err) {
+ out.println("fatal: Cannot open connection: " + err.getMessage());
+ } finally {
+ out.flush();
+ }
+ }
+
+ public void execute(String query) {
+ try {
+ db = dbFactory.open();
+ try {
+ connection = ((JdbcSchema) db).getConnection();
+ connection.setAutoCommit(true);
+
+ statement = connection.createStatement();
+ try {
+ executeStatement(query);
+ } finally {
+ statement.close();
+ statement = null;
+ }
+ } finally {
+ db.close();
+ db = null;
+ }
+ } catch (OrmException | SQLException err) {
+ out.println("fatal: Cannot open connection: " + err.getMessage());
+ } finally {
+ out.flush();
+ }
+ }
+
+ private void readEvalPrintLoop() {
+ final StringBuilder buffer = new StringBuilder();
+ boolean executed = false;
+ for (;;) {
+ if (outputFormat == OutputFormat.PRETTY) {
+ print(buffer.length() == 0 || executed ? "gerrit> " : " -> ");
+ }
+ String line = readLine();
+ if (line == null) {
+ return;
+ }
+
+ if (line.startsWith("\\")) {
+ // Shell command, check the various cases we recognize
+ //
+ line = line.substring(1);
+ if (line.equals("h") || line.equals("?")) {
+ showHelp();
+
+ } else if (line.equals("q")) {
+ if (outputFormat == OutputFormat.PRETTY) {
+ println("Bye");
+ }
+ return;
+
+ } else if (line.equals("r")) {
+ buffer.setLength(0);
+ executed = false;
+
+ } else if (line.equals("p")) {
+ println(buffer.toString());
+
+ } else if (line.equals("g")) {
+ if (buffer.length() > 0) {
+ executeStatement(buffer.toString());
+ executed = true;
+ }
+
+ } else if (line.equals("d")) {
+ listTables();
+
+ } else if (line.startsWith("d ")) {
+ showTable(line.substring(2).trim());
+
+ } else {
+ final String msg = "'\\" + line + "' not supported";
+ switch (outputFormat) {
+ case JSON_SINGLE:
+ case JSON: {
+ final JsonObject err = new JsonObject();
+ err.addProperty("type", "error");
+ err.addProperty("message", msg);
+ println(err.toString());
+ break;
+ }
+ case PRETTY:
+ default:
+ println("ERROR: " + msg);
+ println("");
+ showHelp();
+ break;
+ }
+ }
+ continue;
+ }
+
+ if (executed) {
+ buffer.setLength(0);
+ executed = false;
+ }
+ if (buffer.length() > 0) {
+ buffer.append('\n');
+ }
+ buffer.append(line);
+
+ if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == ';') {
+ executeStatement(buffer.toString());
+ executed = true;
+ }
+ }
+ }
+
+ private void listTables() {
+ final DatabaseMetaData meta;
+ try {
+ meta = connection.getMetaData();
+ } catch (SQLException e) {
+ error(e);
+ return;
+ }
+
+ final String[] types = {"TABLE", "VIEW"};
+ try (ResultSet rs = meta.getTables(null, null, null, types)) {
+ if (outputFormat == OutputFormat.PRETTY) {
+ println(" List of relations");
+ }
+ showResultSet(rs, false, 0,
+ Identity.create(rs, "TABLE_SCHEM"),
+ Identity.create(rs, "TABLE_NAME"),
+ Identity.create(rs, "TABLE_TYPE"));
+ } catch (SQLException e) {
+ error(e);
+ }
+
+ println("");
+ }
+
+ private void showTable(String tableName) {
+ final DatabaseMetaData meta;
+ try {
+ meta = connection.getMetaData();
+
+ if (meta.storesUpperCaseIdentifiers()) {
+ tableName = tableName.toUpperCase();
+ } else if (meta.storesLowerCaseIdentifiers()) {
+ tableName = tableName.toLowerCase();
+ }
+ } catch (SQLException e) {
+ error(e);
+ return;
+ }
+
+ try (ResultSet rs = meta.getColumns(null, null, tableName, null)) {
+ if (!rs.next()) {
+ throw new SQLException("Table " + tableName + " not found");
+ }
+
+ if (outputFormat == OutputFormat.PRETTY) {
+ println(" Table " + tableName);
+ }
+ showResultSet(rs, true, 0,
+ Identity.create(rs, "COLUMN_NAME"),
+ new Function("TYPE") {
+ @Override
+ String apply(final ResultSet rs) throws SQLException {
+ String type = rs.getString("TYPE_NAME");
+ switch (rs.getInt("DATA_TYPE")) {
+ case java.sql.Types.CHAR:
+ case java.sql.Types.VARCHAR:
+ type += "(" + rs.getInt("COLUMN_SIZE") + ")";
+ break;
+ }
+
+ String def = rs.getString("COLUMN_DEF");
+ if (def != null && !def.isEmpty()) {
+ type += " DEFAULT " + def;
+ }
+
+ int nullable = rs.getInt("NULLABLE");
+ if (nullable == DatabaseMetaData.columnNoNulls) {
+ type += " NOT NULL";
+ }
+ return type;
+ }
+ });
+ } catch (SQLException e) {
+ error(e);
+ return;
+ }
+
+ try (ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true)) {
+ Map<String, IndexInfo> indexes = new TreeMap<>();
+ while (rs.next()) {
+ final String indexName = rs.getString("INDEX_NAME");
+ IndexInfo def = indexes.get(indexName);
+ if (def == null) {
+ def = new IndexInfo();
+ def.name = indexName;
+ indexes.put(indexName, def);
+ }
+
+ if (!rs.getBoolean("NON_UNIQUE")) {
+ def.unique = true;
+ }
+
+ final int pos = rs.getInt("ORDINAL_POSITION");
+ final String col = rs.getString("COLUMN_NAME");
+ String desc = rs.getString("ASC_OR_DESC");
+ if ("D".equals(desc)) {
+ desc = " DESC";
+ } else {
+ desc = "";
+ }
+ def.addColumn(pos, col + desc);
+
+ String filter = rs.getString("FILTER_CONDITION");
+ if (filter != null && !filter.isEmpty()) {
+ def.filter.append(filter);
+ }
+ }
+
+ if (outputFormat == OutputFormat.PRETTY) {
+ println("");
+ println("Indexes on " + tableName + ":");
+ for (IndexInfo def : indexes.values()) {
+ println(" " + def);
+ }
+ }
+ } catch (SQLException e) {
+ error(e);
+ return;
+ }
+
+ println("");
+ }
+
+ private void executeStatement(final String sql) {
+ final long start = TimeUtil.nowMs();
+ final boolean hasResultSet;
+ try {
+ hasResultSet = statement.execute(sql);
+ } catch (SQLException e) {
+ error(e);
+ return;
+ }
+
+ try {
+ if (hasResultSet) {
+ try (ResultSet rs = statement.getResultSet()) {
+ showResultSet(rs, false, start);
+ }
+
+ } else {
+ final int updateCount = statement.getUpdateCount();
+ final long ms = TimeUtil.nowMs() - start;
+ switch (outputFormat) {
+ case JSON_SINGLE:
+ case JSON: {
+ final JsonObject tail = new JsonObject();
+ tail.addProperty("type", "update-stats");
+ tail.addProperty("rowCount", updateCount);
+ tail.addProperty("runTimeMilliseconds", ms);
+ println(tail.toString());
+ break;
+ }
+
+ case PRETTY:
+ default:
+ println("UPDATE " + updateCount + "; " + ms + " ms");
+ break;
+ }
+ }
+ } catch (SQLException e) {
+ error(e);
+ }
+ }
+
+ /**
+ * Outputs a result set to stdout.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSet(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
+ switch (outputFormat) {
+ case JSON_SINGLE:
+ case JSON:
+ showResultSetJson(rs, alreadyOnRow, start, show);
+ break;
+ case PRETTY:
+ default:
+ showResultSetPretty(rs, alreadyOnRow, start, show);
+ break;
+ }
+ }
+
+ /**
+ * Outputs a result set to stdout in Json format.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
+ JsonArray collector = new JsonArray();
+ final ResultSetMetaData meta = rs.getMetaData();
+ final Function[] columnMap;
+ if (show != null && 0 < show.length) {
+ columnMap = show;
+
+ } else {
+ final int colCnt = meta.getColumnCount();
+ columnMap = new Function[colCnt];
+ for (int colId = 0; colId < colCnt; colId++) {
+ final int p = colId + 1;
+ final String name = meta.getColumnLabel(p);
+ columnMap[colId] = new Identity(p, name);
+ }
+ }
+
+ int rowCnt = 0;
+ while (alreadyOnRow || rs.next()) {
+ final JsonObject row = new JsonObject();
+ final JsonObject cols = new JsonObject();
+ for (Function function : columnMap) {
+ String v = function.apply(rs);
+ if (v == null) {
+ continue;
+ }
+ cols.addProperty(function.name.toLowerCase(), v);
+ }
+ row.addProperty("type", "row");
+ row.add("columns", cols);
+ switch (outputFormat) {
+ case JSON:
+ println(row.toString());
+ break;
+ case JSON_SINGLE:
+ collector.add(row);
+ break;
+ default:
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "error");
+ obj.addProperty("message", "Unsupported Json variant");
+ println(obj.toString());
+ return;
+ }
+ alreadyOnRow = false;
+ rowCnt++;
+ }
+
+ JsonObject tail = null;
+ if (start != 0) {
+ tail = new JsonObject();
+ tail.addProperty("type", "query-stats");
+ tail.addProperty("rowCount", rowCnt);
+ final long ms = TimeUtil.nowMs() - start;
+ tail.addProperty("runTimeMilliseconds", ms);
+ }
+
+ switch (outputFormat) {
+ case JSON:
+ if (tail != null) {
+ println(tail.toString());
+ }
+ break;
+ case JSON_SINGLE:
+ if (tail != null) {
+ collector.add(tail);
+ }
+ println(collector.toString());
+ break;
+ default:
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "error");
+ obj.addProperty("message", "Unsupported Json variant");
+ println(obj.toString());
+ }
+ }
+
+ /**
+ * Outputs a result set to stdout in plain text format.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
+ final ResultSetMetaData meta = rs.getMetaData();
+
+ final Function[] columnMap;
+ if (show != null && 0 < show.length) {
+ columnMap = show;
+
+ } else {
+ final int colCnt = meta.getColumnCount();
+ columnMap = new Function[colCnt];
+ for (int colId = 0; colId < colCnt; colId++) {
+ final int p = colId + 1;
+ final String name = meta.getColumnLabel(p);
+ columnMap[colId] = new Identity(p, name);
+ }
+ }
+
+ final int colCnt = columnMap.length;
+ final int[] widths = new int[colCnt];
+ for (int c = 0; c < colCnt; c++) {
+ widths[c] = columnMap[c].name.length();
+ }
+
+ final List<String[]> rows = new ArrayList<>();
+ while (alreadyOnRow || rs.next()) {
+ final String[] row = new String[columnMap.length];
+ for (int c = 0; c < colCnt; c++) {
+ row[c] = columnMap[c].apply(rs);
+ if (row[c] == null) {
+ row[c] = "NULL";
+ }
+ widths[c] = Math.max(widths[c], row[c].length());
+ }
+ rows.add(row);
+ alreadyOnRow = false;
+ }
+
+ final StringBuilder b = new StringBuilder();
+ for (int c = 0; c < colCnt; c++) {
+ if (0 < c) {
+ b.append(" | ");
+ }
+
+ String n = columnMap[c].name;
+ if (widths[c] < n.length()) {
+ n = n.substring(0, widths[c]);
+ }
+ b.append(n);
+
+ if (c < colCnt - 1) {
+ for (int pad = n.length(); pad < widths[c]; pad++) {
+ b.append(' ');
+ }
+ }
+ }
+ println(" " + b.toString());
+
+ b.setLength(0);
+ for (int c = 0; c < colCnt; c++) {
+ if (0 < c) {
+ b.append("-+-");
+ }
+ for (int pad = 0; pad < widths[c]; pad++) {
+ b.append('-');
+ }
+ }
+ println(" " + b.toString());
+
+ boolean dataTruncated = false;
+ for (String[] row : rows) {
+ b.setLength(0);
+ b.append(' ');
+
+ for (int c = 0; c < colCnt; c++) {
+ final int max = widths[c];
+ if (0 < c) {
+ b.append(" | ");
+ }
+
+ String s = row[c];
+ if (1 < colCnt && max < s.length()) {
+ s = s.substring(0, max);
+ dataTruncated = true;
+ }
+ b.append(s);
+
+ if (c < colCnt - 1) {
+ for (int pad = s.length(); pad < max; pad++) {
+ b.append(' ');
+ }
+ }
+ }
+ println(b.toString());
+ }
+
+ if (dataTruncated) {
+ warning("some column data was truncated");
+ }
+
+ if (start != 0) {
+ final int rowCount = rows.size();
+ final long ms = TimeUtil.nowMs() - start;
+ println("(" + rowCount + (rowCount == 1 ? " row" : " rows")
+ + "; " + ms + " ms)");
+ }
+ }
+
+ private void warning(final String msg) {
+ switch (outputFormat) {
+ case JSON_SINGLE:
+ case JSON: {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "warning");
+ obj.addProperty("message", msg);
+ println(obj.toString());
+ break;
+ }
+
+ case PRETTY:
+ default:
+ println("WARNING: " + msg);
+ break;
+ }
+ }
+
+ private void error(final SQLException err) {
+ switch (outputFormat) {
+ case JSON_SINGLE:
+ case JSON: {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "error");
+ obj.addProperty("message", err.getMessage());
+ println(obj.toString());
+ break;
+ }
+
+ case PRETTY:
+ default:
+ println("ERROR: " + err.getMessage());
+ break;
+ }
+ }
+
+ private void print(String s) {
+ out.print(s);
+ out.flush();
+ }
+
+ private void println(String s) {
+ out.print(s);
+ out.print('\n');
+ out.flush();
+ }
+
+ private String readLine() {
+ try {
+ return in.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private void showBanner() {
+ if (outputFormat == OutputFormat.PRETTY) {
+ println("Welcome to Gerrit Code Review " + Version.getVersion());
+ try {
+ print("(");
+ print(connection.getMetaData().getDatabaseProductName());
+ print(" ");
+ print(connection.getMetaData().getDatabaseProductVersion());
+ println(")");
+ } catch (SQLException err) {
+ error(err);
+ }
+ println("");
+ println("Type '\\h' for help. Type '\\r' to clear the buffer.");
+ println("");
+ }
+ }
+
+ private void showHelp() {
+ final StringBuilder help = new StringBuilder();
+ help.append("General\n");
+ help.append(" \\q quit\n");
+
+ help.append("\n");
+ help.append("Query Buffer\n");
+ help.append(" \\g execute the query buffer\n");
+ help.append(" \\p display the current buffer\n");
+ help.append(" \\r clear the query buffer\n");
+
+ help.append("\n");
+ help.append("Informational\n");
+ help.append(" \\d list all tables\n");
+ help.append(" \\d NAME describe table\n");
+
+ help.append("\n");
+ print(help.toString());
+ }
+
+ private abstract static class Function {
+ final String name;
+
+ Function(final String name) {
+ this.name = name;
+ }
+
+ abstract String apply(ResultSet rs) throws SQLException;
+ }
+
+ private static class Identity extends Function {
+ static Identity create(final ResultSet rs, final String name)
+ throws SQLException {
+ return new Identity(rs.findColumn(name), name);
+ }
+
+ final int colId;
+
+ Identity(final int colId, final String name) {
+ super(name);
+ this.colId = colId;
+ }
+
+ @Override
+ String apply(final ResultSet rs) throws SQLException {
+ return rs.getString(colId);
+ }
+ }
+
+ private static class IndexInfo {
+ String name;
+ boolean unique;
+ final Map<Integer, String> columns = new TreeMap<>();
+ final StringBuilder filter = new StringBuilder();
+
+ void addColumn(int pos, String column) {
+ columns.put(Integer.valueOf(pos), column);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder r = new StringBuilder();
+ r.append(name);
+ if (unique) {
+ r.append(" UNIQUE");
+ }
+ r.append(" (");
+ boolean first = true;
+ for (String c : columns.values()) {
+ if (!first) {
+ r.append(", ");
+ }
+ r.append(c);
+ first = false;
+ }
+ r.append(")");
+ if (filter.length() > 0) {
+ r.append(" WHERE ");
+ r.append(filter);
+ }
+ return r.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/commands/VerifyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/VerifyCommand.java
new file mode 100644
index 0000000..9b680ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/commands/VerifyCommand.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2015 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.ci.commands;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gerrit.sshd.commands.CommandUtils;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.ci.common.VerificationInfo;
+import com.googlesource.gerrit.plugins.ci.common.VerifyInput;
+import com.googlesource.gerrit.plugins.ci.server.PostVerification;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@CommandMetaData(name = "verify", description = "Verify one or more patch sets")
+public class VerifyCommand extends SshCommand {
+ private static final Logger log =
+ LoggerFactory.getLogger(VerifyCommand.class);
+
+ private final Set<PatchSet> patchSets = new HashSet<>();
+
+ @Argument(index = 0, required = true, multiValued = true,
+ metaVar = "{COMMIT | CHANGE,PATCHSET}",
+ usage = "list of commits or patch sets to verify")
+ void addPatchSetId(String token) {
+ try {
+ PatchSet ps = CommandUtils.parsePatchSet(token, db, projectControl,
+ branch);
+ patchSets.add(ps);
+ } catch (UnloggedFailure e) {
+ throw new IllegalArgumentException(e.getMessage(), e);
+ } catch (OrmException e) {
+ throw new IllegalArgumentException("database error", e);
+ }
+ }
+
+ @Option(name = "--project", aliases = "-p",
+ usage = "project containing the specified patch set(s)")
+ private ProjectControl projectControl;
+
+ @Option(name = "--branch", aliases = "-b",
+ usage = "branch containing the specified patch set(s)")
+ private String branch;
+
+ @Option(name = "--verification", aliases = "-v",
+ usage = "verification to set the result for", metaVar = "VERIFY=OUTCOME")
+ void addJob(String token) {
+ parseWithEquals(token);
+ }
+
+ private void parseWithEquals(String text) {
+ log.debug("processing verification: " + text);
+ checkArgument(!Strings.isNullOrEmpty(text), "Empty verification vote");
+ Map<String, String> params = null;
+ try {
+ params = Splitter.on("|").withKeyValueSeparator("=").split(text);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(String.valueOf("Invalid verification parameters"));
+ }
+
+ String category = params.get("category");
+ checkArgument(category != null, "Verification is missing a category");
+ String value = params.get("value");
+ checkArgument(value != null, "Verification is missing a value");
+ VerificationInfo data = new VerificationInfo();
+ data.value = Short.parseShort(value);
+ data.url = params.get("url");
+ data.verifier = params.get("verifier");
+ data.comment = params.get("comment");
+ jobResult.put(category, data);
+ }
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ private IdentifiedUser currentUser;
+
+ @Inject
+ private PostVerification postVerification;
+
+ @Inject
+ private ChangeControl.GenericFactory genericFactory;
+
+ private Map<String, VerificationInfo> jobResult = Maps.newHashMap();
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ boolean ok = true;
+ for (PatchSet patchSet : patchSets) {
+ try {
+ verifyOne(patchSet);
+ } catch (UnloggedFailure e) {
+ ok = false;
+ writeError("error: " + e.getMessage() + "\n");
+ }
+ }
+
+ if (!ok) {
+ throw new UnloggedFailure(1, "one or more verifications failed;"
+ + " review output above");
+ }
+ }
+
+ private void applyVerification(PatchSet patchSet, VerifyInput verify)
+ throws RestApiException, NoSuchChangeException, OrmException,
+ IOException {
+ ChangeControl ctl =
+ genericFactory.controlFor(patchSet.getId().getParentKey(),
+ currentUser);
+ ChangeResource changeResource = new ChangeResource(ctl);
+ RevisionResource revResource = new RevisionResource(changeResource,
+ patchSet);
+ postVerification.apply(revResource, verify);
+ }
+
+ private void verifyOne(PatchSet patchSet) throws UnloggedFailure {
+ VerifyInput verify = new VerifyInput();
+ verify.verifications = jobResult;
+ try {
+ applyVerification(patchSet, verify);
+ } catch (RestApiException | NoSuchChangeException | OrmException
+ | IOException e) {
+ throw CommandUtils.error(e.getMessage());
+ }
+ }
+
+ private void writeError(String msg) {
+ try {
+ err.write(msg.getBytes(ENC));
+ } catch (IOException e) {
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerificationInfo.java b/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerificationInfo.java
new file mode 100644
index 0000000..58a05c7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerificationInfo.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2015 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.ci.common;
+
+public class VerificationInfo {
+ public String url;
+ public Short value;
+ public String verifier;
+ public String comment;
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerifyInput.java b/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerifyInput.java
new file mode 100644
index 0000000..710d1d4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/common/VerifyInput.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2015 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.ci.common;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+import java.util.Map;
+
+public class VerifyInput {
+ @DefaultInput
+ public Map<String, VerificationInfo> verifications;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/CiDb.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/CiDb.java
new file mode 100644
index 0000000..a2dfac9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/CiDb.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2015 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.ci.server;
+
+import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
+import com.google.gwtorm.server.Relation;
+import com.google.gwtorm.server.Schema;
+
+public interface CiDb extends Schema {
+
+ @Relation(id = 1)
+ SchemaVersionAccess schemaVersion();
+
+ @Relation(id = 2)
+ PatchSetVerificationAccess patchSetVerifications();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/GetVerifications.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/GetVerifications.java
new file mode 100644
index 0000000..efff050
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/GetVerifications.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 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.ci.server;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.ci.common.VerificationInfo;
+
+import java.io.IOException;
+import java.util.Map;
+
+@Singleton
+public class GetVerifications implements RestReadView<RevisionResource> {
+ private final SchemaFactory<CiDb> schemaFactory;
+
+ @Inject
+ GetVerifications(SchemaFactory<CiDb> schemaFactory) {
+ this.schemaFactory = schemaFactory;
+ }
+
+ @Override
+ public Map<String, VerificationInfo> apply(RevisionResource rsrc)
+ throws IOException, OrmException {
+ Map<String, VerificationInfo> out = Maps.newHashMap();
+ try (CiDb db = schemaFactory.open()) {
+ for (PatchSetVerification v : db.patchSetVerifications()
+ .byPatchSet(rsrc.getPatchSet().getId())) {
+ VerificationInfo info = new VerificationInfo();
+ info.value = v.getValue();
+ info.url = v.getUrl();
+ info.verifier = v.getVerifier();
+ info.comment = v.getComment();
+ out.put(v.getLabelId().get(), info);
+ }
+ }
+ return out;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerification.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerification.java
new file mode 100644
index 0000000..1c9ab59
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerification.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2015 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.ci.server;
+
+import java.sql.Timestamp;
+import java.util.Objects;
+
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+public class PatchSetVerification {
+
+ public static class Key extends CompoundKey<PatchSet.Id> {
+ private static final long serialVersionUID = 1L;
+
+ @Column(id = 1, name = Column.NONE)
+ protected PatchSet.Id patchSetId;
+
+ @Column(id = 2)
+ protected LabelId categoryId;
+
+ protected Key() {
+ patchSetId = new PatchSet.Id();
+ categoryId = new LabelId();
+ }
+
+ public Key(PatchSet.Id ps, LabelId c) {
+ this.patchSetId = ps;
+ this.categoryId = c;
+ }
+
+ @Override
+ public PatchSet.Id getParentKey() {
+ return patchSetId;
+ }
+
+ public LabelId getLabelId() {
+ return categoryId;
+ }
+
+ @Override
+ public com.google.gwtorm.client.Key<?>[] members() {
+ return new com.google.gwtorm.client.Key<?>[] {categoryId};
+ }
+ }
+
+ @Column(id = 1, name = Column.NONE)
+ protected Key key;
+
+ @Column(id = 2)
+ protected short value;
+
+ @Column(id = 3)
+ protected Timestamp granted;
+
+ @Column(id = 4, notNull = false, length = 255)
+ protected String url;
+
+ @Column(id = 5, notNull = false, length = 255)
+ protected String verifier;
+
+ @Column(id = 6, notNull = false, length = 255)
+ protected String comment;
+
+ protected PatchSetVerification() {
+ }
+
+ public PatchSetVerification(PatchSetVerification.Key k, short v,
+ Timestamp ts) {
+ key = k;
+ setValue(v);
+ setGranted(ts);
+ }
+
+ public PatchSetVerification.Key getKey() {
+ return key;
+ }
+
+ public PatchSet.Id getPatchSetId() {
+ return key.patchSetId;
+ }
+
+ public LabelId getLabelId() {
+ return key.categoryId;
+ }
+
+ public short getValue() {
+ return value;
+ }
+
+ public void setValue(short v) {
+ value = v;
+ }
+
+ public Timestamp getGranted() {
+ return granted;
+ }
+
+ public void setGranted(Timestamp ts) {
+ granted = ts;
+ }
+
+ public String getLabel() {
+ return getLabelId().get();
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getVerifier() {
+ return verifier;
+ }
+
+ public void setVerifier(String reporter) {
+ this.verifier = reporter;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder().append('[').append(key).append(": ")
+ .append(value).append(']').toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof PatchSetVerification) {
+ PatchSetVerification p = (PatchSetVerification) o;
+ return Objects.equals(key, p.key)
+ && Objects.equals(value, p.value)
+ && Objects.equals(granted, p.granted);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value, granted);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerificationAccess.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerificationAccess.java
new file mode 100644
index 0000000..5d14103
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PatchSetVerificationAccess.java
@@ -0,0 +1,36 @@
+//Copyright (C) 2015 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.ci.server;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface PatchSetVerificationAccess extends
+ Access<PatchSetVerification, PatchSetVerification.Key> {
+ @Override
+ @PrimaryKey("key")
+ PatchSetVerification get(PatchSetVerification.Key key) throws OrmException;
+
+ @Query("WHERE key.patchSetId.changeId = ?")
+ ResultSet<PatchSetVerification> byChange(Change.Id id) throws OrmException;
+
+ @Query("WHERE key.patchSetId = ?")
+ ResultSet<PatchSetVerification> byPatchSet(PatchSet.Id id) throws OrmException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/PostVerification.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PostVerification.java
new file mode 100644
index 0000000..e7eaee0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/PostVerification.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2015 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.ci.server;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.ci.common.VerificationInfo;
+import com.googlesource.gerrit.plugins.ci.common.VerifyInput;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class PostVerification
+ implements RestModifyView<RevisionResource, VerifyInput> {
+ private static final Logger log =
+ LoggerFactory.getLogger(PostVerification.class);
+ private final SchemaFactory<CiDb> schemaFactory;
+
+ @Inject
+ PostVerification(SchemaFactory<CiDb> schemaFactory) {
+ this.schemaFactory = schemaFactory;
+ }
+
+ @Override
+ public Response<?> apply(RevisionResource revision, VerifyInput input)
+ throws AuthException, BadRequestException, UnprocessableEntityException,
+ OrmException, IOException {
+ if (input.verifications == null) {
+ throw new BadRequestException("Missing verifications field");
+ }
+
+ try (CiDb db = schemaFactory.open()) {
+ // Only needed for Google Megastore (that we don't use yet)
+ db.patchSetVerifications().beginTransaction(null);
+ boolean dirty = false;
+ try {
+ dirty |= updateLabels(revision, db, input.verifications);
+ if (dirty) {
+ db.commit();
+ }
+ } finally {
+ db.rollback();
+ }
+ }
+ return Response.none();
+ }
+
+ private boolean updateLabels(RevisionResource resource, CiDb db,
+ Map<String, VerificationInfo> jobs)
+ throws OrmException, BadRequestException {
+ Preconditions.checkNotNull(jobs);
+
+ List<PatchSetVerification> ups = Lists.newArrayList();
+ Map<String, PatchSetVerification> current = scanLabels(resource, db);
+
+ Timestamp ts = TimeUtil.nowTs();
+ for (Map.Entry<String, VerificationInfo> ent : jobs.entrySet()) {
+ String name = ent.getKey();
+ PatchSetVerification c = current.remove(name);
+ Short value = ent.getValue().value;
+ if (value == null) {
+ throw new BadRequestException("Missing value field");
+ }
+ if (c != null) {
+ c.setGranted(ts);
+ c.setValue(value);
+ String url = ent.getValue().url;
+ if (url != null) {
+ c.setUrl(url);
+ }
+ String verifier = ent.getValue().verifier;
+ if (verifier != null) {
+ c.setVerifier(verifier);
+ }
+ String comment = ent.getValue().comment;
+ if (comment != null) {
+ c.setComment(comment);
+ }
+ log.info("Updating job " + c.getLabel() + " for change "
+ + c.getPatchSetId());
+ ups.add(c);
+ } else {
+ c = new PatchSetVerification(new PatchSetVerification.Key(
+ resource.getPatchSet().getId(),
+ new LabelId(name)),
+ value, TimeUtil.nowTs());
+ c.setGranted(ts);
+ c.setUrl(ent.getValue().url);
+ c.setVerifier(ent.getValue().verifier);
+ c.setComment(ent.getValue().comment);
+ log.info("Adding job " + c.getLabel() + " for change "
+ + c.getPatchSetId());
+ ups.add(c);
+ }
+ }
+
+ db.patchSetVerifications().upsert(ups);
+ return !ups.isEmpty();
+ }
+
+ private Map<String, PatchSetVerification> scanLabels(
+ RevisionResource resource, CiDb db)
+ throws OrmException {
+ Map<String, PatchSetVerification> current = Maps.newHashMap();
+ for (PatchSetVerification v : db.patchSetVerifications()
+ .byPatchSet(resource.getPatchSet().getId())) {
+ current.put(v.getLabelId().get(), v);
+ }
+ return current;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiBaseDataSourceType.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiBaseDataSourceType.java
new file mode 100644
index 0000000..c839a1b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiBaseDataSourceType.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+public abstract class CiBaseDataSourceType implements CiDataSourceType {
+
+ private final String driver;
+
+ protected CiBaseDataSourceType(String driver) {
+ this.driver = driver;
+ }
+
+ @Override
+ public final String getDriver() {
+ return driver;
+ }
+
+ @Override
+ public boolean usePool() {
+ return true;
+ }
+
+ @Override
+ public ScriptRunner getIndexScript() throws IOException {
+ return getScriptRunner("index_generic.sql");
+ }
+
+ protected static final ScriptRunner getScriptRunner(String path) throws IOException {
+ if (path == null) {
+ return ScriptRunner.NOOP;
+ }
+ ScriptRunner runner;
+ try (InputStream in = CiDb.class.getResourceAsStream(path)) {
+ if (in == null) {
+ throw new IllegalStateException("SQL script " + path + " not found");
+ }
+ runner = new ScriptRunner(path, in);
+ }
+ return runner;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceModule.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceModule.java
new file mode 100644
index 0000000..58933fe
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceModule.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class CiDataSourceModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(CiDataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
+ bind(CiDataSourceType.class).annotatedWith(Names.named("derby")).to(Derby.class);
+ bind(CiDataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
+ bind(CiDataSourceType.class).annotatedWith(Names.named("oracle")).to(Oracle.class);
+ bind(CiDataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceProvider.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceProvider.java
new file mode 100644
index 0000000..1a24291
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceProvider.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbcp.BasicDataSource;
+import org.eclipse.jgit.lib.Config;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.persistence.DataSourceInterceptor;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+/** Provides access to the DataSource. */
+@Singleton
+public class CiDataSourceProvider implements Provider<DataSource>,
+ LifecycleListener {
+ public static final int DEFAULT_POOL_LIMIT = 8;
+
+ private final Config cfg;
+ private final MetricMaker metrics;
+ private final Context ctx;
+ private final CiDataSourceType dst;
+ private DataSource ds;
+
+ @Inject
+ protected CiDataSourceProvider(PluginConfigFactory cfgFactory,
+ @PluginName String pluginName,
+ MetricMaker metrics,
+ Context ctx,
+ CiDataSourceType dst) {
+ this.cfg = cfgFactory.getGlobalPluginConfig(pluginName);
+ this.metrics = metrics;
+ this.ctx = ctx;
+ this.dst = dst;
+ }
+
+ @Override
+ public synchronized DataSource get() {
+ if (ds == null) {
+ ds = open(cfg, ctx, dst);
+ }
+ return ds;
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public synchronized void stop() {
+ if (ds instanceof BasicDataSource) {
+ try {
+ ((BasicDataSource) ds).close();
+ } catch (SQLException e) {
+ // Ignore the close failure.
+ }
+ }
+ }
+
+ public static enum Context {
+ SINGLE_USER, MULTI_USER
+ }
+
+ private DataSource open(Config cfg, Context context, CiDataSourceType dst) {
+ ConfigSection dbs = new ConfigSection(cfg, "database");
+ String driver = dbs.optional("driver");
+ if (Strings.isNullOrEmpty(driver)) {
+ driver = dst.getDriver();
+ }
+
+ String url = dbs.optional("url");
+ if (Strings.isNullOrEmpty(url)) {
+ url = dst.getUrl();
+ }
+
+ String username = dbs.optional("username");
+ String password = dbs.optional("password");
+ String interceptor = dbs.optional("dataSourceInterceptorClass");
+
+ boolean usePool;
+ if (context == Context.SINGLE_USER) {
+ usePool = false;
+ } else {
+ usePool = cfg.getBoolean("database", "connectionpool", dst.usePool());
+ }
+
+ if (usePool) {
+ final BasicDataSource ds = new BasicDataSource();
+ ds.setDriverClassName(driver);
+ ds.setUrl(url);
+ if (username != null && !username.isEmpty()) {
+ ds.setUsername(username);
+ }
+ if (password != null && !password.isEmpty()) {
+ ds.setPassword(password);
+ }
+ ds.setMaxActive(cfg.getInt("database", "poollimit", DEFAULT_POOL_LIMIT));
+ ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
+ ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
+ ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
+ "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
+ ds.setInitialSize(ds.getMinIdle());
+ exportPoolMetrics(ds);
+ return intercept(interceptor, ds);
+
+ } else {
+ // Don't use the connection pool.
+ //
+ try {
+ Properties p = new Properties();
+ p.setProperty("driver", driver);
+ p.setProperty("url", url);
+ if (username != null) {
+ p.setProperty("user", username);
+ }
+ if (password != null) {
+ p.setProperty("password", password);
+ }
+ return intercept(interceptor, new SimpleDataSource(p));
+ } catch (SQLException se) {
+ throw new ProvisionException("Database unavailable", se);
+ }
+ }
+ }
+
+ private void exportPoolMetrics(final BasicDataSource pool) {
+ final CallbackMetric1<Boolean, Integer> cnt = metrics.newCallbackMetric(
+ "sql/connection_pool/connections",
+ Integer.class,
+ new Description("SQL database connections")
+ .setGauge()
+ .setUnit("connections"),
+ Field.ofBoolean("active"));
+ metrics.newTrigger(cnt, new Runnable() {
+ @Override
+ public void run() {
+ synchronized (pool) {
+ cnt.set(true, pool.getNumActive());
+ cnt.set(false, pool.getNumIdle());
+ }
+ }
+ });
+ }
+
+ private DataSource intercept(String interceptor, DataSource ds) {
+ if (interceptor == null) {
+ return ds;
+ }
+ try {
+ Constructor<?> c = Class.forName(interceptor).getConstructor();
+ DataSourceInterceptor datasourceInterceptor =
+ (DataSourceInterceptor) c.newInstance();
+ return datasourceInterceptor.intercept("CiDb", ds);
+ } catch (ClassNotFoundException | SecurityException | NoSuchMethodException
+ | IllegalArgumentException | InstantiationException
+ | IllegalAccessException | InvocationTargetException e) {
+ throw new ProvisionException("Cannot intercept datasource", e);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceType.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceType.java
new file mode 100644
index 0000000..59f565b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceType.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import java.io.IOException;
+
+/** Abstraction of a supported database platform */
+public interface CiDataSourceType {
+
+ public String getDriver();
+
+ public String getUrl();
+
+ public boolean usePool();
+
+ /**
+ * Return a ScriptRunner that runs the index script. Must not return
+ * {@code null}, but may return a ScriptRunner that does nothing.
+ *
+ * @throws IOException
+ */
+ public ScriptRunner getIndexScript() throws IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceTypeGuesser.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceTypeGuesser.java
new file mode 100644
index 0000000..8417304
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDataSourceTypeGuesser.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import java.nio.file.Path;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+
+public class CiDataSourceTypeGuesser {
+
+ private final Config cfg;
+ private final String configFile;
+
+ @Inject
+ CiDataSourceTypeGuesser(SitePaths site,
+ PluginConfigFactory pluginConfig,
+ @PluginName String pluginName) {
+ this.cfg = pluginConfig.getGlobalPluginConfig(pluginName);
+ configFile = String.format("%s.config", pluginName);
+ Path config = site.resolve("etc").resolve(configFile);
+ if (!config.toFile().exists()) {
+ throw new ProvisionException(
+ String.format("Config file %s for plugin %s doesn't exist",
+ configFile, pluginName));
+ }
+ }
+
+ public String guessDataSourceType() {
+ String dbType = cfg.getString("database", null, "type");
+ if (Strings.isNullOrEmpty(dbType)) {
+ throw new ProvisionException(
+ String.format("'database.type' must be defined in: %s", configFile));
+ }
+ return dbType.toLowerCase();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDatabaseModule.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDatabaseModule.java
new file mode 100644
index 0000000..49c7b66
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDatabaseModule.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.TypeLiteral;
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+/** Loads the database with standard dependencies. */
+public class CiDatabaseModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ bind(new TypeLiteral<SchemaFactory<CiDb>>() {}).to(
+ new TypeLiteral<Database<CiDb>>() {}).in(SINGLETON);
+ bind(new TypeLiteral<Database<CiDb>>() {}).toProvider(
+ CiDbDatabaseProvider.class).in(SINGLETON);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDbDatabaseProvider.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDbDatabaseProvider.java
new file mode 100644
index 0000000..67cde9e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/CiDbDatabaseProvider.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import javax.sql.DataSource;
+
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+/** Provides the {@code Database<CiDb>} database handle. */
+final class CiDbDatabaseProvider implements Provider<Database<CiDb>> {
+ private final DataSource datasource;
+
+ @Inject
+ CiDbDatabaseProvider(@Named("CiDb") DataSource ds) {
+ datasource = ds;
+ }
+
+ @Override
+ public Database<CiDb> get() {
+ try {
+ return new Database<>(datasource, CiDb.class);
+ } catch (OrmException e) {
+ throw new ProvisionException("Cannot create CiDb", e);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Derby.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Derby.java
new file mode 100644
index 0000000..f89662e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Derby.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class Derby extends CiBaseDataSourceType {
+
+ private final Config cfg;
+ private final SitePaths site;
+
+ @Inject
+ Derby(SitePaths site,
+ PluginConfigFactory pluginConfig,
+ @PluginName String pluginName) {
+ super("org.apache.derby.jdbc.EmbeddedDriver");
+ this.cfg = pluginConfig.getGlobalPluginConfig(pluginName);
+ this.site = site;
+ }
+
+ @Override
+ public String getUrl() {
+ String database = cfg.getString("database", null, "database");
+ if (database == null || database.isEmpty()) {
+ database = "db/CiDB";
+ }
+ return "jdbc:derby:" + site.resolve(database).toString() + ";create=true";
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/H2.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/H2.java
new file mode 100644
index 0000000..7d29865
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/H2.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+class H2 extends CiBaseDataSourceType {
+
+ private final Config cfg;
+ private final SitePaths site;
+
+ @Inject
+ H2(SitePaths site,
+ PluginConfigFactory pluginConfig,
+ @PluginName String pluginName) {
+ super("org.h2.Driver");
+ this.cfg = pluginConfig.getGlobalPluginConfig(pluginName);
+ this.site = site;
+ }
+
+ @Override
+ public String getUrl() {
+ String database = cfg.getString("database", null, "database");
+ if (database == null || database.isEmpty()) {
+ database = "db/CiDB";
+ }
+ return "jdbc:h2:" + site.resolve(database).toUri().toString();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/MySql.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/MySql.java
new file mode 100644
index 0000000..b5ad309
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/MySql.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class MySql extends CiBaseDataSourceType {
+
+ private Config cfg;
+
+ @Inject
+ public MySql(@GerritServerConfig final Config cfg) {
+ super("com.mysql.jdbc.Driver");
+ this.cfg = cfg;
+ }
+
+ @Override
+ public String getUrl() {
+ final StringBuilder b = new StringBuilder();
+ final ConfigSection dbs = new ConfigSection(cfg, "database");
+ b.append("jdbc:mysql://");
+ b.append(hostname(dbs.optional("hostname")));
+ b.append(port(dbs.optional("port")));
+ b.append("/");
+ b.append(dbs.required("database"));
+ return b.toString();
+ }
+
+ @Override
+ public boolean usePool() {
+ // MySQL has given us trouble with the connection pool,
+ // sometimes the backend disconnects and the pool winds
+ // up with a stale connection. Fortunately opening up
+ // a new MySQL connection is usually very fast.
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Oracle.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Oracle.java
new file mode 100644
index 0000000..ce766a4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Oracle.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+public class Oracle extends CiBaseDataSourceType {
+ private Config cfg;
+
+ @Inject
+ public Oracle(@GerritServerConfig Config cfg) {
+ super("oracle.jdbc.driver.OracleDriver");
+ this.cfg = cfg;
+ }
+
+ @Override
+ public String getUrl() {
+ final StringBuilder b = new StringBuilder();
+ final ConfigSection dbc = new ConfigSection(cfg, "database");
+ b.append("jdbc:oracle:thin:@");
+ b.append(hostname(dbc.optional("hostname")));
+ b.append(port(dbc.optional("port")));
+ b.append(":");
+ b.append(dbc.required("instance"));
+ return b.toString();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/PostgreSQL.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/PostgreSQL.java
new file mode 100644
index 0000000..ed41857
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/PostgreSQL.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+class PostgreSQL extends CiBaseDataSourceType {
+
+ private Config cfg;
+
+ @Inject
+ public PostgreSQL(@GerritServerConfig Config cfg) {
+ super("org.postgresql.Driver");
+ this.cfg = cfg;
+ }
+
+ @Override
+ public String getUrl() {
+ final StringBuilder b = new StringBuilder();
+ final ConfigSection dbc = new ConfigSection(cfg, "database");
+ b.append("jdbc:postgresql://");
+ b.append(hostname(dbc.optional("hostname")));
+ b.append(port(dbc.optional("port")));
+ b.append("/");
+ b.append(dbc.required("database"));
+ return b.toString();
+ }
+
+ @Override
+ public ScriptRunner getIndexScript() throws IOException {
+ return getScriptRunner("index_postgres.sql");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/SchemaVersion.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/SchemaVersion.java
new file mode 100644
index 0000000..1517824
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/SchemaVersion.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Provider;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.List;
+
+/** A version of the database schema. */
+public abstract class SchemaVersion {
+ /** The current schema version. */
+ public static final Class<Schema_1> C = Schema_1.class;
+
+ public static int getBinaryVersion() {
+ return guessVersion(C);
+ }
+
+ private final Provider<? extends SchemaVersion> prior;
+ private final int versionNbr;
+
+ protected SchemaVersion(Provider<? extends SchemaVersion> prior) {
+ this.prior = prior;
+ this.versionNbr = guessVersion(getClass());
+ }
+
+ public static int guessVersion(Class<?> c) {
+ String n = c.getName();
+ n = n.substring(n.lastIndexOf('_') + 1);
+ while (n.startsWith("0")) {
+ n = n.substring(1);
+ }
+ return Integer.parseInt(n);
+ }
+
+ /** @return the {@link CurrentSchemaVersion#versionNbr} this step targets. */
+ public final int getVersionNbr() {
+ return versionNbr;
+ }
+
+ public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
+ throws OrmException, SQLException {
+ if (curr.versionNbr == versionNbr) {
+ // Nothing to do, we are at the correct schema.
+ } else if (curr.versionNbr > versionNbr) {
+ throw new OrmException("Cannot downgrade database schema from version "
+ + curr.versionNbr + " to " + versionNbr + ".");
+ } else {
+ upgradeFrom(ui, curr, db);
+ }
+ }
+
+ /** Runs check on the prior schema version, and then upgrades. */
+ private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
+ throws OrmException, SQLException {
+ List<SchemaVersion> pending = pending(curr.versionNbr);
+ updateSchema(pending, ui, db);
+ migrateData(pending, ui, curr, db);
+
+ JdbcSchema s = (JdbcSchema) db;
+ final List<String> pruneList = Lists.newArrayList();
+ s.pruneSchema(new StatementExecutor() {
+ @Override
+ public void execute(String sql) {
+ pruneList.add(sql);
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ });
+
+ try (JdbcExecutor e = new JdbcExecutor(s)) {
+ if (!pruneList.isEmpty()) {
+ ui.pruneSchema(e, pruneList);
+ }
+ }
+ }
+
+ private List<SchemaVersion> pending(int curr) {
+ List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
+ for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
+ r.add(v);
+ }
+ Collections.reverse(r);
+ return r;
+ }
+
+ private void updateSchema(List<SchemaVersion> pending, UpdateUI ui,
+ ReviewDb db) throws OrmException, SQLException {
+ for (SchemaVersion v : pending) {
+ ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
+ v.preUpdateSchema(db);
+ }
+
+ JdbcSchema s = (JdbcSchema) db;
+ try (JdbcExecutor e = new JdbcExecutor(s)) {
+ s.updateSchema(e);
+ }
+ }
+
+ /**
+ * Invoked before updateSchema adds new columns/tables.
+ *
+ * @param db open database handle.
+ * @throws OrmException if a Gerrit-specific exception occurred.
+ * @throws SQLException if an underlying SQL exception occurred.
+ */
+ protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
+ }
+
+ private void migrateData(List<SchemaVersion> pending, UpdateUI ui,
+ CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException {
+ for (SchemaVersion v : pending) {
+ ui.message(String.format(
+ "Migrating data to schema %d ...",
+ v.getVersionNbr()));
+ v.migrateData(db, ui);
+ v.finish(curr, db);
+ }
+ }
+
+ /**
+ * Invoked between updateSchema (adds new columns/tables) and pruneSchema
+ * (removes deleted columns/tables).
+ *
+ * @param db open database handle.
+ * @param ui interface for interacting with the user.
+ * @throws OrmException if a Gerrit-specific exception occurred.
+ * @throws SQLException if an underlying SQL exception occurred.
+ */
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ }
+
+ /** Mark the current schema version. */
+ protected void finish(CurrentSchemaVersion curr, ReviewDb db)
+ throws OrmException {
+ curr.versionNbr = versionNbr;
+ db.schemaVersion().update(Collections.singleton(curr));
+ }
+
+ /** Rename an existing table. */
+ protected static void renameTable(ReviewDb db, String from, String to)
+ throws OrmException {
+ JdbcSchema s = (JdbcSchema) db;
+ try (JdbcExecutor e = new JdbcExecutor(s)) {
+ s.renameTable(e, from, to);
+ }
+ }
+
+ /** Rename an existing column. */
+ protected static void renameColumn(ReviewDb db, String table, String from, String to)
+ throws OrmException {
+ JdbcSchema s = (JdbcSchema) db;
+ try (JdbcExecutor e = new JdbcExecutor(s)) {
+ s.renameField(e, table, from, to);
+ }
+ }
+
+ /** Execute an SQL statement. */
+ protected static void execute(ReviewDb db, String sql) throws SQLException {
+ try (Statement s = newStatement(db)) {
+ s.execute(sql);
+ }
+ }
+
+ /** Open a new single statement. */
+ protected static Statement newStatement(ReviewDb db) throws SQLException {
+ return ((JdbcSchema) db).getConnection().createStatement();
+ }
+
+ /** Open a new prepared statement. */
+ protected static PreparedStatement prepareStatement(ReviewDb db, String sql)
+ throws SQLException {
+ return ((JdbcSchema) db).getConnection().prepareStatement(sql);
+ }
+
+ /** Open a new statement executor. */
+ protected static JdbcExecutor newExecutor(ReviewDb db) throws OrmException {
+ return new JdbcExecutor(((JdbcSchema) db).getConnection());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Schema_1.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Schema_1.java
new file mode 100644
index 0000000..d4620ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/Schema_1.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+public class Schema_1 extends SchemaVersion {
+ @Inject
+ Schema_1() {
+ super(new Provider<SchemaVersion>() {
+ @Override
+ public SchemaVersion get() {
+ throw new ProvisionException("initial version");
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/ScriptRunner.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/ScriptRunner.java
new file mode 100644
index 0000000..d16d087
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/ScriptRunner.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.base.CharMatcher;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.googlesource.gerrit.plugins.ci.server.CiDb;
+
+/** Parses an SQL script from a resource file and later runs it. */
+class ScriptRunner {
+ private final String name;
+ private final List<String> commands;
+
+ static final ScriptRunner NOOP = new ScriptRunner(null, null) {
+ @Override
+ void run(CiDb db) {
+ }
+ };
+
+ ScriptRunner(String scriptName, InputStream script) {
+ this.name = scriptName;
+ try {
+ this.commands = script != null ? parse(script) : null;
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot parse " + name, e);
+ }
+ }
+
+ void run(CiDb db) throws OrmException {
+ try {
+ JdbcSchema schema = (JdbcSchema) db;
+ Connection c = schema.getConnection();
+ SqlDialect dialect = schema.getDialect();
+ try (Statement stmt = c.createStatement()) {
+ for (String sql : commands) {
+ try {
+ if (!dialect.isStatementDelimiterSupported()) {
+ sql = CharMatcher.is(';').trimTrailingFrom(sql);
+ }
+ stmt.execute(sql);
+ } catch (SQLException e) {
+ throw new OrmException("Error in " + name + ":\n" + sql, e);
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new OrmException("Cannot run statements for " + name, e);
+ }
+ }
+
+ private List<String> parse(InputStream in) throws IOException {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8))) {
+ String delimiter = ";";
+ List<String> commands = new ArrayList<>();
+ StringBuilder buffer = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.isEmpty()) {
+ continue;
+ }
+ if (line.startsWith("--")) {
+ continue;
+ }
+
+ if (buffer.length() == 0 && line.toLowerCase().startsWith("delimiter ")) {
+ delimiter = line.substring("delimiter ".length()).trim();
+ continue;
+ }
+
+ if (buffer.length() > 0) {
+ buffer.append('\n');
+ }
+ buffer.append(line);
+
+ if (isDone(delimiter, line, buffer)) {
+ String cmd = buffer.toString();
+ commands.add(cmd);
+ buffer = new StringBuilder();
+ }
+ }
+ if (buffer.length() > 0) {
+ commands.add(buffer.toString());
+ }
+ return commands;
+ }
+ }
+
+ private boolean isDone(String delimiter, String line, StringBuilder buffer) {
+ if (";".equals(delimiter)) {
+ return buffer.charAt(buffer.length() - 1) == ';';
+
+ } else if (line.equals(delimiter)) {
+ buffer.setLength(buffer.length() - delimiter.length());
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/UpdateUI.java b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/UpdateUI.java
new file mode 100644
index 0000000..4a1e0aa
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ci/server/schema/UpdateUI.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2015 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.ci.server.schema;
+
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+
+import java.util.List;
+
+public interface UpdateUI {
+ void message(String msg);
+
+ boolean yesno(boolean def, String msg);
+
+ boolean isBatch();
+
+ void pruneSchema(StatementExecutor e, List<String> pruneList)
+ throws OrmException;
+}