001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.pyranid;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.function.Consumer;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Options for a Pyranid-managed transaction.
032 * <p>
033 * Use this when transaction behavior differs from the defaults. For example:
034 * <pre>{@code
035 * database.transaction(
036 *   TransactionOptions.withIsolation(TransactionIsolation.REPEATABLE_READ)
037 *     .readOnly(true)
038 *     .build(),
039 *   () -> {
040 *     // transactional work
041 *   });
042 * }</pre>
043 *
044 * @author <a href="https://www.revetkn.com">Mark Allen</a>
045 * @since 4.2.0
046 */
047@ThreadSafe
048public final class TransactionOptions {
049        @NonNull
050        static final TransactionOptions DEFAULT;
051
052        static {
053                DEFAULT = new TransactionOptions(TransactionIsolation.DEFAULT, null, null);
054        }
055
056        @NonNull
057        private final TransactionIsolation isolation;
058        @Nullable
059        private final Boolean readOnly;
060        @Nullable
061        private final Consumer<@NonNull TransactionCompletion> completionObserver;
062
063        private TransactionOptions(@NonNull TransactionIsolation isolation,
064                                                                                                                 @Nullable Boolean readOnly,
065                        @Nullable Consumer<@NonNull TransactionCompletion> completionObserver) {
066                requireNonNull(isolation);
067
068                this.isolation = isolation;
069                this.readOnly = readOnly;
070                this.completionObserver = completionObserver;
071        }
072
073        /**
074         * Starts building transaction options with a transaction isolation level.
075         *
076         * @param isolation the desired database transaction isolation level
077         * @return a transaction options builder
078         */
079        @NonNull
080        public static Builder withIsolation(@NonNull TransactionIsolation isolation) {
081                return new Builder().isolation(isolation);
082        }
083
084        /**
085         * Starts building transaction options with a read-only setting.
086         * <p>
087         * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null}
088         * leaves the connection's read-only state unchanged.
089         *
090         * @param readOnly read-only setting to apply
091         * @return a transaction options builder
092         */
093        @NonNull
094        public static Builder withReadOnly(@Nullable Boolean readOnly) {
095                return new Builder().readOnly(readOnly);
096        }
097
098        /**
099         * Gets the transaction isolation level.
100         *
101         * @return transaction isolation level
102         */
103        @NonNull
104        public TransactionIsolation getIsolation() {
105                return this.isolation;
106        }
107
108        /**
109         * Gets the read-only setting to apply, if any.
110         * <p>
111         * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and empty leaves the
112         * connection's read-only state unchanged.
113         *
114         * @return read-only setting to apply
115         */
116        @NonNull
117        public Optional<Boolean> getReadOnly() {
118                return Optional.ofNullable(this.readOnly);
119        }
120
121        /**
122         * Gets the diagnostic observer invoked after connection cleanup and all post-transaction hooks.
123         *
124         * @return completion observer, if configured
125         * @since 4.8.0
126         */
127        @NonNull
128        public Optional<Consumer<@NonNull TransactionCompletion>> getCompletionObserver() {
129                return Optional.ofNullable(this.completionObserver);
130        }
131
132        @Override
133        public boolean equals(Object object) {
134                if (this == object)
135                        return true;
136
137                if (!(object instanceof TransactionOptions))
138                        return false;
139
140                TransactionOptions transactionOptions = (TransactionOptions) object;
141                return getIsolation() == transactionOptions.getIsolation()
142                                && Objects.equals(getReadOnly(), transactionOptions.getReadOnly())
143                                && Objects.equals(this.completionObserver, transactionOptions.completionObserver);
144        }
145
146        @Override
147        public int hashCode() {
148                return Objects.hash(getIsolation(), getReadOnly(), this.completionObserver);
149        }
150
151        @Override
152        @NonNull
153        public String toString() {
154                return String.format("%s{isolation=%s, readOnly=%s}",
155                                getClass().getSimpleName(), getIsolation().name(), getReadOnly().map(String::valueOf).orElse("unchanged"));
156        }
157
158        /**
159         * Builder used to construct {@link TransactionOptions}.
160         * <p>
161         * This class is intended for use by a single thread.
162         *
163         * @author <a href="https://www.revetkn.com">Mark Allen</a>
164         * @since 4.2.0
165         */
166        @NotThreadSafe
167        public static final class Builder {
168                @NonNull
169                private TransactionIsolation isolation;
170                @Nullable
171                private Boolean readOnly;
172                @Nullable
173                private Consumer<@NonNull TransactionCompletion> completionObserver;
174
175                private Builder() {
176                        this.isolation = TransactionIsolation.DEFAULT;
177                        this.readOnly = null;
178                }
179
180                /**
181                 * Configures the transaction isolation level.
182                 *
183                 * @param isolation the desired database transaction isolation level
184                 * @return this builder, for chaining
185                 */
186                @NonNull
187                public Builder isolation(@NonNull TransactionIsolation isolation) {
188                        requireNonNull(isolation);
189                        this.isolation = isolation;
190                        return this;
191                }
192
193                /**
194                 * Configures the transaction read-only setting.
195                 * <p>
196                 * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null}
197                 * leaves the connection's read-only state unchanged.
198                 *
199                 * @param readOnly read-only setting to apply
200                 * @return this builder, for chaining
201                 */
202                @NonNull
203                public Builder readOnly(@Nullable Boolean readOnly) {
204                        this.readOnly = readOnly;
205                        return this;
206                }
207
208                /**
209                 * Configures diagnostic observation of each transaction boundary, including every retry attempt.
210                 * <p>
211                 * The observer runs synchronously after JDBC cleanup and all post-transaction hooks, before the transaction
212                 * method returns or throws. Its failure is contained and does not change the transaction result or retry
213                 * decisions. Use {@link Transaction#addPostTransactionOperation(Consumer)} for application hooks whose failures
214                 * should propagate. Observers must be fast and thread-safe when these options are shared across threads.
215                 * Argument validation failures before a transaction scope exists are not observed.
216                 *
217                 * @param completionObserver observer, or {@code null} to remove it
218                 * @return this builder, for chaining
219                 * @since 4.8.0
220                 */
221                @NonNull
222                public Builder completionObserver(@Nullable Consumer<@NonNull TransactionCompletion> completionObserver) {
223                        this.completionObserver = completionObserver;
224                        return this;
225                }
226
227                /**
228                 * Builds transaction options.
229                 *
230                 * @return transaction options
231                 */
232                @NonNull
233                public TransactionOptions build() {
234                        return new TransactionOptions(this.isolation, this.readOnly, this.completionObserver);
235                }
236        }
237}