2 * Copyright © 2016 Red Hat, Inc. and others.
3 * Copyright (c) 2022 PANTHEON.tech, s.r.o.
5 * This program and the accompanying materials are made available under the
6 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
7 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.aaa.datastore.h2;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.annotations.VisibleForTesting;
14 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import java.sql.Connection;
16 import java.sql.PreparedStatement;
17 import java.sql.ResultSet;
18 import java.sql.SQLException;
19 import java.sql.Statement;
20 import java.util.ArrayList;
21 import java.util.List;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
27 * Base class for H2 stores.
29 abstract class AbstractStore<T> {
30 private static final Logger LOG = LoggerFactory.getLogger(AbstractStore.class);
33 * The name of the table used to represent this store.
35 private final @NonNull String tableName;
37 * Database connection factory.
39 private final @NonNull ConnectionProvider dbConnectionFactory;
41 * Table types we're interested in (when checking tables' existence).
44 static final String[] TABLE_TYPES = new String[] { "TABLE" };
47 * Creates an instance.
49 * @param dbConnectionFactory factory to obtain JDBC Connections from
50 * @param tableName The name of the table being managed.
52 AbstractStore(final ConnectionProvider dbConnectionFactory, final String tableName) {
53 this.dbConnectionFactory = requireNonNull(dbConnectionFactory);
54 this.tableName = requireNonNull(tableName);
58 * Returns a database connection. It is the caller's responsibility to close it. If the managed table does not
59 * exist, it will be created (using {@link #getTableCreationStatement()}).
61 * @return A database connection.
62 * @throws StoreException if an error occurs.
64 final Connection dbConnect() throws StoreException {
65 final var conn = dbConnectionFactory.getConnection();
66 // Ensure table check/creation is atomic
69 final var dbm = conn.getMetaData();
70 try (var rs = dbm.getTables(null, null, tableName, TABLE_TYPES)) {
72 LOG.info("Table {} does not exist, creating it", tableName);
73 try (var stmt = conn.createStatement()) {
77 LOG.debug("Table {} already exists", tableName);
80 } catch (SQLException e) {
81 LOG.error("Error connecting to the H2 database", e);
82 throw new StoreException("Cannot connect to database server", e);
89 * Create a managed table for on a particular connection..
91 * @param stmt A pre-allocated SQL statement
92 * @throws SQLException If table creation fails
94 abstract void createTable(Statement stmt) throws SQLException;
99 * @throws StoreException if a connection error occurs.
102 @SuppressFBWarnings(value = "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE",
103 justification = "table name cannot be a parameter in a prepared statement")
104 final void dbClean() throws StoreException {
105 try (var c = dbConnect();
106 var statement = c.createStatement()) {
107 // FIXME: can we somehow make this a constant?
108 statement.execute("DELETE FROM " + tableName);
109 } catch (SQLException e) {
110 LOG.error("Error clearing table {}", tableName, e);
111 throw new StoreException("Error clearing table " + tableName, e);
115 abstract void cleanTable(Statement stmt) throws SQLException;
118 * Lists all the stored items.
120 * @return The stored item.
121 * @throws StoreException if an error occurs.
123 @SuppressFBWarnings(value = "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE",
124 justification = "table name cannot be a parameter in a prepared statement")
125 final List<T> listAll() throws StoreException {
126 List<T> result = new ArrayList<>();
127 try (var conn = dbConnect();
128 var stmt = conn.createStatement();
129 var rs = stmt.executeQuery("SELECT * FROM " + tableName)) {
131 result.add(fromResultSet(rs));
133 } catch (SQLException e) {
134 LOG.error("Error listing all items from {}", tableName, e);
135 throw new StoreException(e);
141 * Lists the stored items returned by the given statement.
143 * @param ps The statement (which must be ready for execution). It is the caller's responsibility to close this.
144 * @return The stored items.
145 * @throws StoreException if an error occurs.
147 final List<T> listFromStatement(final PreparedStatement ps) throws StoreException {
148 final var result = new ArrayList<T>();
149 try (var rs = ps.executeQuery()) {
151 result.add(fromResultSet(rs));
153 } catch (SQLException e) {
154 LOG.error("Error listing matching items from {}", tableName, e);
155 throw new StoreException(e);
161 * Extracts the first item returned by the given statement, if any.
163 * @param ps The statement (which must be ready for execution). It is the caller's responsibility to close this.
164 * @return The first item, or {@code null} if none.
165 * @throws StoreException if an error occurs.
167 final T firstFromStatement(final PreparedStatement ps) throws StoreException {
168 try (var rs = ps.executeQuery()) {
169 return rs.next() ? fromResultSet(rs) : null;
170 } catch (SQLException e) {
171 LOG.error("Error listing first matching item from {}", tableName, e);
172 throw new StoreException(e);
177 * Converts a single row in a result set to an instance of the managed type.
179 * @param rs The result set (which is ready for extraction; {@link ResultSet#next()} must <b>not</b> be called).
180 * @return The corresponding instance.
181 * @throws SQLException if an error occurs.
183 abstract T fromResultSet(ResultSet rs) throws SQLException;