9b9aa3130b1e1002f1bebe8759389c4052dc75bb
[aaa.git] / aaa-idm-store-h2 / src / main / java / org / opendaylight / aaa / datastore / h2 / AbstractStore.java
1 /*
2  * Copyright © 2016 Red Hat, Inc. and others.
3  * Copyright (c) 2022 PANTHEON.tech, s.r.o.
4  *
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
8  */
9 package org.opendaylight.aaa.datastore.h2;
10
11 import static java.util.Objects.requireNonNull;
12
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;
25
26 /**
27  * Base class for H2 stores.
28  */
29 abstract class AbstractStore<T> {
30     private static final Logger LOG = LoggerFactory.getLogger(AbstractStore.class);
31
32     /**
33      * The name of the table used to represent this store.
34      */
35     private final @NonNull String tableName;
36     /**
37      * Database connection factory.
38      */
39     private final @NonNull ConnectionProvider dbConnectionFactory;
40     /**
41      * Table types we're interested in (when checking tables' existence).
42      */
43     @VisibleForTesting
44     static final String[] TABLE_TYPES = new String[] { "TABLE" };
45
46     /**
47      * Creates an instance.
48      *
49      * @param dbConnectionFactory factory to obtain JDBC Connections from
50      * @param tableName The name of the table being managed.
51      */
52     AbstractStore(final ConnectionProvider dbConnectionFactory, final String tableName) {
53         this.dbConnectionFactory = requireNonNull(dbConnectionFactory);
54         this.tableName = requireNonNull(tableName);
55     }
56
57     /**
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()}).
60      *
61      * @return A database connection.
62      * @throws StoreException if an error occurs.
63      */
64     final Connection dbConnect() throws StoreException {
65         final var conn = dbConnectionFactory.getConnection();
66         // Ensure table check/creation is atomic
67         synchronized (this) {
68             try {
69                 final var dbm = conn.getMetaData();
70                 try (var rs = dbm.getTables(null, null, tableName, TABLE_TYPES)) {
71                     if (!rs.next()) {
72                         LOG.info("Table {} does not exist, creating it", tableName);
73                         try (var stmt = conn.createStatement()) {
74                             createTable(stmt);
75                         }
76                     } else {
77                         LOG.debug("Table {} already exists", tableName);
78                     }
79                 }
80             } catch (SQLException e) {
81                 LOG.error("Error connecting to the H2 database", e);
82                 throw new StoreException("Cannot connect to database server", e);
83             }
84         }
85         return conn;
86     }
87
88     /**
89      * Create a managed table for on a particular connection..
90      *
91      * @param stmt A pre-allocated SQL statement
92      * @throws SQLException If table creation fails
93      */
94     abstract void createTable(Statement stmt) throws SQLException;
95
96     /**
97      * Empties the store.
98      *
99      * @throws StoreException if a connection error occurs.
100      */
101     @VisibleForTesting
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);
112         }
113     }
114
115     abstract void cleanTable(Statement stmt) throws SQLException;
116
117     /**
118      * Lists all the stored items.
119      *
120      * @return The stored item.
121      * @throws StoreException if an error occurs.
122      */
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)) {
130             while (rs.next()) {
131                 result.add(fromResultSet(rs));
132             }
133         } catch (SQLException e) {
134             LOG.error("Error listing all items from {}", tableName, e);
135             throw new StoreException(e);
136         }
137         return result;
138     }
139
140     /**
141      * Lists the stored items returned by the given statement.
142      *
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.
146      */
147     final List<T> listFromStatement(final PreparedStatement ps) throws StoreException {
148         final var result = new ArrayList<T>();
149         try (var rs = ps.executeQuery()) {
150             while (rs.next()) {
151                 result.add(fromResultSet(rs));
152             }
153         } catch (SQLException e) {
154             LOG.error("Error listing matching items from {}", tableName, e);
155             throw new StoreException(e);
156         }
157         return result;
158     }
159
160     /**
161      * Extracts the first item returned by the given statement, if any.
162      *
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.
166      */
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);
173         }
174     }
175
176     /**
177      * Converts a single row in a result set to an instance of the managed type.
178      *
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.
182      */
183     abstract T fromResultSet(ResultSet rs) throws SQLException;
184 }