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;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Options for a Pyranid-managed transaction.
031 * <p>
032 * Use this when transaction behavior differs from the defaults. For example:
033 * <pre>{@code
034 * database.transaction(
035 *   TransactionOptions.withIsolation(TransactionIsolation.REPEATABLE_READ)
036 *     .readOnly(true)
037 *     .build(),
038 *   () -> {
039 *     // transactional work
040 *   });
041 * }</pre>
042 *
043 * @author <a href="https://www.revetkn.com">Mark Allen</a>
044 * @since 4.2.0
045 */
046@ThreadSafe
047public final class TransactionOptions {
048        @NonNull
049        static final TransactionOptions DEFAULT;
050
051        static {
052                DEFAULT = new TransactionOptions(TransactionIsolation.DEFAULT, null);
053        }
054
055        @NonNull
056        private final TransactionIsolation isolation;
057        @Nullable
058        private final Boolean readOnly;
059
060        private TransactionOptions(@NonNull TransactionIsolation isolation,
061                                                                                                                 @Nullable Boolean readOnly) {
062                requireNonNull(isolation);
063
064                this.isolation = isolation;
065                this.readOnly = readOnly;
066        }
067
068        /**
069         * Starts building transaction options with a transaction isolation level.
070         *
071         * @param isolation the desired database transaction isolation level
072         * @return a transaction options builder
073         */
074        @NonNull
075        public static Builder withIsolation(@NonNull TransactionIsolation isolation) {
076                return new Builder().isolation(isolation);
077        }
078
079        /**
080         * Starts building transaction options with a read-only setting.
081         * <p>
082         * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null}
083         * leaves the connection's read-only state unchanged.
084         *
085         * @param readOnly read-only setting to apply
086         * @return a transaction options builder
087         */
088        @NonNull
089        public static Builder withReadOnly(@Nullable Boolean readOnly) {
090                return new Builder().readOnly(readOnly);
091        }
092
093        /**
094         * Gets the transaction isolation level.
095         *
096         * @return transaction isolation level
097         */
098        @NonNull
099        public TransactionIsolation getIsolation() {
100                return this.isolation;
101        }
102
103        /**
104         * Gets the read-only setting to apply, if any.
105         * <p>
106         * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and empty leaves the
107         * connection's read-only state unchanged.
108         *
109         * @return read-only setting to apply
110         */
111        @NonNull
112        public Optional<Boolean> getReadOnly() {
113                return Optional.ofNullable(this.readOnly);
114        }
115
116        @Override
117        public boolean equals(Object object) {
118                if (this == object)
119                        return true;
120
121                if (!(object instanceof TransactionOptions))
122                        return false;
123
124                TransactionOptions transactionOptions = (TransactionOptions) object;
125                return getIsolation() == transactionOptions.getIsolation()
126                                && Objects.equals(getReadOnly(), transactionOptions.getReadOnly());
127        }
128
129        @Override
130        public int hashCode() {
131                return Objects.hash(getIsolation(), getReadOnly());
132        }
133
134        @Override
135        @NonNull
136        public String toString() {
137                return String.format("%s{isolation=%s, readOnly=%s}",
138                                getClass().getSimpleName(), getIsolation().name(), getReadOnly().map(String::valueOf).orElse("unchanged"));
139        }
140
141        /**
142         * Builder used to construct {@link TransactionOptions}.
143         * <p>
144         * This class is intended for use by a single thread.
145         *
146         * @author <a href="https://www.revetkn.com">Mark Allen</a>
147         * @since 4.2.0
148         */
149        @NotThreadSafe
150        public static final class Builder {
151                @NonNull
152                private TransactionIsolation isolation;
153                @Nullable
154                private Boolean readOnly;
155
156                private Builder() {
157                        this.isolation = TransactionIsolation.DEFAULT;
158                        this.readOnly = null;
159                }
160
161                /**
162                 * Configures the transaction isolation level.
163                 *
164                 * @param isolation the desired database transaction isolation level
165                 * @return this builder, for chaining
166                 */
167                @NonNull
168                public Builder isolation(@NonNull TransactionIsolation isolation) {
169                        requireNonNull(isolation);
170                        this.isolation = isolation;
171                        return this;
172                }
173
174                /**
175                 * Configures the transaction read-only setting.
176                 * <p>
177                 * {@code true} requests a read-only transaction, {@code false} requests a read-write transaction, and {@code null}
178                 * leaves the connection's read-only state unchanged.
179                 *
180                 * @param readOnly read-only setting to apply
181                 * @return this builder, for chaining
182                 */
183                @NonNull
184                public Builder readOnly(@Nullable Boolean readOnly) {
185                        this.readOnly = readOnly;
186                        return this;
187                }
188
189                /**
190                 * Builds transaction options.
191                 *
192                 * @return transaction options
193                 */
194                @NonNull
195                public TransactionOptions build() {
196                        return new TransactionOptions(this.isolation, this.readOnly);
197                }
198        }
199}