File: ClientSession.java

package info (click to toggle)
eclipselink 2.7.11-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 44,820 kB
  • sloc: java: 477,843; xml: 503; makefile: 21
file content (908 lines) | stat: -rw-r--r-- 35,974 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
/*
 * Copyright (c) 1998, 2019 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019 IBM Corporation. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     05/28/2008-1.0M8 Andrei Ilitchev
//        - 224964: Provide support for Proxy Authentication through JPA.
//        Added a new constructor that takes Properties.
//     14/05/2012-2.4 Guy Pelletier
//       - 376603: Provide for table per tenant support for multitenant applications
//     08/11/2012-2.5 Guy Pelletier
//       - 393867: Named queries do not work when using EM level Table Per Tenant Multitenancy.
//     09/03/2015 - Will Dazey
//       - 456067 : Added support for defining query timeout units
package org.eclipse.persistence.sessions.server;

import java.util.*;
import java.io.*;
import org.eclipse.persistence.platform.server.ServerPlatform;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.SchemaPerMultitenantPolicy;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.sequencing.Sequencing;
import org.eclipse.persistence.internal.sequencing.SequencingFactory;
import org.eclipse.persistence.sessions.coordination.CommandManager;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.internal.sessions.*;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.SessionProfiler;
import org.eclipse.persistence.internal.sessions.AbstractSession;

/**
 * <b>Purpose</b>: Acts as a client to the server session.
 * <p>
 * <b>Description</b>: This session is brokered by the server session for use in three-tiered applications.
 * It is used to store the context of the connection, i.e. the login to be used for this client.
 * This allows each client connected to the server to contain its own user login.
 * <p>
 * <b>Responsibilities</b>:
 *    <ul>
 *    <li> Allow units of work to be acquired and pass them the client login's exclusive connection.
 *    <li> Forward all requests and queries to its parent server session.
 *    </ul>
 *  <p>
 * This class is an implementation of {@link org.eclipse.persistence.sessions.Session}.
 * Please refer to that class for a full API.  The public interface should be used.
 * @see Server
 * @see org.eclipse.persistence.sessions.Session
 * @see org.eclipse.persistence.sessions.UnitOfWork
 */
public class ClientSession extends AbstractSession {
    protected ServerSession parent;
    protected ConnectionPolicy connectionPolicy;
    protected Map<String, Accessor> writeConnections;
    protected boolean isActive;
    protected Sequencing sequencing;

    /**
     * INTERNAL:
     * Create and return a new client session.
     */
    public ClientSession(ServerSession parent, ConnectionPolicy connectionPolicy) {
        this(parent, connectionPolicy, null);
    }

    public ClientSession(ServerSession parent, ConnectionPolicy connectionPolicy, Map properties) {
        super();
        // If we have table per tenant descriptors let's clone the project so
        // that we can have a separate jpql parse cache for each tenant.
        if (parent.hasTablePerTenantDescriptors() || parent.getProject().getMultitenantPolicy() != null) {
            this.project = parent.getProject().clone();
            this.project.setJPQLParseCacheMaxSize(parent.getProject().getJPQLParseCache().getMaxSize());
        } else {
            this.project = parent.getProject();
        }

        if (connectionPolicy.isUserDefinedConnection()) {
            // PERF: project only requires clone if login is different
            this.setProject(getProject().clone());
            this.setLogin(connectionPolicy.getLogin());
        }
        if (this.project.getMultitenantPolicy() != null && this.project.getMultitenantPolicy().isSchemaPerMultitenantPolicy()) {
            SchemaPerMultitenantPolicy mp = (SchemaPerMultitenantPolicy) this.project.getMultitenantPolicy();
            if (mp.shouldUseSharedEMF()) {
                //force different login instance
                this.setLogin(getLogin().clone());
            }
        }
        this.isLoggingOff = parent.isLoggingOff();
        this.isActive = true;
        this.externalTransactionController = parent.getExternalTransactionController();
        this.parent = parent;
        this.connectionPolicy = connectionPolicy;
        this.name = parent.getName();
        this.profiler = parent.getProfiler();
        this.serializer = parent.getSerializer();
        this.isInProfile = parent.isInProfile();
        this.commitManager = parent.getCommitManager();
        this.partitioningPolicy = parent.getPartitioningPolicy();
        this.sessionLog = parent.getSessionLog();
        if (parent.hasEventManager()) {
            this.eventManager = parent.getEventManager().clone(this);
        }
        this.exceptionHandler = parent.getExceptionHandler();
        this.pessimisticLockTimeoutDefault = parent.getPessimisticLockTimeoutDefault();
        this.pessimisticLockTimeoutUnitDefault = parent.getPessimisticLockTimeoutUnitDefault();
        this.queryTimeoutDefault = parent.getQueryTimeoutDefault();
        this.queryTimeoutUnitDefault = parent.getQueryTimeoutUnitDefault();
        this.isConcurrent = parent.isConcurrent();
        this.shouldOptimizeResultSetAccess = parent.shouldOptimizeResultSetAccess();
        this.properties = properties;
        this.multitenantContextProperties = parent.getMultitenantContextProperties();

        if (this.eventManager != null) {
            this.eventManager.postAcquireClientSession();
        }

        // Copy down the table per tenant queries from the parent. These queries
        // must be cloned per client session.
        if (parent.hasTablePerTenantQueries()) {
            for (DatabaseQuery query : parent.getTablePerTenantQueries()) {
                addTablePerTenantQuery((DatabaseQuery) query.clone());
            }
        }
        // If we have table per tenant descriptors, they will need to be
        // cloned as we will be changing the descriptors per tenant.
        if (parent.hasTablePerTenantDescriptors()) {
            this.descriptors = new HashMap<Class, ClassDescriptor>();
            this.descriptors.putAll(parent.getDescriptors());

            for (ClassDescriptor descriptor : parent.getTablePerTenantDescriptors()) {
                ClassDescriptor clonedDescriptor = (ClassDescriptor) descriptor.clone();
                addTablePerTenantDescriptor(clonedDescriptor);
                this.descriptors.put(clonedDescriptor.getJavaClass(), clonedDescriptor);
            }

            if (hasProperties()) {
                for (Object propertyName : properties.keySet()) {
                    updateTablePerTenantDescriptors((String) propertyName, properties.get(propertyName));
                }
            }
        } else {
            this.descriptors = parent.getDescriptors();
        }

        incrementProfile(SessionProfiler.ClientSessionCreated);
    }

    protected ClientSession(org.eclipse.persistence.sessions.Project project) {
        super(project);
    }

    /**
     * INTERNAL:
     * Called in the end of beforeCompletion of external transaction synchronization listener.
     * Close the managed sql connection corresponding to the external transaction
     * and releases accessor.
     */
    @Override
    public void releaseJTSConnection() {
        if (hasWriteConnection()) {
            for (Accessor accessor : getWriteConnections().values()) {
                accessor.closeJTSConnection();
            }
            releaseWriteConnection();
        }
    }

    /**
     * INTERNAL:
     * This is internal to the unit of work and should not be called otherwise.
     */
    @Override
    public void basicCommitTransaction() {
        //Only release connection when transaction succeeds.
        //If not, connection will be released in rollback.
        super.basicCommitTransaction();

        // if synchronized then the connection will be released in external transaction callback.
        if (hasExternalTransactionController()) {
            if(!isSynchronized()) {
                releaseJTSConnection();
            }
        } else {
            releaseWriteConnection();
        }
    }

    /**
     * INTERNAL:
     * This is internal to the unit of work and should not be called otherwise.
     */
    @Override
    public void basicRollbackTransaction() {
        try {
            //BUG 2660471: Make sure there is an accessor (moved here from Session)
            //BUG 2846785: EXCEPTION THROWN IN PREBEGINTRANSACTION EVENT CAUSES NPE
            if (hasWriteConnection()) {
                super.basicRollbackTransaction();
            }
        } finally {
            // if synchronized then the connection will be released in external transaction callback.
            if (hasExternalTransactionController()) {
                if(!isSynchronized()) {
                    releaseJTSConnection();
                }
            } else {
                releaseWriteConnection();
            }
        }
    }

    /**
     * INTERNAL:
     * Connect the session only (this must be the write connection as the read is shared).
     */
    public void connect(Accessor accessor) throws DatabaseException {
        accessor.connect(getDatasourceLogin(), this);
    }

    /**
     * INTERNAL:
     * Was PUBLIC: customer will be redirected to {@link org.eclipse.persistence.sessions.Session}.
     * Return true if the pre-defined query is defined on the session.
     */
    @Override
    public boolean containsQuery(String queryName) {
        boolean containsQuery = getQueries().containsKey(queryName);
        if (containsQuery == false) {
            containsQuery = this.parent.containsQuery(queryName);
        }
        return containsQuery;
    }

    /**
     * INTERNAL:
     * Disconnect the accessor only (this must be the write connection as the read is shared).
     */
    public void disconnect(Accessor accessor) throws DatabaseException {
        accessor.disconnect(this);
    }

    /**
     * INTERNAL:
     * Execute the call on the correct connection accessor.
     * Outside of a transaction the server session's read connection pool is used.
     * In side a transaction, or for exclusive sessions the write connection is used.
     * For partitioning there may be multiple write connections.
     */
    @Override
    public Object executeCall(Call call, AbstractRecord translationRow, DatabaseQuery query) throws DatabaseException {
        if ((!isInTransaction() || (query.isObjectLevelReadQuery() && ((ObjectLevelReadQuery)query).isReadOnly())) && !isExclusiveIsolatedClientSession() ) {
            return this.parent.executeCall(call, translationRow, query);
        }
        boolean shouldReleaseConnection = false;
        if (query.getAccessors() == null) {
            // First check for a partitioning policy.
            // An exclusive session will always use a single connection once allocated.
            if (!hasWriteConnection()  || !isExclusiveIsolatedClientSession()) {
                Collection<Accessor> accessors = getAccessors(call, translationRow, query);
                if (accessors != null && !accessors.isEmpty()) {
                    query.setAccessors(accessors);
                    // the session has been already released and this query is likely instantiates a ValueHolder -
                    // release exclusive connection immediately after the query is executed, otherwise it may never be released.
                    shouldReleaseConnection = !this.isActive;
                }
            }
        }
        if (query.getAccessors() == null) {
            // If the connection has not yet been acquired then do it here.
            if (!hasWriteConnection()) {
                this.parent.acquireClientConnection(this);
                // The session has been already released and this query is likely instantiates a ValueHolder -
                // release exclusive connection immediately after the query is executed, otherwise it may never be released.
                shouldReleaseConnection = !this.isActive;
                query.setAccessors(getAccessors());
            } else {
                // Must use the default write connection if there are multiple connections.
                if (!isExclusiveIsolatedClientSession() && this.connectionPolicy.isPooled()) {
                    Accessor defaultWriteConnection = this.writeConnections.get(this.connectionPolicy.getPoolName());
                    if (defaultWriteConnection == null) {
                        // No default connection yet, must acquire it.
                        this.parent.acquireClientConnection(this);
                    }
                    if (this.writeConnections.size() == 1) {
                        // Connection is the default, just use it.
                        query.setAccessors(getAccessors());
                    } else {
                        List<Accessor> accessors = new ArrayList(1);
                        accessors.add(defaultWriteConnection);
                        query.setAccessors(accessors);
                    }
                } else {
                    query.setAccessors(getAccessors());
                }
            }
        }
        Object result = null;
        RuntimeException exception = null;
        try {
            result = basicExecuteCall(call, translationRow, query);
        } catch (RuntimeException caughtException) {
            exception = caughtException;
        } finally {
            if (call.isFinished() || exception != null) {
                query.setAccessors(null);
                // Note that connection could be release only if it has been acquired by the same query,
                // that allows to execute other queries from postAcquireConnection / preReleaseConnection events
                // without wiping out connection set by the original query or causing stack overflow, see
                // bug 299048 - Triggering indirection on closed ExclusiveIsolatedSession may cause exception
                if (shouldReleaseConnection && hasWriteConnection()) {
                    try {
                        this.parent.releaseClientSession(this);
                    } catch (RuntimeException releaseException) {
                        if (exception == null) {
                            throw releaseException;
                        }
                        //else ignore
                    }
                }
            } else {
                if (query.isObjectLevelReadQuery()) {
                    ((DatabaseCall)call).setHasAllocatedConnection(shouldReleaseConnection);
                }
            }
            if (exception != null) {
                throw exception;
            }
        }
        return result;
    }

    /**
     * INTERNAL:
     * Release (if required) connection after call.
     * @param query
     */
    public void releaseConnectionAfterCall(DatabaseQuery query) {
        if ((!isInTransaction() || (query.isObjectLevelReadQuery() && ((ObjectLevelReadQuery)query).isReadOnly())) && !isExclusiveIsolatedClientSession() ) {
            this.parent.releaseConnectionAfterCall(query);
        } else {
            if (hasWriteConnection()) {
                query.setAccessors(null);
                this.parent.releaseClientSession(this);
            }
        }
    }

    /**
     * INTERNAL:
     * Return the write connections if in a transaction.
     * These may be empty/null until the first query has been executed inside the transaction.
     * This should only be called within a transaction.
     * If outside of a transaction it will return null (unless using an exclusive connection).
     */
    @Override
    public Collection<Accessor> getAccessors() {
        if (isInTransaction()) {
            if (this.writeConnections == null) {
                return null;
            }
            return this.writeConnections.values();
        } else {
            return this.accessors;
        }
    }

    /**
     * INTERNAL:
     * This should normally not be used, getAccessors() should be used to support partitioning.
     * To maintain backward compatibility, and to support certain cases that required a default accessor,
     * if inside a transaction, then a default connection will be allocated.
     * This is required for sequencing, and JPA connection unwrapping, and ordt mappings.
     * Outside of a transaction, to maintain backward compatibility the server session's accessor will be returned.
     */
    @Override
    public Accessor getAccessor() {
        Collection<Accessor> accessors = getAccessors();
        if ((accessors == null) || accessors.isEmpty()) {
            if (isInTransaction()) {
                this.parent.acquireClientConnection(this);
                accessors = getAccessors();
            } else {
                return this.parent.getAccessor();
            }
        }
        if (accessors instanceof List) {
            return ((List<Accessor>)accessors).get(0);
        }
        return accessors.iterator().next();
    }

    /**
     * ADVANCED:
     * This method will return the connection policy that was used during the
     * acquisition of this client session.  The properties within the ConnectionPolicy
     * may be used when acquiring an exclusive connection for an IsolatedSession.
     */
    public ConnectionPolicy getConnectionPolicy() {
        return connectionPolicy;
    }

    /**
     * ADVANCED:
     * Return all registered descriptors.
     */
    public Map<Class, ClassDescriptor> getDescriptors() {
        // descriptors from the project may have been modified (for table per
        // tenants so make sure to return the updated ones)
        if (hasTablePerTenantDescriptors()) {
            return this.descriptors;
        } else {
            return super.getDescriptors();
        }
    }

    /**
     * INTERNAL:
     * Returns the appropriate IdentityMap session for this descriptor.  Sessions can be
     * chained and each session can have its own Cache/IdentityMap.  Entities can be stored
     * at different levels based on Cache Isolation.  This method will return the correct Session
     * for a particular Entity class based on the Isolation Level and the attributes provided.
     * <p>
     * @param canReturnSelf true when method calls itself.  If the path
     * starting at <code>this</code> is acceptable.  Sometimes true if want to
     * move to the first valid session, i.e. executing on ClientSession when really
     * should be on ServerSession.
     * @param terminalOnly return the last session in the chain where the Enitity is stored.
     * @return Session with the required IdentityMap
     */
    @Override
    public AbstractSession getParentIdentityMapSession(ClassDescriptor descriptor, boolean canReturnSelf, boolean terminalOnly) {
        // Note could return self as ClientSession shares the same identity map
        // as parent.  This reveals a deep problem, as queries will be cached in
        // the Server identity map but executed here using the write connection.
        return this.parent.getParentIdentityMapSession(descriptor, canReturnSelf, terminalOnly);
    }

    /**
     * Search for and return the user defined property from this client session, if it not found then search for the property
     * from parent.
     */
    @Override
    public Object getProperty(String name){
        Object propertyValue = super.getProperty(name);
        if (propertyValue == null) {
           propertyValue = this.parent.getProperty(name);
        }
        return propertyValue;
    }

    /**
     * INTERNAL:
     * Gets the session which this query will be executed on.
     * Generally will be called immediately before the call is translated,
     * which is immediately before session.executeCall.
     * <p>
     * Since the execution session also knows the correct datasource platform
     * to execute on, it is often used in the mappings where the platform is
     * needed for type conversion, or where calls are translated.
     * <p>
     * Is also the session with the accessor.  Will return a ClientSession if
     * it is in transaction and has a write connection.
     * @return a session with a live accessor
     * @param query may store session name or reference class for brokers case
     */
    @Override
    public AbstractSession getExecutionSession(DatabaseQuery query) {
        // For CR#4334 if in transaction stay on client session.
        // That way client's write accessor will be used for all queries.
        // This is to preserve transaction isolation levels.
        // For bug 3602222 if a query is executed directly on a client session when
        // in transaction, then dirty data could be put in the shared cache for the
        // client session uses the identity map of its parent.
        // However beginTransaction() is not public API on ClientSession.
        // if fix this could add: && (query.getSession() != this).
        if (isInTransaction()) {
            return this;
        }
        return this.parent.getExecutionSession(query);
    }

    /**
     * INTERNAL:
     * Return the parent.
     * This is a server session.
     */
    @Override
    public ServerSession getParent() {
        return parent;
    }

    /**
     * INTERNAL:
     * Was PUBLIC: customer will be redirected to {@link org.eclipse.persistence.sessions.Session}.
     * Return the query from the session pre-defined queries with the given name.
     * This allows for common queries to be pre-defined, reused and executed by name.
     */
    @Override
    public DatabaseQuery getQuery(String name) {
        DatabaseQuery query = super.getQuery(name);
        if (query == null) {
            query = this.parent.getQuery(name);
        }

        return query;
    }

    /**
     * INTERNAL:
     */
    @Override
    public DatabaseQuery getQuery(String name, Vector args) {// CR3716; Predrag;
        DatabaseQuery query = super.getQuery(name, args);
        if (query == null) {
            query = this.parent.getQuery(name, args);
        }
        return query;
    }

    /**
     * INTERNAL:
     * was ADVANCED:
     * Creates sequencing object for the session.
     * Typically there is no need for the user to call this method -
     * it is called from the constructor.
     */
    public void initializeSequencing() {
        this.sequencing = SequencingFactory.createSequencing(this);
    }

    /**
     * INTERNAL:
     * Return the Sequencing object used by the session.
     * Lazy init sequencing to defer from client session creation to improve creation performance.
     */
    @Override
    public Sequencing getSequencing() {
        // PERF: lazy init defer from constructor, only created when needed.
        if (this.sequencing == null) {
            initializeSequencing();
        }
        return this.sequencing;
    }

    /**
     * INTERNAL:
     * Marked internal as this is not customer API but helper methods for
     * accessing the server platform from within other sessions types
     * (i.e. not DatabaseSession)
     */
    @Override
    public ServerPlatform getServerPlatform() {
        return this.parent.getServerPlatform();
    }

    /**
     * INTERNAL:
     * Returns the type of session, its class.
     * <p>
     * Override to hide from the user when they are using an internal subclass
     * of a known class.
     * <p>
     * A user does not need to know that their UnitOfWork is a
     * non-deferred UnitOfWork, or that their ClientSession is an
     * IsolatedClientSession.
     */
    @Override
    public String getSessionTypeString() {
        return "ClientSession";
    }

    /**
     * INTERNAL:
     * Return the map of write connections.
     * Multiple connections can be used for data partitioning and replication.
     * The connections are keyed by connection pool name.
     */
    public Map<String, Accessor> getWriteConnections() {
        if (this.writeConnections == null) {
            this.writeConnections = new HashMap(4);
        }
        return this.writeConnections;
    }

    /**
     * INTERNAL:
     * Return the connection to be used for database modification.
     */
    public Accessor getWriteConnection() {
        if ((this.writeConnections == null) || this.writeConnections.isEmpty()) {
            return null;
        }
        return this.writeConnections.values().iterator().next();
    }

    /**
     * INTERNAL:
     * Return if this session has been connected.
     */
    public boolean hasWriteConnection() {
        if (this.writeConnections == null) {
            return false;
        }

        return !this.writeConnections.isEmpty();
    }

    /**
     * INTERNAL:
     * Set up the IdentityMapManager.  This method allows subclasses of Session to override
     * the default IdentityMapManager functionality.
     */
    @Override
    public void initializeIdentityMapAccessor() {
        this.identityMapAccessor = new ClientSessionIdentityMapAccessor(this);
    }

    /**
     * INTERNAL:
     * Was PUBLIC: customer will be redirected to {@link org.eclipse.persistence.sessions.Session}.
     * Return if the client session is active (has not been released).
     */
    public boolean isActive() {
        return isActive;
    }

    /**
     * INTERNAL:
     * Return if this session is a client session.
     */
    @Override
    public boolean isClientSession() {
        return true;
    }

    /**
     * INTERNAL:
     * Was PUBLIC: customer will be redirected to {@link org.eclipse.persistence.sessions.Session}.
     * Return if this session has been connected to the database.
     */
    @Override
    public boolean isConnected() {
        return this.parent.isConnected();
    }

    /**
     * INTERNAL:
     * Was PUBLIC: customer will be redirected to {@link org.eclipse.persistence.sessions.Session}.
     * Release the client session.
     * This releases the client session back to it server.
     * Normally this will logout of the client session's connection,
     * and allow the client session to garbage collect.
     */
    @Override
    public void release() throws DatabaseException {
        // Clear referencing classes. If this is not done the object is not garbage collected.
        for (Map.Entry<Class, ClassDescriptor> entry : getDescriptors().entrySet()) {
            entry.getValue().clearReferencingClasses();
        }

        if (!this.isActive) {
            return;
        }
        if (this.eventManager != null) {
            this.eventManager.preReleaseClientSession();
        }

        //removed is Lazy check as we should always release the connection once
        //the client session has been released.  It is also required for the
        //behavior of a subclass ExclusiveIsolatedClientSession
        if (hasWriteConnection()) {
            this.parent.releaseClientSession(this);
        }

        // we are not inactive until the connection is  released
        this.isActive = false;
        log(SessionLog.FINER, SessionLog.CONNECTION, "client_released");
        if (this.eventManager != null) {
            this.eventManager.postReleaseClientSession();
        }
        incrementProfile(SessionProfiler.ClientSessionReleased);
    }

    /**
     * INTERNAL:
     * A query execution failed due to an invalid query.
     * Re-connect and retry the query.
     */
    @Override
    public Object retryQuery(DatabaseQuery query, AbstractRecord row, DatabaseException databaseException, int retryCount, AbstractSession executionSession) {
        // If not in a transaction and has a write connection, must release it if invalid.
        getParent().releaseInvalidClientSession(this);
        return super.retryQuery(query, row, databaseException, retryCount, executionSession);
    }

    /**
     * INTERNAL:
     * This is internal to the unit of work and should not be called otherwise.
     */
    protected void releaseWriteConnection() {
        if (this.connectionPolicy.isLazy() && hasWriteConnection()) {
            this.parent.releaseClientSession(this);
        }
    }

    /**
     * INTERNAL:
     * Set the connection policy.
     */
    public void setConnectionPolicy(ConnectionPolicy connectionPolicy) {
        this.connectionPolicy = connectionPolicy;
    }

    /**
     * INTERNAL:
     * Set if the client session is active (has not been released).
     */
    protected void setIsActive(boolean isActive) {
        this.isActive = isActive;
    }

    /**
     * INTERNAL:
     * Set the parent.
     * This is a server session.
     */
    protected void setParent(ServerSession parent) {
        this.parent = parent;
    }

    /**
     * INTERNAL:
     * Add the connection to the client session.
     * Multiple connections are supported to allow data partitioning and replication.
     * The accessor is returned, as if detected to be dead it may be replaced.
     */
    public Accessor addWriteConnection(String poolName, Accessor writeConnection) {
        getWriteConnections().put(poolName, writeConnection);
        writeConnection.createCustomizer(this);
        //if connection is using external connection pooling then the event will be risen right after it connects.
        if (!writeConnection.usesExternalConnectionPooling()) {
            postAcquireConnection(writeConnection);
        }
        // Transactions are lazily started on connections.
        if (isInTransaction()) {
            basicBeginTransaction(writeConnection);
        }
        return getWriteConnections().get(poolName);
    }

    /**
     * INTERNAL:
     * A begin transaction failed.
     * Re-connect and retry the begin transaction.
     */
    @Override
    public DatabaseException retryTransaction(Accessor writeConnection, DatabaseException databaseException, int retryCount, AbstractSession executionSession) {
        if (writeConnection.getPool() == null) {
            return super.retryTransaction(writeConnection, databaseException, retryCount, executionSession);
        }
        String poolName = writeConnection.getPool().getName();
        DatabaseLogin login = getLogin();
        int count = login.getQueryRetryAttemptCount();
        DatabaseException exceptionToThrow = databaseException;
        while (retryCount < count) {
            getWriteConnections().remove(poolName);
            //if connection is using external connection pooling then the event will be risen right after it connects.
            if (!writeConnection.usesExternalConnectionPooling()) {
                preReleaseConnection(writeConnection);
            }
            writeConnection.getPool().releaseConnection(writeConnection);
            try {
                // attempt to reconnect for a certain number of times.
                // servers may take some time to recover.
                ++retryCount;
                writeConnection = writeConnection.getPool().acquireConnection();
                writeConnection.beginTransaction(this);
                //passing the retry count will prevent a runaway retry where
                // we can acquire connections but are unable to execute any queries
                if (retryCount > 1) {
                    // We are retrying more than once lets wait to give connection time to restart.
                    //Give the failover time to recover.
                    Thread.currentThread().sleep(login.getDelayBetweenConnectionAttempts());
                }
                getWriteConnections().put(poolName, writeConnection);
                writeConnection.createCustomizer(this);
                //if connection is using external connection pooling then the event will be risen right after it connects.
                if (!writeConnection.usesExternalConnectionPooling()) {
                    postAcquireConnection(writeConnection);
                }
                return null;
            } catch (DatabaseException ex){
                //replace original exception with last exception thrown
                //this exception could be a data based exception as opposed
                //to a connection exception that needs to go back to the customer.
                exceptionToThrow = ex;
            } catch (InterruptedException ex) {
                //Ignore interrupted exception.
            }
        }
        return exceptionToThrow;
    }

    /**
     * INTERNAL:
     * Set the connection to be used for database modification.
     */
    public void setWriteConnections(Map<String, Accessor> writeConnections) {
        // Clear customizers.
        if ((this.writeConnections != null) && (writeConnections == null)) {
            for (Accessor accessor : this.writeConnections.values()) {
                accessor.releaseCustomizer(this);
            }
        }
        this.writeConnections = writeConnections;
    }

    /**
     * INTERNAL:
     * Set the connection to be used for database modification.
     */
    public void setWriteConnection(Accessor writeConnection) {
        if (writeConnection == null) {
            setWriteConnections(null);
            return;
        }
        String poolName = null;
        if (writeConnection.getPool() != null) {
            poolName = writeConnection.getPool().getName();
        } else {
            poolName = ServerSession.NOT_POOLED;
        }
        addWriteConnection(poolName, writeConnection);
    }

    /**
     * INTERNAL:
     * Print the connection status with the session.
     */
    @Override
    public String toString() {
        StringWriter writer = new StringWriter();
        writer.write(getSessionTypeString());
        writer.write("(");
        writer.write(String.valueOf(getWriteConnections()));
        writer.write(")");
        return writer.toString();
    }

    /**
     * INTERNAL:
     * Return the manager that allows this processor to receive or propagate commands from/to TopLink cluster
     * @see CommandManager
     * @return a remote command manager
     */
    @Override
    public CommandManager getCommandManager() {
        return this.parent.getCommandManager();
    }

    /**
     * INTERNAL:
     * Return whether changes should be propagated to TopLink cluster.  This is one of the required
     * cache synchronization setting
     */
    @Override
    public boolean shouldPropagateChanges() {
        return this.parent.shouldPropagateChanges();
    }

    /**
     * INTERNAL:
     * Release the cursor query's connection.
     */
    @Override
    public void releaseReadConnection(Accessor connection) {
        // If the cursor's connection is the write connection, then do not release it.
        if ((this.writeConnections != null) && this.writeConnections.containsValue(connection)) {
            return;
        }
        //bug 4668234 -- used to only release connections on server sessions but should always release
        this.parent.releaseReadConnection(connection);
    }

    /**
     * INTERNAL:
     * This method is called in case externalConnectionPooling is used.
     * If returns true, accessor used by the session keeps its
     * connection open until released by the session.
     */
    @Override
    public boolean isExclusiveConnectionRequired() {
        return !this.connectionPolicy.isLazy && isActive();
    }
}