1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: Christian d'Heureuse, www.source-code.biz |
5 | * |
6 | * This class is multi-licensed under LGPL, MPL 2.0, and EPL 1.0. |
7 | * |
8 | * This module is free software: you can redistribute it and/or |
9 | * modify it under the terms of the GNU Lesser General Public |
10 | * License as published by the Free Software Foundation, either |
11 | * version 3 of the License, or (at your option) any later version. |
12 | * See http://www.gnu.org/licenses/lgpl.html |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied |
16 | * warranty of MERCHANTABILITY or FITNESS FOR A |
17 | * PARTICULAR PURPOSE. See the GNU Lesser General Public |
18 | * License for more details. |
19 | */ |
20 | package org.h2.jdbcx; |
21 | |
22 | import java.io.PrintWriter; |
23 | import java.sql.Connection; |
24 | import java.sql.SQLException; |
25 | import java.util.ArrayList; |
26 | import javax.sql.ConnectionEvent; |
27 | import javax.sql.ConnectionEventListener; |
28 | import javax.sql.ConnectionPoolDataSource; |
29 | import javax.sql.DataSource; |
30 | import javax.sql.PooledConnection; |
31 | import org.h2.util.New; |
32 | import org.h2.message.DbException; |
33 | |
34 | //## Java 1.7 ## |
35 | import java.util.logging.Logger; |
36 | //*/ |
37 | |
38 | /** |
39 | * A simple standalone JDBC connection pool. |
40 | * It is based on the |
41 | * <a href="http://www.source-code.biz/snippets/java/8.htm"> |
42 | * MiniConnectionPoolManager written by Christian d'Heureuse (Java 1.5) |
43 | * </a>. It is used as follows: |
44 | * <pre> |
45 | * import java.sql.*; |
46 | * import org.h2.jdbcx.JdbcConnectionPool; |
47 | * public class Test { |
48 | * public static void main(String... args) throws Exception { |
49 | * JdbcConnectionPool cp = JdbcConnectionPool.create( |
50 | * "jdbc:h2:~/test", "sa", "sa"); |
51 | * for (String sql : args) { |
52 | * Connection conn = cp.getConnection(); |
53 | * conn.createStatement().execute(sql); |
54 | * conn.close(); |
55 | * } |
56 | * cp.dispose(); |
57 | * } |
58 | * } |
59 | * </pre> |
60 | * |
61 | * @author Christian d'Heureuse |
62 | * (<a href="http://www.source-code.biz">www.source-code.biz</a>) |
63 | * @author Thomas Mueller |
64 | */ |
65 | public class JdbcConnectionPool implements DataSource, ConnectionEventListener { |
66 | |
67 | private static final int DEFAULT_TIMEOUT = 30; |
68 | private static final int DEFAULT_MAX_CONNECTIONS = 10; |
69 | |
70 | private final ConnectionPoolDataSource dataSource; |
71 | private final ArrayList<PooledConnection> recycledConnections = New.arrayList(); |
72 | private PrintWriter logWriter; |
73 | private int maxConnections = DEFAULT_MAX_CONNECTIONS; |
74 | private int timeout = DEFAULT_TIMEOUT; |
75 | private int activeConnections; |
76 | private boolean isDisposed; |
77 | |
78 | protected JdbcConnectionPool(ConnectionPoolDataSource dataSource) { |
79 | this.dataSource = dataSource; |
80 | if (dataSource != null) { |
81 | try { |
82 | logWriter = dataSource.getLogWriter(); |
83 | } catch (SQLException e) { |
84 | // ignore |
85 | } |
86 | } |
87 | } |
88 | |
89 | /** |
90 | * Constructs a new connection pool. |
91 | * |
92 | * @param dataSource the data source to create connections |
93 | * @return the connection pool |
94 | */ |
95 | public static JdbcConnectionPool create(ConnectionPoolDataSource dataSource) { |
96 | return new JdbcConnectionPool(dataSource); |
97 | } |
98 | |
99 | /** |
100 | * Constructs a new connection pool for H2 databases. |
101 | * |
102 | * @param url the database URL of the H2 connection |
103 | * @param user the user name |
104 | * @param password the password |
105 | * @return the connection pool |
106 | */ |
107 | public static JdbcConnectionPool create(String url, String user, |
108 | String password) { |
109 | JdbcDataSource ds = new JdbcDataSource(); |
110 | ds.setURL(url); |
111 | ds.setUser(user); |
112 | ds.setPassword(password); |
113 | return new JdbcConnectionPool(ds); |
114 | } |
115 | |
116 | /** |
117 | * Sets the maximum number of connections to use from now on. |
118 | * The default value is 10 connections. |
119 | * |
120 | * @param max the maximum number of connections |
121 | */ |
122 | public synchronized void setMaxConnections(int max) { |
123 | if (max < 1) { |
124 | throw new IllegalArgumentException("Invalid maxConnections value: " + max); |
125 | } |
126 | this.maxConnections = max; |
127 | // notify waiting threads if the value was increased |
128 | notifyAll(); |
129 | } |
130 | |
131 | /** |
132 | * Gets the maximum number of connections to use. |
133 | * |
134 | * @return the max the maximum number of connections |
135 | */ |
136 | public synchronized int getMaxConnections() { |
137 | return maxConnections; |
138 | } |
139 | |
140 | /** |
141 | * Gets the maximum time in seconds to wait for a free connection. |
142 | * |
143 | * @return the timeout in seconds |
144 | */ |
145 | @Override |
146 | public synchronized int getLoginTimeout() { |
147 | return timeout; |
148 | } |
149 | |
150 | /** |
151 | * Sets the maximum time in seconds to wait for a free connection. |
152 | * The default timeout is 30 seconds. Calling this method with the |
153 | * value 0 will set the timeout to the default value. |
154 | * |
155 | * @param seconds the timeout, 0 meaning the default |
156 | */ |
157 | @Override |
158 | public synchronized void setLoginTimeout(int seconds) { |
159 | if (seconds == 0) { |
160 | seconds = DEFAULT_TIMEOUT; |
161 | } |
162 | this.timeout = seconds; |
163 | } |
164 | |
165 | /** |
166 | * Closes all unused pooled connections. |
167 | * Exceptions while closing are written to the log stream (if set). |
168 | */ |
169 | public synchronized void dispose() { |
170 | if (isDisposed) { |
171 | return; |
172 | } |
173 | isDisposed = true; |
174 | ArrayList<PooledConnection> list = recycledConnections; |
175 | for (int i = 0, size = list.size(); i < size; i++) { |
176 | closeConnection(list.get(i)); |
177 | } |
178 | } |
179 | |
180 | /** |
181 | * Retrieves a connection from the connection pool. If |
182 | * <code>maxConnections</code> connections are already in use, the method |
183 | * waits until a connection becomes available or <code>timeout</code> |
184 | * seconds elapsed. When the application is finished using the connection, |
185 | * it must close it in order to return it to the pool. |
186 | * If no connection becomes available within the given timeout, an exception |
187 | * with SQL state 08001 and vendor code 8001 is thrown. |
188 | * |
189 | * @return a new Connection object. |
190 | * @throws SQLException when a new connection could not be established, |
191 | * or a timeout occurred |
192 | */ |
193 | @Override |
194 | public Connection getConnection() throws SQLException { |
195 | long max = System.currentTimeMillis() + timeout * 1000; |
196 | do { |
197 | synchronized (this) { |
198 | if (activeConnections < maxConnections) { |
199 | return getConnectionNow(); |
200 | } |
201 | try { |
202 | wait(1000); |
203 | } catch (InterruptedException e) { |
204 | // ignore |
205 | } |
206 | } |
207 | } while (System.currentTimeMillis() <= max); |
208 | throw new SQLException("Login timeout", "08001", 8001); |
209 | } |
210 | |
211 | /** |
212 | * INTERNAL |
213 | */ |
214 | @Override |
215 | public Connection getConnection(String user, String password) { |
216 | throw new UnsupportedOperationException(); |
217 | } |
218 | |
219 | private Connection getConnectionNow() throws SQLException { |
220 | if (isDisposed) { |
221 | throw new IllegalStateException("Connection pool has been disposed."); |
222 | } |
223 | PooledConnection pc; |
224 | if (!recycledConnections.isEmpty()) { |
225 | pc = recycledConnections.remove(recycledConnections.size() - 1); |
226 | } else { |
227 | pc = dataSource.getPooledConnection(); |
228 | } |
229 | Connection conn = pc.getConnection(); |
230 | activeConnections++; |
231 | pc.addConnectionEventListener(this); |
232 | return conn; |
233 | } |
234 | |
235 | /** |
236 | * This method usually puts the connection back into the pool. There are |
237 | * some exceptions: if the pool is disposed, the connection is disposed as |
238 | * well. If the pool is full, the connection is closed. |
239 | * |
240 | * @param pc the pooled connection |
241 | */ |
242 | synchronized void recycleConnection(PooledConnection pc) { |
243 | if (activeConnections <= 0) { |
244 | throw new AssertionError(); |
245 | } |
246 | activeConnections--; |
247 | if (!isDisposed && activeConnections < maxConnections) { |
248 | recycledConnections.add(pc); |
249 | } else { |
250 | closeConnection(pc); |
251 | } |
252 | if (activeConnections >= maxConnections - 1) { |
253 | notifyAll(); |
254 | } |
255 | } |
256 | |
257 | private void closeConnection(PooledConnection pc) { |
258 | try { |
259 | pc.close(); |
260 | } catch (SQLException e) { |
261 | if (logWriter != null) { |
262 | e.printStackTrace(logWriter); |
263 | } |
264 | } |
265 | } |
266 | |
267 | /** |
268 | * INTERNAL |
269 | */ |
270 | @Override |
271 | public void connectionClosed(ConnectionEvent event) { |
272 | PooledConnection pc = (PooledConnection) event.getSource(); |
273 | pc.removeConnectionEventListener(this); |
274 | recycleConnection(pc); |
275 | } |
276 | |
277 | /** |
278 | * INTERNAL |
279 | */ |
280 | @Override |
281 | public void connectionErrorOccurred(ConnectionEvent event) { |
282 | // not used |
283 | } |
284 | |
285 | /** |
286 | * Returns the number of active (open) connections of this pool. This is the |
287 | * number of <code>Connection</code> objects that have been issued by |
288 | * getConnection() for which <code>Connection.close()</code> has |
289 | * not yet been called. |
290 | * |
291 | * @return the number of active connections. |
292 | */ |
293 | public synchronized int getActiveConnections() { |
294 | return activeConnections; |
295 | } |
296 | |
297 | /** |
298 | * INTERNAL |
299 | */ |
300 | @Override |
301 | public PrintWriter getLogWriter() { |
302 | return logWriter; |
303 | } |
304 | |
305 | /** |
306 | * INTERNAL |
307 | */ |
308 | @Override |
309 | public void setLogWriter(PrintWriter logWriter) { |
310 | this.logWriter = logWriter; |
311 | } |
312 | |
313 | /** |
314 | * [Not supported] Return an object of this class if possible. |
315 | * |
316 | * @param iface the class |
317 | */ |
318 | @Override |
319 | public <T> T unwrap(Class<T> iface) throws SQLException { |
320 | throw DbException.getUnsupportedException("unwrap"); |
321 | } |
322 | |
323 | /** |
324 | * [Not supported] Checks if unwrap can return an object of this class. |
325 | * |
326 | * @param iface the class |
327 | */ |
328 | @Override |
329 | public boolean isWrapperFor(Class<?> iface) throws SQLException { |
330 | throw DbException.getUnsupportedException("isWrapperFor"); |
331 | } |
332 | |
333 | /** |
334 | * [Not supported] |
335 | */ |
336 | //## Java 1.7 ## |
337 | @Override |
338 | public Logger getParentLogger() { |
339 | return null; |
340 | } |
341 | //*/ |
342 | |
343 | |
344 | } |