1 | /* |
2 | |
3 | Derby - Class org.apache.derby.client.net.NetXAResource |
4 | |
5 | Copyright (c) 2003, 2005 The Apache Software Foundation or its licensors, where applicable. |
6 | |
7 | Licensed under the Apache License, Version 2.0 (the "License"); |
8 | you may not use this file except in compliance with the License. |
9 | You may obtain a copy of the License at |
10 | |
11 | http://www.apache.org/licenses/LICENSE-2.0 |
12 | |
13 | Unless required by applicable law or agreed to in writing, software |
14 | distributed under the License is distributed on an "AS IS" BASIS, |
15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 | See the License for the specific language governing permissions and |
17 | limitations under the License. |
18 | |
19 | */ |
20 | /********************************************************************** |
21 | * |
22 | * |
23 | * Component Name = |
24 | * |
25 | * Package Name = org.apache.derby.client.net |
26 | * |
27 | * Descriptive Name = class implements XAResource |
28 | * |
29 | * Status = New code |
30 | * |
31 | * Function = Handle XA methods |
32 | * |
33 | * List of Classes |
34 | * - NetXAResource |
35 | * |
36 | * Restrictions : None |
37 | * |
38 | **********************************************************************/ |
39 | package org.apache.derby.client.net; |
40 | |
41 | import java.net.InetAddress; |
42 | import java.net.UnknownHostException; |
43 | import java.util.Collections; |
44 | import java.util.Enumeration; |
45 | import java.util.LinkedList; |
46 | import java.util.List; |
47 | import java.util.Vector; |
48 | import javax.sql.XAConnection; |
49 | import javax.transaction.xa.XAException; |
50 | import javax.transaction.xa.XAResource; |
51 | import javax.transaction.xa.Xid; |
52 | |
53 | import org.apache.derby.client.ClientXid; |
54 | import org.apache.derby.client.am.Connection; |
55 | import org.apache.derby.client.am.SqlException; |
56 | import org.apache.derby.client.am.ClientMessageId; |
57 | import org.apache.derby.shared.common.reference.SQLState; |
58 | |
59 | public class NetXAResource implements XAResource { |
60 | public static final int TMTIMEOUT = 0x00000100; |
61 | public static final int ACTIVE_ONLY = -1; |
62 | public static final int XA_NULL_XID = -1; // null Xid has Format Id of -1 |
63 | public static final int INITIAL_CALLINFO_ELEMENTS = 1; |
64 | public static final int RECOVER_XID_ARRAY_LENGTH = 10; |
65 | public static final ClientXid nullXid = new ClientXid(); |
66 | |
67 | // xaFunction defines, shows which queued XA function is being performed |
68 | public static final int XAFUNC_NONE = 0; |
69 | public static final int XAFUNC_COMMIT = 1; |
70 | public static final int XAFUNC_END = 2; |
71 | public static final int XAFUNC_FORGET = 3; |
72 | public static final int XAFUNC_PREPARE = 4; |
73 | public static final int XAFUNC_RECOVER = 5; |
74 | public static final int XAFUNC_ROLLBACK = 6; |
75 | public static final int XAFUNC_START = 7; |
76 | public static final String XAFUNCSTR_NONE = "No XA Function"; |
77 | public static final String XAFUNCSTR_COMMIT = "XAResource.commit()"; |
78 | public static final String XAFUNCSTR_END = "XAResource.end()"; |
79 | public static final String XAFUNCSTR_FORGET = "XAResource.forget()"; |
80 | public static final String XAFUNCSTR_PREPARE = "XAResource.prepare()"; |
81 | public static final String XAFUNCSTR_RECOVER = "XAResource.recover()"; |
82 | public static final String XAFUNCSTR_ROLLBACK = "XAResource.rollback()"; |
83 | public static final String XAFUNCSTR_START = "XAResource.start()"; |
84 | |
85 | public int nextElement = 0; |
86 | |
87 | // XAResources with same RM group list |
88 | protected static Vector xaResourceSameRMGroup_ = new Vector(); |
89 | protected int sameRMGroupIndex_ = 0; |
90 | protected NetXAResource nextSameRM_ = null; |
91 | protected boolean ignoreMe_ = false; |
92 | |
93 | |
94 | |
95 | public org.apache.derby.client.am.SqlException exceptionsOnXA = null; |
96 | |
97 | XAConnection xaconn_; |
98 | org.apache.derby.client.net.NetXAConnection netXAConn_; |
99 | org.apache.derby.client.net.NetConnection conn_; |
100 | int rmId_; // unique RmId generated by XAConnection |
101 | // TODO: change to a single callInfo field (not an array) |
102 | NetXACallInfo callInfoArray_[] = |
103 | new NetXACallInfo[INITIAL_CALLINFO_ELEMENTS]; |
104 | int numXACallInfo_ = INITIAL_CALLINFO_ELEMENTS; |
105 | int connectionCount_ = 1; |
106 | int activeXATransCount_ = 0; |
107 | String rmIdx_; // userid in case we need to create a secondary connection |
108 | String rmIdy_; // password in case we need to create a secondary connection |
109 | // TODO: remove port and ipaddr_ |
110 | int port_; // port needed to make secondary connection for recover in DS mode. |
111 | String ipaddr_; // ip address needed to make secondary connection for recover in DS mode. |
112 | |
113 | private List specialRegisters_ = Collections.synchronizedList(new LinkedList()); |
114 | |
115 | public NetXAResource(XAConnection xaconn, int rmId, |
116 | String userId, String password, |
117 | org.apache.derby.client.net.NetXAConnection conn) { |
118 | xaconn_ = xaconn; |
119 | rmId_ = rmId; |
120 | conn_ = conn.getNetConnection(); |
121 | netXAConn_ = conn; |
122 | rmIdx_ = userId; |
123 | rmIdy_ = password; |
124 | port_ = conn_.netAgent_.getPort(); |
125 | ipaddr_ = conn_.netAgent_.socket_.getLocalAddress().getHostAddress(); |
126 | conn.setNetXAResource(this); |
127 | |
128 | // link the primary connection to the first XACallInfo element |
129 | conn_.currXACallInfoOffset_ = 0; |
130 | |
131 | // construct the NetXACallInfo object for the array. |
132 | for (int i = 0; i < INITIAL_CALLINFO_ELEMENTS; ++i) { |
133 | callInfoArray_[i] = new NetXACallInfo(null, XAResource.TMNOFLAGS, this, |
134 | null); |
135 | } |
136 | |
137 | // initialize the first XACallInfo element with the information from the |
138 | // primary connection |
139 | callInfoArray_[0].actualConn_ = conn; |
140 | callInfoArray_[0].currConnection_ = true; |
141 | callInfoArray_[0].freeEntry_ = false; |
142 | // ~~~ save conn_ connection variables in callInfoArray_[0] |
143 | callInfoArray_[0].saveConnectionVariables(); |
144 | |
145 | // add this new XAResource to the list of other XAResources for the Same RM |
146 | initForReuse(); |
147 | } |
148 | |
149 | public void commit(Xid xid, boolean onePhase) throws XAException { |
150 | NetAgent netAgent = conn_.netAgent_; |
151 | int rc = XAResource.XA_OK; |
152 | |
153 | exceptionsOnXA = null; |
154 | if (conn_.agent_.loggingEnabled()) { |
155 | conn_.agent_.logWriter_.traceEntry(this, "commit", xid, onePhase); |
156 | } |
157 | if (conn_.isPhysicalConnClosed()) { |
158 | connectionClosedFailure(); |
159 | } |
160 | |
161 | // update the XACallInfo |
162 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
163 | callInfo.xaFlags_ = (onePhase ? XAResource.TMONEPHASE : |
164 | XAResource.TMNOFLAGS); |
165 | callInfo.xid_ = xid; |
166 | callInfo.xaResource_ = this; |
167 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
168 | try { |
169 | netAgent.beginWriteChainOutsideUOW(); |
170 | netAgent.netConnectionRequest_.writeXaCommit(conn_, xid); |
171 | netAgent.flowOutsideUOW(); |
172 | netAgent.netConnectionReply_.readXaCommit(conn_); |
173 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
174 | callInfo.xaFunction_ = XAFUNC_COMMIT; |
175 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
176 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
177 | } |
178 | netAgent.endReadChain(); |
179 | } catch (SqlException sqle) { |
180 | rc = XAException.XAER_RMERR; |
181 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
182 | (sqle, exceptionsOnXA); |
183 | } finally { |
184 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
185 | } |
186 | if (rc != XAResource.XA_OK) { |
187 | throwXAException(rc, false); |
188 | } |
189 | } |
190 | |
191 | /** |
192 | * Ends the work performed on behalf of a transaction branch. The resource manager dissociates the XA resource from |
193 | * the transaction branch specified and let the transaction be completed. |
194 | * <p/> |
195 | * If TMSUSPEND is specified in flags, the transaction branch is temporarily suspended in incomplete state. The |
196 | * transaction context is in suspened state and must be resumed via start with TMRESUME specified. |
197 | * <p/> |
198 | * If TMFAIL is specified, the portion of work has failed. The resource manager may mark the transaction as |
199 | * rollback-only |
200 | * <p/> |
201 | * If TMSUCCESS is specified, the portion of work has completed successfully. |
202 | * |
203 | * @param xid A global transaction identifier that is the same as what was used previously in the start method. |
204 | * @param flags One of TMSUCCESS, TMFAIL, or TMSUSPEND |
205 | * |
206 | * @throws XAException An error has occurred. Possible XAException values are XAER_RMERR, XAER_RMFAILED, XAER_NOTA, |
207 | * XAER_INVAL, XAER_PROTO, or XA_RB*. |
208 | */ |
209 | |
210 | public void end(Xid xid, int flags) throws XAException { |
211 | |
212 | NetAgent netAgent = conn_.netAgent_; |
213 | int rc = XAResource.XA_OK; |
214 | exceptionsOnXA = null; |
215 | if (conn_.agent_.loggingEnabled()) { |
216 | conn_.agent_.logWriter_.traceEntry(this, "end", xid, flags); |
217 | } |
218 | if (conn_.isPhysicalConnClosed()) { |
219 | connectionClosedFailure(); |
220 | } |
221 | |
222 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
223 | callInfo.setReadOnlyTransactionFlag(conn_.readOnlyTransaction_); |
224 | callInfo.xaFlags_ = flags; |
225 | callInfo.xid_ = xid; |
226 | callInfo.xaResource_ = this; |
227 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
228 | try { |
229 | netAgent.beginWriteChainOutsideUOW(); |
230 | netAgent.netConnectionRequest_.writeXaEndUnitOfWork(conn_); |
231 | netAgent.flowOutsideUOW(); |
232 | rc = netAgent.netConnectionReply_.readXaEndUnitOfWork(conn_); |
233 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending end |
234 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
235 | callInfo.xaFunction_ = XAFUNC_END; |
236 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
237 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
238 | } |
239 | netAgent.endReadChain(); |
240 | } catch (SqlException sqle) { |
241 | rc = XAException.XAER_RMERR; |
242 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
243 | (sqle, exceptionsOnXA); |
244 | } finally { |
245 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
246 | } |
247 | if (rc != XAResource.XA_OK) { |
248 | throwXAException(rc, false); |
249 | }else { |
250 | conn_.setXAState(Connection.XA_T0_NOT_ASSOCIATED); |
251 | } |
252 | } |
253 | |
254 | /** |
255 | * Tell the resource manager to forget about a heuristically (MANUALLY) completed transaction branch. |
256 | * |
257 | * @param xid A global transaction identifier |
258 | * |
259 | * @throws XAException An error has occurred. Possible exception values are XAER_RMERR, XAER_RMFAIL, XAER_NOTA, |
260 | * XAER_INVAL, or XAER_PROTO. |
261 | */ |
262 | |
263 | public void forget(Xid xid) throws XAException { |
264 | NetAgent netAgent = conn_.netAgent_; |
265 | int rc = XAResource.XA_OK; |
266 | exceptionsOnXA = null; |
267 | |
268 | if (conn_.agent_.loggingEnabled()) { |
269 | conn_.agent_.logWriter_.traceEntry(this, "forget", xid); |
270 | } |
271 | if (conn_.isPhysicalConnClosed()) { |
272 | connectionClosedFailure(); |
273 | } |
274 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
275 | callInfo.xid_ = xid; |
276 | callInfo.xaResource_ = this; |
277 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
278 | try { |
279 | // flow the required PROTOCOL to the server |
280 | netAgent.beginWriteChainOutsideUOW(); |
281 | |
282 | // sent the commit PROTOCOL |
283 | netAgent.netConnectionRequest_.writeXaForget(netAgent.netConnection_, xid); |
284 | |
285 | netAgent.flowOutsideUOW(); |
286 | |
287 | // read the reply to the commit |
288 | netAgent.netConnectionReply_.readXaForget(netAgent.netConnection_); |
289 | |
290 | netAgent.endReadChain(); |
291 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
292 | callInfo.xaFunction_ = XAFUNC_FORGET; |
293 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
294 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
295 | } |
296 | } catch (SqlException sqle) { |
297 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
298 | (sqle, exceptionsOnXA); |
299 | throwXAException(XAException.XAER_RMERR); |
300 | } finally { |
301 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
302 | } |
303 | if (rc != XAResource.XA_OK) { |
304 | throwXAException(rc, false); |
305 | } |
306 | |
307 | } |
308 | |
309 | /** |
310 | * Obtain the current transaction timeout value set for this XAResource instance. If |
311 | * <CODE>XAResource.setTransactionTimeout</CODE> was not use prior to invoking this method, the return value is the |
312 | * default timeout set for the resource manager; otherwise, the value used in the previous |
313 | * <CODE>setTransactionTimeout</CODE> call is returned. |
314 | * |
315 | * @return the transaction timeout value in seconds. |
316 | * |
317 | * @throws XAException An error has occurred. Possible exception values are XAER_RMERR, XAER_RMFAIL. |
318 | */ |
319 | public int getTransactionTimeout() throws XAException { |
320 | if (conn_.agent_.loggingEnabled()) { |
321 | conn_.agent_.logWriter_.traceEntry(this, "getTransactionTimeout"); |
322 | } |
323 | exceptionsOnXA = null; |
324 | if (conn_.isPhysicalConnClosed()) { |
325 | connectionClosedFailure(); |
326 | } |
327 | |
328 | if (conn_.agent_.loggingEnabled()) { |
329 | conn_.agent_.logWriter_.traceExit(this, "getTransactionTimeout", 0); |
330 | } |
331 | return 0; // we don't support transaction timeout |
332 | } |
333 | |
334 | /** |
335 | * Ask the resource manager to prepare for a transaction commit of the transaction specified in xid. |
336 | * |
337 | * @param xid A global transaction identifier |
338 | * |
339 | * @return A value indicating the resource manager's vote on the outcome of the transaction. The possible values |
340 | * are: XA_RDONLY or XA_OK. If the resource manager wants to roll back the transaction, it should do so by |
341 | * raising an appropriate XAException in the prepare method. |
342 | * |
343 | * @throws XAException An error has occurred. Possible exception values are: XA_RB*, XAER_RMERR, XAER_RMFAIL, |
344 | * XAER_NOTA, XAER_INVAL, or XAER_PROTO. |
345 | */ |
346 | public int prepare(Xid xid) throws XAException { // public interface for prepare |
347 | // just call prepareX with the recursion flag set to true |
348 | exceptionsOnXA = null; |
349 | |
350 | if (conn_.agent_.loggingEnabled()) { |
351 | conn_.agent_.logWriter_.traceEntry(this, "prepare", xid); |
352 | } |
353 | if (conn_.isPhysicalConnClosed()) { |
354 | connectionClosedFailure(); |
355 | } |
356 | |
357 | /// update the XACallInfo |
358 | NetAgent netAgent = conn_.netAgent_; |
359 | int rc = XAResource.XA_OK; |
360 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
361 | callInfo.xid_ = xid; |
362 | callInfo.xaResource_ = this; |
363 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
364 | try { |
365 | netAgent.beginWriteChainOutsideUOW(); |
366 | // sent the prepare PROTOCOL |
367 | netAgent.netConnectionRequest_.writeXaPrepare(conn_); |
368 | netAgent.flowOutsideUOW(); |
369 | |
370 | // read the reply to the prepare |
371 | rc = netAgent.netConnectionReply_.readXaPrepare(conn_); |
372 | if ((callInfo.xaRetVal_ != XAResource.XA_OK) && |
373 | (callInfo.xaRetVal_ != XAException.XA_RDONLY)) { // xaRetVal has possible error, format it |
374 | callInfo.xaFunction_ = XAFUNC_PREPARE; |
375 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
376 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
377 | } |
378 | |
379 | netAgent.endReadChain(); |
380 | } catch (SqlException sqle) { |
381 | rc = XAException.XAER_RMERR; |
382 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
383 | (sqle, exceptionsOnXA); |
384 | } finally { |
385 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
386 | } |
387 | if ((rc != XAResource.XA_OK ) && (rc != XAResource.XA_RDONLY)) { |
388 | throwXAException(rc, false); |
389 | } |
390 | if (conn_.agent_.loggingEnabled()) { |
391 | conn_.agent_.logWriter_.traceExit(this, "prepare", rc); |
392 | } |
393 | return rc; |
394 | } |
395 | |
396 | /** |
397 | * Obtain a list of prepared transaction branches from a resource manager. The transaction manager calls this method |
398 | * during recovery to obtain the list of transaction branches that are currently in prepared or heuristically |
399 | * completed states. |
400 | * |
401 | * @param flag One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must be used when no other flags are set in |
402 | * flags. |
403 | * |
404 | * @return The resource manager returns zero or more XIDs for the transaction branches that are currently in a |
405 | * prepared or heuristically completed state. If an error occurs during the operation, the resource manager |
406 | * should raise the appropriate XAException. |
407 | * |
408 | * @throws XAException An error has occurred. Possible values are XAER_RMERR, XAER_RMFAIL, XAER_INVAL, and |
409 | * XAER_PROTO. |
410 | */ |
411 | public Xid[] recover(int flag) throws XAException { |
412 | int rc = XAResource.XA_OK; |
413 | NetAgent netAgent = conn_.netAgent_; |
414 | |
415 | if (conn_.agent_.loggingEnabled()) { |
416 | conn_.agent_.logWriter_.traceEntry(this, "recover", flag); |
417 | } |
418 | exceptionsOnXA = null; |
419 | if (conn_.isPhysicalConnClosed()) { |
420 | connectionClosedFailure(); |
421 | } |
422 | |
423 | Xid[] xidList = null; |
424 | int numXid = 0; |
425 | |
426 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
427 | callInfo.xaFlags_ = flag; |
428 | callInfo.xaResource_ = this; |
429 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
430 | try { |
431 | netAgent.beginWriteChainOutsideUOW(); |
432 | // sent the recover PROTOCOL |
433 | netAgent.netConnectionRequest_.writeXaRecover(conn_, flag); |
434 | netAgent.flowOutsideUOW(); |
435 | netAgent.netConnectionReply_.readXaRecover(conn_); |
436 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
437 | callInfo.xaFunction_ = XAFUNC_RECOVER; |
438 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
439 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
440 | } |
441 | netAgent.endReadChain(); |
442 | if (conn_.indoubtTransactions_ != null) { |
443 | numXid = conn_.indoubtTransactions_.size(); |
444 | xidList = new Xid[numXid]; |
445 | int i = 0; |
446 | nextElement = 0; |
447 | for (Enumeration e = conn_.indoubtTransactions_.keys(); |
448 | e.hasMoreElements(); i++) { |
449 | xidList[i] = (Xid) e.nextElement(); |
450 | } |
451 | } |
452 | } catch (SqlException sqle) { |
453 | rc = XAException.XAER_RMERR; |
454 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
455 | (sqle, exceptionsOnXA); |
456 | } finally { |
457 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
458 | } |
459 | if (rc != XAResource.XA_OK) { |
460 | throwXAException(rc, false); |
461 | } |
462 | |
463 | if (conn_.agent_.loggingEnabled()) { |
464 | conn_.agent_.logWriter_.traceExit(this, "recover", xidList); |
465 | } |
466 | return xidList; |
467 | } |
468 | |
469 | /** |
470 | * Inform the resource manager to roll back work done on behalf of a transaction branch |
471 | * |
472 | * @param xid A global transaction identifier |
473 | * |
474 | * @throws XAException An error has occurred |
475 | */ |
476 | public void rollback(Xid xid) throws XAException { |
477 | NetAgent netAgent = conn_.netAgent_; |
478 | int rc = XAResource.XA_OK; |
479 | exceptionsOnXA = null; |
480 | |
481 | if (conn_.agent_.loggingEnabled()) { |
482 | conn_.agent_.logWriter_.traceEntry(this, "rollback", xid); |
483 | } |
484 | if (conn_.isPhysicalConnClosed()) { |
485 | connectionClosedFailure(); |
486 | } |
487 | |
488 | // update the XACallInfo |
489 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
490 | callInfo.xid_ = xid; |
491 | callInfo.xaResource_ = this; |
492 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
493 | try { |
494 | netAgent.beginWriteChainOutsideUOW(); |
495 | netAgent.netConnectionRequest_.writeXaRollback(conn_, xid); |
496 | netAgent.flowOutsideUOW(); |
497 | // read the reply to the rollback |
498 | rc = netAgent.netConnectionReply_.readXaRollback(conn_); |
499 | netAgent.endReadChain(); |
500 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
501 | callInfo.xaFunction_ = XAFUNC_END; |
502 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
503 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
504 | } |
505 | } catch (SqlException sqle) { |
506 | rc = XAException.XAER_RMERR; |
507 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
508 | (sqle, exceptionsOnXA); |
509 | } finally { |
510 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
511 | } |
512 | if (rc != XAResource.XA_OK) { |
513 | throwXAException(rc, false); |
514 | } |
515 | |
516 | } |
517 | |
518 | /** |
519 | * <P>Set the current transaction timeout value for this <CODE>XAResource</CODE> instance. This value overwrites the |
520 | * default transaction timeout value in the resource manager. The newly assigned timeout value is effective for the |
521 | * life of this <CODE>XAResource</CODE> instance unless a new value is set.<P> |
522 | * |
523 | * @param seconds the transaction timeout value in seconds. |
524 | * |
525 | * @throws XAException An error has occurred. Possible exception values are XAER_RMERR, XAER_RMFAIL, or XAER_INVAL. |
526 | */ |
527 | public boolean setTransactionTimeout(int seconds) throws XAException { |
528 | if (conn_.agent_.loggingEnabled()) { |
529 | conn_.agent_.logWriter_.traceExit(this, "setTransactionTimeout", false); |
530 | } |
531 | exceptionsOnXA = null; |
532 | return false; // we don't support transaction timeout in our layer. |
533 | /* int rc = xaSetTransTimeOut(seconds); |
534 | if (rc != XAResource.XA_OK) |
535 | throwXAException(rc); */ |
536 | } |
537 | |
538 | /** |
539 | * Start work on behalf of a transaction branch specified in xid |
540 | * |
541 | * @param xid A global transaction identifier to be associated with the resource |
542 | * @param flags One of TMNOFLAGS, TMJOIN, or TMRESUME |
543 | * |
544 | * @throws XAException An error has occurred. Possible exceptions * are XA_RB*, XAER_RMERR, XAER_RMFAIL, |
545 | * XAER_DUPID, XAER_OUTSIDE, XAER_NOTA, XAER_INVAL, or XAER_PROTO. |
546 | */ |
547 | public synchronized void start(Xid xid, int flags) throws XAException { |
548 | |
549 | NetAgent netAgent = conn_.netAgent_; |
550 | int rc = XAResource.XA_OK; |
551 | exceptionsOnXA = null; |
552 | if (conn_.agent_.loggingEnabled()) { |
553 | conn_.agent_.logWriter_.traceEntry(this, "start", xid, flags); |
554 | } |
555 | if (conn_.isPhysicalConnClosed()) { |
556 | connectionClosedFailure(); |
557 | } |
558 | |
559 | // DERBY-1025 - Flow an auto-commit if in auto-commit mode before |
560 | // entering a global transaction |
561 | try { |
562 | if(conn_.autoCommit_) |
563 | conn_.flowAutoCommit(); |
564 | } catch (SqlException sqle) { |
565 | rc = XAException.XAER_RMERR; |
566 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
567 | (sqle, exceptionsOnXA); |
568 | } |
569 | |
570 | // update the XACallInfo |
571 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
572 | callInfo.xaFlags_ = flags; |
573 | callInfo.xaInProgress_ = true; |
574 | callInfo.xid_ = xid; |
575 | callInfo.xaResource_ = this; |
576 | callInfo.xaRetVal_ = XAResource.XA_OK; // initialize XARETVAL |
577 | try { |
578 | netAgent.beginWriteChainOutsideUOW(); |
579 | netAgent.netConnectionRequest_.writeXaStartUnitOfWork(conn_); |
580 | netAgent.flowOutsideUOW(); |
581 | netAgent.netConnectionReply_.readXaStartUnitOfWork(conn_); |
582 | if (callInfo.xaRetVal_ != XAResource.XA_OK) { // xaRetVal has possible error, format it |
583 | callInfo.xaFunction_ = XAFUNC_START; |
584 | rc = xaRetValErrorAccumSQL(callInfo, rc); |
585 | callInfo.xaRetVal_ = XAResource.XA_OK; // re-initialize XARETVAL |
586 | } |
587 | // Setting this is currently required to avoid client from sending |
588 | // commit for autocommit. |
589 | if (rc == XAResource.XA_OK) { |
590 | conn_.setXAState(Connection.XA_T1_ASSOCIATED); |
591 | } |
592 | |
593 | } catch (SqlException sqle) { |
594 | rc = XAException.XAER_RMERR; |
595 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
596 | (sqle, exceptionsOnXA); |
597 | } finally { |
598 | conn_.pendingEndXACallinfoOffset_ = -1; // indicate no pending callinfo |
599 | } |
600 | if (rc != XAResource.XA_OK) { |
601 | throwXAException(rc, false); |
602 | } |
603 | } |
604 | |
605 | |
606 | protected void throwXAException(int rc) throws XAException { |
607 | throwXAException(rc, rc != XAException.XAER_NOTA); |
608 | } |
609 | |
610 | private String getXAExceptionText(int rc) { |
611 | String xaExceptionText; |
612 | switch (rc) { |
613 | case javax.transaction.xa.XAException.XA_RBROLLBACK: |
614 | xaExceptionText = "XA_RBROLLBACK"; |
615 | break; |
616 | case javax.transaction.xa.XAException.XA_RBCOMMFAIL: |
617 | xaExceptionText = "XA_RBCOMMFAIL"; |
618 | break; |
619 | case javax.transaction.xa.XAException.XA_RBDEADLOCK: |
620 | xaExceptionText = "XA_RBDEADLOCK"; |
621 | break; |
622 | case javax.transaction.xa.XAException.XA_RBINTEGRITY: |
623 | xaExceptionText = "XA_RBINTEGRITY"; |
624 | break; |
625 | case javax.transaction.xa.XAException.XA_RBOTHER: |
626 | xaExceptionText = "XA_RBOTHER"; |
627 | break; |
628 | case javax.transaction.xa.XAException.XA_RBPROTO: |
629 | xaExceptionText = "XA_RBPROTO"; |
630 | break; |
631 | case javax.transaction.xa.XAException.XA_RBTIMEOUT: |
632 | xaExceptionText = "XA_RBTIMEOUT"; |
633 | break; |
634 | case javax.transaction.xa.XAException.XA_RBTRANSIENT: |
635 | xaExceptionText = "XA_RBTRANSIENT"; |
636 | break; |
637 | case javax.transaction.xa.XAException.XA_NOMIGRATE: |
638 | xaExceptionText = "XA_NOMIGRATE"; |
639 | break; |
640 | case javax.transaction.xa.XAException.XA_HEURHAZ: |
641 | xaExceptionText = "XA_HEURHAZ"; |
642 | break; |
643 | case javax.transaction.xa.XAException.XA_HEURCOM: |
644 | xaExceptionText = "XA_HEURCOM"; |
645 | break; |
646 | case javax.transaction.xa.XAException.XA_HEURRB: |
647 | xaExceptionText = "XA_HEURRB"; |
648 | break; |
649 | case javax.transaction.xa.XAException.XA_HEURMIX: |
650 | xaExceptionText = "XA_HEURMIX"; |
651 | break; |
652 | case javax.transaction.xa.XAException.XA_RETRY: |
653 | xaExceptionText = "XA_RETRY"; |
654 | break; |
655 | case javax.transaction.xa.XAException.XA_RDONLY: |
656 | xaExceptionText = "XA_RDONLY"; |
657 | break; |
658 | case javax.transaction.xa.XAException.XAER_ASYNC: |
659 | xaExceptionText = "XAER_ASYNC"; |
660 | break; |
661 | case javax.transaction.xa.XAException.XAER_RMERR: |
662 | xaExceptionText = "XAER_RMERR"; |
663 | break; |
664 | case javax.transaction.xa.XAException.XAER_NOTA: |
665 | xaExceptionText = "XAER_NOTA"; |
666 | break; |
667 | case javax.transaction.xa.XAException.XAER_INVAL: |
668 | xaExceptionText = "XAER_INVAL"; |
669 | break; |
670 | case javax.transaction.xa.XAException.XAER_PROTO: |
671 | xaExceptionText = "XAER_PROTO"; |
672 | break; |
673 | case javax.transaction.xa.XAException.XAER_RMFAIL: |
674 | xaExceptionText = "XAER_RMFAIL"; |
675 | break; |
676 | case javax.transaction.xa.XAException.XAER_DUPID: |
677 | xaExceptionText = "XAER_DUPID"; |
678 | break; |
679 | case javax.transaction.xa.XAException.XAER_OUTSIDE: |
680 | xaExceptionText = "XAER_OUTSIDE"; |
681 | break; |
682 | case XAResource.XA_OK: |
683 | xaExceptionText = "XA_OK"; |
684 | break; |
685 | default: |
686 | xaExceptionText = "Unknown Error"; |
687 | break; |
688 | } |
689 | return xaExceptionText; |
690 | } |
691 | |
692 | protected void throwXAException(int rc, boolean resetFlag) throws XAException { // ~~~ |
693 | String xaExceptionText; |
694 | if (resetFlag) { |
695 | // reset the state of the failed connection |
696 | NetXACallInfo callInfo = callInfoArray_[conn_.currXACallInfoOffset_]; |
697 | callInfo.xaInProgress_ = false; |
698 | callInfo.xaWasSuspended = false; |
699 | } |
700 | |
701 | xaExceptionText = getXAExceptionText(rc); |
702 | // save the SqlException chain to add it to the XAException |
703 | org.apache.derby.client.am.SqlException sqlExceptions = exceptionsOnXA; |
704 | |
705 | while (exceptionsOnXA != null) { // one or more SqlExceptions received, format them |
706 | xaExceptionText = xaExceptionText + " : " + exceptionsOnXA.getMessage(); |
707 | exceptionsOnXA = (org.apache.derby.client.am.SqlException) |
708 | exceptionsOnXA.getNextException(); |
709 | } |
710 | org.apache.derby.client.am.XaException xaException = |
711 | new org.apache.derby.client.am.XaException(conn_.agent_.logWriter_, |
712 | sqlExceptions, |
713 | xaExceptionText); |
714 | xaException.errorCode = rc; |
715 | setXaStateForXAException(rc); |
716 | throw xaException; |
717 | } |
718 | |
719 | |
720 | /** |
721 | * Reset the transaction branch association state to XA_T0_NOT_ASSOCIATED |
722 | * for XAER_RM* and XA_RB* Exceptions. All other exeptions leave the state |
723 | * unchanged |
724 | * |
725 | * @param rc // return code from XAException |
726 | * @throws XAException |
727 | */ |
728 | private void setXaStateForXAException(int rc) { |
729 | switch (rc) |
730 | { |
731 | // Reset to T0, not associated for XA_RB*, RM* |
732 | // XAER_RMFAIL and XAER_RMERR will be fatal to the connection |
733 | // but that is not dealt with here |
734 | case javax.transaction.xa.XAException.XAER_RMFAIL: |
735 | case javax.transaction.xa.XAException.XAER_RMERR: |
736 | case javax.transaction.xa.XAException.XA_RBROLLBACK: |
737 | case javax.transaction.xa.XAException.XA_RBCOMMFAIL: |
738 | case javax.transaction.xa.XAException.XA_RBDEADLOCK: |
739 | case javax.transaction.xa.XAException.XA_RBINTEGRITY: |
740 | case javax.transaction.xa.XAException.XA_RBOTHER: |
741 | case javax.transaction.xa.XAException.XA_RBPROTO: |
742 | case javax.transaction.xa.XAException.XA_RBTIMEOUT: |
743 | case javax.transaction.xa.XAException.XA_RBTRANSIENT: |
744 | conn_.setXAState(Connection.XA_T0_NOT_ASSOCIATED); |
745 | break; |
746 | // No change for other XAExceptions |
747 | // javax.transaction.xa.XAException.XA_NOMIGRATE |
748 | //javax.transaction.xa.XAException.XA_HEURHAZ |
749 | // javax.transaction.xa.XAException.XA_HEURCOM |
750 | // javax.transaction.xa.XAException.XA_HEURRB |
751 | // javax.transaction.xa.XAException.XA_HEURMIX |
752 | // javax.transaction.xa.XAException.XA_RETRY |
753 | // javax.transaction.xa.XAException.XA_RDONLY |
754 | // javax.transaction.xa.XAException.XAER_ASYNC |
755 | // javax.transaction.xa.XAException.XAER_NOTA |
756 | // javax.transaction.xa.XAException.XAER_INVAL |
757 | // javax.transaction.xa.XAException.XAER_PROTO |
758 | // javax.transaction.xa.XAException.XAER_DUPID |
759 | // javax.transaction.xa.XAException.XAER_OUTSIDE |
760 | default: |
761 | return; |
762 | } |
763 | } |
764 | |
765 | public boolean isSameRM(XAResource xares) throws XAException { |
766 | boolean isSame = false; // preset that the RMs are NOT the same |
767 | exceptionsOnXA = null; |
768 | |
769 | if (conn_.agent_.loggingEnabled()) { |
770 | conn_.agent_.logWriter_.traceEntry(this, "isSameRM", xares); |
771 | } |
772 | if (conn_.isPhysicalConnClosed()) { |
773 | connectionClosedFailure(); |
774 | } |
775 | |
776 | if (xares instanceof org.apache.derby.client.net.NetXAResource) { // both are NetXAResource so check to see if this is the same RM |
777 | // remember, isSame is initialized to false |
778 | NetXAResource derbyxares = (NetXAResource) xares; |
779 | while (true) { |
780 | if (!conn_.databaseName_.equalsIgnoreCase(derbyxares.conn_.databaseName_)) { |
781 | break; // database names are not equal, not same RM |
782 | } |
783 | if (!conn_.netAgent_.server_.equalsIgnoreCase |
784 | (derbyxares.conn_.netAgent_.server_)) { // server name strings not equal, compare IP addresses |
785 | try { |
786 | // 1st convert "localhost" to actual server name |
787 | String server1 = this.processLocalHost(conn_.netAgent_.server_); |
788 | String server2 = |
789 | this.processLocalHost(derbyxares.conn_.netAgent_.server_); |
790 | // now convert the server name to ip address |
791 | InetAddress serverIP1 = InetAddress.getByName(server1); |
792 | InetAddress serverIP2 = InetAddress.getByName(server2); |
793 | if (!serverIP1.equals(serverIP2)) { |
794 | break; // server IPs are not equal, not same RM |
795 | } |
796 | } catch (UnknownHostException ue) { |
797 | break; |
798 | } |
799 | } |
800 | if (conn_.netAgent_.port_ != derbyxares.conn_.netAgent_.port_) { |
801 | break; // ports are not equal, not same RM |
802 | } |
803 | isSame = true; // everything the same, set RMs are the same |
804 | break; |
805 | } |
806 | } |
807 | |
808 | if (conn_.agent_.loggingEnabled()) { |
809 | conn_.agent_.logWriter_.traceExit |
810 | (this, "isSameRM", isSame); |
811 | } |
812 | return isSame; |
813 | } |
814 | |
815 | public static boolean xidsEqual(Xid xid1, Xid xid2) { // determine if the 2 xids contain the same values even if not same object |
816 | // comapre the format ids |
817 | if (xid1.getFormatId() != xid2.getFormatId()) { |
818 | return false; // format ids are not the same |
819 | } |
820 | |
821 | // compare the global transaction ids |
822 | int xid1Length = xid1.getGlobalTransactionId().length; |
823 | if (xid1Length != xid2.getGlobalTransactionId().length) { |
824 | return false; // length of the global trans ids are not the same |
825 | } |
826 | byte[] xid1Bytes = xid1.getGlobalTransactionId(); |
827 | byte[] xid2Bytes = xid2.getGlobalTransactionId(); |
828 | int i; |
829 | for (i = 0; i < xid1Length; ++i) { // check all bytes are the same |
830 | if (xid1Bytes[i] != xid2Bytes[i]) { |
831 | return false; // bytes in the global trans ids are not the same |
832 | } |
833 | } |
834 | |
835 | // compare the branch qualifiers |
836 | xid1Length = xid1.getBranchQualifier().length; |
837 | if (xid1Length != xid2.getBranchQualifier().length) { |
838 | return false; // length of the global trans ids are not the same |
839 | } |
840 | xid1Bytes = xid1.getBranchQualifier(); |
841 | xid2Bytes = xid2.getBranchQualifier(); |
842 | for (i = 0; i < xid1Length; ++i) { // check all bytes are the same |
843 | if (xid1Bytes[i] != xid2Bytes[i]) { |
844 | return false; // bytes in the global trans ids are not the same |
845 | } |
846 | } |
847 | |
848 | return true; // all of the fields are the same, xid1 == xid2 |
849 | } |
850 | |
851 | |
852 | public List getSpecialRegisters() { |
853 | return specialRegisters_; |
854 | } |
855 | |
856 | public void addSpecialRegisters(String s) { |
857 | if (s.substring(0, 1).equals("@")) { |
858 | // SET statement is coming from Client |
859 | if (specialRegisters_.remove(s.substring(1))) { |
860 | specialRegisters_.remove(s); |
861 | specialRegisters_.add(s.substring(1)); |
862 | } else { |
863 | specialRegisters_.remove(s); |
864 | specialRegisters_.add(s); |
865 | } |
866 | } else { // SET statement is coming from Server |
867 | specialRegisters_.remove(s); |
868 | specialRegisters_.add(s); |
869 | } |
870 | } |
871 | |
872 | private void connectionClosedFailure() throws XAException { // throw an XAException XAER_RMFAIL, with a chained SqlException - closed |
873 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
874 | (new SqlException(null, |
875 | new ClientMessageId(SQLState.NO_CURRENT_CONNECTION)), |
876 | exceptionsOnXA); |
877 | throwXAException(javax.transaction.xa.XAException.XAER_RMFAIL); |
878 | } |
879 | |
880 | private String getXAFuncStr(int xaFunc) { |
881 | switch (xaFunc) { |
882 | case XAFUNC_COMMIT: |
883 | return XAFUNCSTR_COMMIT; |
884 | case XAFUNC_END: |
885 | return XAFUNCSTR_END; |
886 | case XAFUNC_FORGET: |
887 | return XAFUNCSTR_FORGET; |
888 | case XAFUNC_PREPARE: |
889 | return XAFUNCSTR_PREPARE; |
890 | case XAFUNC_RECOVER: |
891 | return XAFUNCSTR_RECOVER; |
892 | case XAFUNC_ROLLBACK: |
893 | return XAFUNCSTR_ROLLBACK; |
894 | case XAFUNC_START: |
895 | return XAFUNCSTR_START; |
896 | } |
897 | return XAFUNCSTR_NONE; |
898 | } |
899 | |
900 | protected int xaRetValErrorAccumSQL(NetXACallInfo callInfo, int currentRC) { |
901 | |
902 | // xaRetVal_ is set by the server to be one of the |
903 | // standard constants from XAException. |
904 | int rc = callInfo.xaRetVal_; |
905 | |
906 | if (rc != XAResource.XA_OK) { // error was detected |
907 | // create an SqlException to report this error within |
908 | SqlException accumSql = new SqlException(conn_.netAgent_.logWriter_, |
909 | new ClientMessageId(SQLState.NET_XARETVAL_ERROR), |
910 | getXAFuncStr(callInfo.xaFunction_), |
911 | getXAExceptionText(rc), |
912 | org.apache.derby.client.am.SqlCode.queuedXAError); |
913 | exceptionsOnXA = org.apache.derby.client.am.Utils.accumulateSQLException |
914 | (accumSql, exceptionsOnXA); |
915 | |
916 | if (currentRC != XAResource.XA_OK) { // the rc passed into this function had an error also, prioritize error |
917 | if (currentRC < 0) { // rc passed in was a major error use it instead of current error |
918 | return currentRC; |
919 | } |
920 | } |
921 | } |
922 | return rc; |
923 | } |
924 | |
925 | private String processLocalHost(String serverName) { |
926 | if (serverName.equalsIgnoreCase("localhost")) { // this is a localhost, find hostname |
927 | try { |
928 | InetAddress localhostNameIA = InetAddress.getLocalHost(); |
929 | String localhostName = localhostNameIA.getHostName(); |
930 | return localhostName; |
931 | } catch (SecurityException se) { |
932 | return serverName; |
933 | } catch (UnknownHostException ue) { |
934 | return serverName; |
935 | } |
936 | } |
937 | // not "localhost", return original server name |
938 | return serverName; |
939 | } |
940 | |
941 | protected void removeXaresFromSameRMchain() { |
942 | // check all NetXAResources on the same RM for the NetXAResource to remove |
943 | try { |
944 | this.ignoreMe_ = true; // use the ignoreMe_ flag to indicate the |
945 | // XAResource to remove |
946 | NetXAResource prevXAResource = null; |
947 | NetXAResource currXAResource; |
948 | synchronized (xaResourceSameRMGroup_) { // make sure no one changes this vector list |
949 | currXAResource = (NetXAResource) xaResourceSameRMGroup_.elementAt(sameRMGroupIndex_); |
950 | while (currXAResource != null) { // is this the XAResource to remove? |
951 | if (currXAResource.ignoreMe_) { // this NetXAResource is the one to remove |
952 | if (prevXAResource != null) { // this XAResource is not first in chain, just move next to prev |
953 | prevXAResource.nextSameRM_ = currXAResource.nextSameRM_; |
954 | } else { // this XAResource is first in chain, just move next to root |
955 | xaResourceSameRMGroup_.set(sameRMGroupIndex_, |
956 | currXAResource.nextSameRM_); |
957 | } |
958 | return; |
959 | } |
960 | // this is not the NetXAResource to remove, try the next one |
961 | prevXAResource = currXAResource; |
962 | currXAResource = currXAResource.nextSameRM_; |
963 | } |
964 | } |
965 | } finally { |
966 | this.ignoreMe_ = false; |
967 | } |
968 | } |
969 | |
970 | |
971 | public void initForReuse() { |
972 | // add this new XAResource to the list of other XAResources for the Same RM |
973 | // first find out if there are any other XAResources for the same RM |
974 | // then check to make sure it is not already in the chain |
975 | synchronized (xaResourceSameRMGroup_) { // make sure no one changes this vector list |
976 | int groupCount = xaResourceSameRMGroup_.size(); |
977 | int index = 0; |
978 | int firstFreeElement = -1; |
979 | NetXAResource xaResourceGroup = null; |
980 | |
981 | for (; index < groupCount; ++index) { // check if this group is the same RM |
982 | xaResourceGroup = (NetXAResource) xaResourceSameRMGroup_.elementAt(index); |
983 | if (xaResourceGroup == null) { // this is a free element, save its index if first found |
984 | if (firstFreeElement == -1) { // first free element, save index |
985 | firstFreeElement = index; |
986 | } |
987 | continue; // go to next element |
988 | } |
989 | try { |
990 | if (xaResourceGroup.isSameRM(this)) { // it is the same RM add this XAResource to the chain if not there |
991 | NetXAResource nextXares = (NetXAResource) |
992 | xaResourceSameRMGroup_.elementAt(sameRMGroupIndex_); |
993 | while (nextXares != null) { // is this NetXAResource the one we are trying to add? |
994 | if (nextXares.equals(this)) { // the XAResource to be added already is in chain, don't add |
995 | break; |
996 | } |
997 | // Xid was not on that NetXAResource, try the next one |
998 | nextXares = nextXares.nextSameRM_; |
999 | } |
1000 | |
1001 | if (nextXares == null) { // XAResource to be added is not in the chain already, add it |
1002 | // add it at the head of the chain |
1003 | sameRMGroupIndex_ = index; |
1004 | this.nextSameRM_ = xaResourceGroup.nextSameRM_; |
1005 | xaResourceGroup.nextSameRM_ = this; |
1006 | } |
1007 | return; // done |
1008 | } |
1009 | } catch (XAException xae) { |
1010 | } |
1011 | } |
1012 | |
1013 | // no other same RM was found, add this as first of new group |
1014 | if (firstFreeElement == -1) { // no free element found, add new element to end |
1015 | xaResourceSameRMGroup_.add(this); |
1016 | sameRMGroupIndex_ = groupCount; |
1017 | } else { // use first free element found |
1018 | xaResourceSameRMGroup_.setElementAt(this, firstFreeElement); |
1019 | sameRMGroupIndex_ = firstFreeElement; |
1020 | } |
1021 | } |
1022 | } |
1023 | } |