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: H2 Group |
5 | */ |
6 | package org.h2.jdbcx; |
7 | |
8 | import java.sql.Connection; |
9 | import java.sql.ResultSet; |
10 | import java.sql.SQLException; |
11 | import java.sql.Statement; |
12 | import java.util.ArrayList; |
13 | import javax.sql.ConnectionEvent; |
14 | import javax.sql.ConnectionEventListener; |
15 | import javax.sql.StatementEventListener; |
16 | import javax.sql.XAConnection; |
17 | import javax.transaction.xa.XAException; |
18 | import javax.transaction.xa.XAResource; |
19 | import javax.transaction.xa.Xid; |
20 | |
21 | import org.h2.api.ErrorCode; |
22 | import org.h2.jdbc.JdbcConnection; |
23 | import org.h2.util.JdbcUtils; |
24 | import org.h2.util.New; |
25 | |
26 | import org.h2.message.DbException; |
27 | import org.h2.message.TraceObject; |
28 | |
29 | |
30 | /** |
31 | * This class provides support for distributed transactions. |
32 | * An application developer usually does not use this interface. |
33 | * It is used by the transaction manager internally. |
34 | */ |
35 | public class JdbcXAConnection extends TraceObject implements XAConnection, |
36 | XAResource { |
37 | |
38 | private final JdbcDataSourceFactory factory; |
39 | |
40 | // this connection is kept open as long as the XAConnection is alive |
41 | private JdbcConnection physicalConn; |
42 | |
43 | // this connection is replaced whenever getConnection is called |
44 | private volatile Connection handleConn; |
45 | private final ArrayList<ConnectionEventListener> listeners = New.arrayList(); |
46 | private Xid currentTransaction; |
47 | private boolean prepared; |
48 | |
49 | static { |
50 | org.h2.Driver.load(); |
51 | } |
52 | |
53 | JdbcXAConnection(JdbcDataSourceFactory factory, int id, |
54 | JdbcConnection physicalConn) { |
55 | this.factory = factory; |
56 | setTrace(factory.getTrace(), TraceObject.XA_DATA_SOURCE, id); |
57 | this.physicalConn = physicalConn; |
58 | } |
59 | |
60 | /** |
61 | * Get the XAResource object. |
62 | * |
63 | * @return itself |
64 | */ |
65 | @Override |
66 | public XAResource getXAResource() { |
67 | debugCodeCall("getXAResource"); |
68 | return this; |
69 | } |
70 | |
71 | /** |
72 | * Close the physical connection. |
73 | * This method is usually called by the connection pool. |
74 | */ |
75 | @Override |
76 | public void close() throws SQLException { |
77 | debugCodeCall("close"); |
78 | Connection lastHandle = handleConn; |
79 | if (lastHandle != null) { |
80 | listeners.clear(); |
81 | lastHandle.close(); |
82 | } |
83 | if (physicalConn != null) { |
84 | try { |
85 | physicalConn.close(); |
86 | } finally { |
87 | physicalConn = null; |
88 | } |
89 | } |
90 | } |
91 | |
92 | /** |
93 | * Get a connection that is a handle to the physical connection. This method |
94 | * is usually called by the connection pool. This method closes the last |
95 | * connection handle if one exists. |
96 | * |
97 | * @return the connection |
98 | */ |
99 | @Override |
100 | public Connection getConnection() throws SQLException { |
101 | debugCodeCall("getConnection"); |
102 | Connection lastHandle = handleConn; |
103 | if (lastHandle != null) { |
104 | lastHandle.close(); |
105 | } |
106 | // this will ensure the rollback command is cached |
107 | physicalConn.rollback(); |
108 | handleConn = new PooledJdbcConnection(physicalConn); |
109 | return handleConn; |
110 | } |
111 | |
112 | /** |
113 | * Register a new listener for the connection. |
114 | * |
115 | * @param listener the event listener |
116 | */ |
117 | @Override |
118 | public void addConnectionEventListener(ConnectionEventListener listener) { |
119 | debugCode("addConnectionEventListener(listener);"); |
120 | listeners.add(listener); |
121 | } |
122 | |
123 | /** |
124 | * Remove the event listener. |
125 | * |
126 | * @param listener the event listener |
127 | */ |
128 | @Override |
129 | public void removeConnectionEventListener(ConnectionEventListener listener) { |
130 | debugCode("removeConnectionEventListener(listener);"); |
131 | listeners.remove(listener); |
132 | } |
133 | |
134 | /** |
135 | * INTERNAL |
136 | */ |
137 | void closedHandle() { |
138 | debugCode("closedHandle();"); |
139 | ConnectionEvent event = new ConnectionEvent(this); |
140 | // go backward so that a listener can remove itself |
141 | // (otherwise we need to clone the list) |
142 | for (int i = listeners.size() - 1; i >= 0; i--) { |
143 | ConnectionEventListener listener = listeners.get(i); |
144 | listener.connectionClosed(event); |
145 | } |
146 | handleConn = null; |
147 | } |
148 | |
149 | /** |
150 | * Get the transaction timeout. |
151 | * |
152 | * @return 0 |
153 | */ |
154 | @Override |
155 | public int getTransactionTimeout() { |
156 | debugCodeCall("getTransactionTimeout"); |
157 | return 0; |
158 | } |
159 | |
160 | /** |
161 | * Set the transaction timeout. |
162 | * |
163 | * @param seconds ignored |
164 | * @return false |
165 | */ |
166 | @Override |
167 | public boolean setTransactionTimeout(int seconds) { |
168 | debugCodeCall("setTransactionTimeout", seconds); |
169 | return false; |
170 | } |
171 | |
172 | /** |
173 | * Checks if this is the same XAResource. |
174 | * |
175 | * @param xares the other object |
176 | * @return true if this is the same object |
177 | */ |
178 | @Override |
179 | public boolean isSameRM(XAResource xares) { |
180 | debugCode("isSameRM(xares);"); |
181 | return xares == this; |
182 | } |
183 | |
184 | /** |
185 | * Get the list of prepared transaction branches. This method is called by |
186 | * the transaction manager during recovery. |
187 | * |
188 | * @param flag TMSTARTRSCAN, TMENDRSCAN, or TMNOFLAGS. If no other flags are |
189 | * set, TMNOFLAGS must be used. |
190 | * @return zero or more Xid objects |
191 | */ |
192 | @Override |
193 | public Xid[] recover(int flag) throws XAException { |
194 | debugCodeCall("recover", quoteFlags(flag)); |
195 | checkOpen(); |
196 | Statement stat = null; |
197 | try { |
198 | stat = physicalConn.createStatement(); |
199 | ResultSet rs = stat.executeQuery("SELECT * FROM " + |
200 | "INFORMATION_SCHEMA.IN_DOUBT ORDER BY TRANSACTION"); |
201 | ArrayList<Xid> list = New.arrayList(); |
202 | while (rs.next()) { |
203 | String tid = rs.getString("TRANSACTION"); |
204 | int id = getNextId(XID); |
205 | Xid xid = new JdbcXid(factory, id, tid); |
206 | list.add(xid); |
207 | } |
208 | rs.close(); |
209 | Xid[] result = new Xid[list.size()]; |
210 | list.toArray(result); |
211 | if (list.size() > 0) { |
212 | prepared = true; |
213 | } |
214 | return result; |
215 | } catch (SQLException e) { |
216 | XAException xa = new XAException(XAException.XAER_RMERR); |
217 | xa.initCause(e); |
218 | throw xa; |
219 | } finally { |
220 | JdbcUtils.closeSilently(stat); |
221 | } |
222 | } |
223 | |
224 | /** |
225 | * Prepare a transaction. |
226 | * |
227 | * @param xid the transaction id |
228 | * @return XA_OK |
229 | */ |
230 | @Override |
231 | public int prepare(Xid xid) throws XAException { |
232 | if (isDebugEnabled()) { |
233 | debugCode("prepare("+JdbcXid.toString(xid)+");"); |
234 | } |
235 | checkOpen(); |
236 | if (!currentTransaction.equals(xid)) { |
237 | throw new XAException(XAException.XAER_INVAL); |
238 | } |
239 | Statement stat = null; |
240 | try { |
241 | stat = physicalConn.createStatement(); |
242 | stat.execute("PREPARE COMMIT " + JdbcXid.toString(xid)); |
243 | prepared = true; |
244 | } catch (SQLException e) { |
245 | throw convertException(e); |
246 | } finally { |
247 | JdbcUtils.closeSilently(stat); |
248 | } |
249 | return XA_OK; |
250 | } |
251 | |
252 | /** |
253 | * Forget a transaction. |
254 | * This method does not have an effect for this database. |
255 | * |
256 | * @param xid the transaction id |
257 | */ |
258 | @Override |
259 | public void forget(Xid xid) { |
260 | if (isDebugEnabled()) { |
261 | debugCode("forget("+JdbcXid.toString(xid)+");"); |
262 | } |
263 | prepared = false; |
264 | } |
265 | |
266 | /** |
267 | * Roll back a transaction. |
268 | * |
269 | * @param xid the transaction id |
270 | */ |
271 | @Override |
272 | public void rollback(Xid xid) throws XAException { |
273 | if (isDebugEnabled()) { |
274 | debugCode("rollback("+JdbcXid.toString(xid)+");"); |
275 | } |
276 | try { |
277 | physicalConn.rollback(); |
278 | physicalConn.setAutoCommit(true); |
279 | if (prepared) { |
280 | Statement stat = null; |
281 | try { |
282 | stat = physicalConn.createStatement(); |
283 | stat.execute("ROLLBACK TRANSACTION " + JdbcXid.toString(xid)); |
284 | } finally { |
285 | JdbcUtils.closeSilently(stat); |
286 | } |
287 | prepared = false; |
288 | } |
289 | } catch (SQLException e) { |
290 | throw convertException(e); |
291 | } |
292 | currentTransaction = null; |
293 | } |
294 | |
295 | /** |
296 | * End a transaction. |
297 | * |
298 | * @param xid the transaction id |
299 | * @param flags TMSUCCESS, TMFAIL, or TMSUSPEND |
300 | */ |
301 | @Override |
302 | public void end(Xid xid, int flags) throws XAException { |
303 | if (isDebugEnabled()) { |
304 | debugCode("end("+JdbcXid.toString(xid)+", "+quoteFlags(flags)+");"); |
305 | } |
306 | // TODO transaction end: implement this method |
307 | if (flags == TMSUSPEND) { |
308 | return; |
309 | } |
310 | if (!currentTransaction.equals(xid)) { |
311 | throw new XAException(XAException.XAER_OUTSIDE); |
312 | } |
313 | prepared = false; |
314 | } |
315 | |
316 | /** |
317 | * Start or continue to work on a transaction. |
318 | * |
319 | * @param xid the transaction id |
320 | * @param flags TMNOFLAGS, TMJOIN, or TMRESUME |
321 | */ |
322 | @Override |
323 | public void start(Xid xid, int flags) throws XAException { |
324 | if (isDebugEnabled()) { |
325 | debugCode("start("+JdbcXid.toString(xid)+", "+quoteFlags(flags)+");"); |
326 | } |
327 | if (flags == TMRESUME) { |
328 | return; |
329 | } |
330 | if (flags == TMJOIN) { |
331 | if (currentTransaction != null && !currentTransaction.equals(xid)) { |
332 | throw new XAException(XAException.XAER_RMERR); |
333 | } |
334 | } else if (currentTransaction != null) { |
335 | throw new XAException(XAException.XAER_NOTA); |
336 | } |
337 | try { |
338 | physicalConn.setAutoCommit(false); |
339 | } catch (SQLException e) { |
340 | throw convertException(e); |
341 | } |
342 | currentTransaction = xid; |
343 | prepared = false; |
344 | } |
345 | |
346 | /** |
347 | * Commit a transaction. |
348 | * |
349 | * @param xid the transaction id |
350 | * @param onePhase use a one-phase protocol if true |
351 | */ |
352 | @Override |
353 | public void commit(Xid xid, boolean onePhase) throws XAException { |
354 | if (isDebugEnabled()) { |
355 | debugCode("commit("+JdbcXid.toString(xid)+", "+onePhase+");"); |
356 | } |
357 | Statement stat = null; |
358 | try { |
359 | if (onePhase) { |
360 | physicalConn.commit(); |
361 | } else { |
362 | stat = physicalConn.createStatement(); |
363 | stat.execute("COMMIT TRANSACTION " + JdbcXid.toString(xid)); |
364 | prepared = false; |
365 | } |
366 | physicalConn.setAutoCommit(true); |
367 | } catch (SQLException e) { |
368 | throw convertException(e); |
369 | } finally { |
370 | JdbcUtils.closeSilently(stat); |
371 | } |
372 | currentTransaction = null; |
373 | } |
374 | |
375 | /** |
376 | * [Not supported] Add a statement event listener. |
377 | * |
378 | * @param listener the new statement event listener |
379 | */ |
380 | @Override |
381 | public void addStatementEventListener(StatementEventListener listener) { |
382 | throw new UnsupportedOperationException(); |
383 | } |
384 | |
385 | /** |
386 | * [Not supported] Remove a statement event listener. |
387 | * |
388 | * @param listener the statement event listener |
389 | */ |
390 | @Override |
391 | public void removeStatementEventListener(StatementEventListener listener) { |
392 | throw new UnsupportedOperationException(); |
393 | } |
394 | |
395 | /** |
396 | * INTERNAL |
397 | */ |
398 | @Override |
399 | public String toString() { |
400 | return getTraceObjectName() + ": " + physicalConn; |
401 | } |
402 | |
403 | private static XAException convertException(SQLException e) { |
404 | XAException xa = new XAException(e.getMessage()); |
405 | xa.initCause(e); |
406 | return xa; |
407 | } |
408 | |
409 | private static String quoteFlags(int flags) { |
410 | StringBuilder buff = new StringBuilder(); |
411 | if ((flags & XAResource.TMENDRSCAN) != 0) { |
412 | buff.append("|XAResource.TMENDRSCAN"); |
413 | } |
414 | if ((flags & XAResource.TMFAIL) != 0) { |
415 | buff.append("|XAResource.TMFAIL"); |
416 | } |
417 | if ((flags & XAResource.TMJOIN) != 0) { |
418 | buff.append("|XAResource.TMJOIN"); |
419 | } |
420 | if ((flags & XAResource.TMONEPHASE) != 0) { |
421 | buff.append("|XAResource.TMONEPHASE"); |
422 | } |
423 | if ((flags & XAResource.TMRESUME) != 0) { |
424 | buff.append("|XAResource.TMRESUME"); |
425 | } |
426 | if ((flags & XAResource.TMSTARTRSCAN) != 0) { |
427 | buff.append("|XAResource.TMSTARTRSCAN"); |
428 | } |
429 | if ((flags & XAResource.TMSUCCESS) != 0) { |
430 | buff.append("|XAResource.TMSUCCESS"); |
431 | } |
432 | if ((flags & XAResource.TMSUSPEND) != 0) { |
433 | buff.append("|XAResource.TMSUSPEND"); |
434 | } |
435 | if ((flags & XAResource.XA_RDONLY) != 0) { |
436 | buff.append("|XAResource.XA_RDONLY"); |
437 | } |
438 | if (buff.length() == 0) { |
439 | buff.append("|XAResource.TMNOFLAGS"); |
440 | } |
441 | return buff.toString().substring(1); |
442 | } |
443 | |
444 | private void checkOpen() throws XAException { |
445 | if (physicalConn == null) { |
446 | throw new XAException(XAException.XAER_RMERR); |
447 | } |
448 | } |
449 | |
450 | /** |
451 | * A pooled connection. |
452 | */ |
453 | class PooledJdbcConnection extends JdbcConnection { |
454 | |
455 | private boolean isClosed; |
456 | |
457 | public PooledJdbcConnection(JdbcConnection conn) { |
458 | super(conn); |
459 | } |
460 | |
461 | @Override |
462 | public synchronized void close() throws SQLException { |
463 | if (!isClosed) { |
464 | try { |
465 | rollback(); |
466 | setAutoCommit(true); |
467 | } catch (SQLException e) { |
468 | // ignore |
469 | } |
470 | closedHandle(); |
471 | isClosed = true; |
472 | } |
473 | } |
474 | |
475 | @Override |
476 | public synchronized boolean isClosed() throws SQLException { |
477 | return isClosed || super.isClosed(); |
478 | } |
479 | |
480 | @Override |
481 | protected synchronized void checkClosed(boolean write) { |
482 | if (isClosed) { |
483 | throw DbException.get(ErrorCode.OBJECT_CLOSED); |
484 | } |
485 | super.checkClosed(write); |
486 | } |
487 | |
488 | } |
489 | |
490 | } |