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.time.ZoneId;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Objects;
030import java.util.Optional;
031import java.util.Queue;
032import java.util.concurrent.ConcurrentLinkedQueue;
033import java.util.function.Supplier;
034import java.util.stream.Collectors;
035
036import static java.lang.String.format;
037import static java.util.Objects.requireNonNull;
038
039/**
040 * Data that represents a SQL statement.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 * @since 2.0.0
044 */
045@ThreadSafe
046public final class StatementContext<T> {
047        @NonNull
048        private final Statement statement;
049        @NonNull
050        private final List<@Nullable Object> parameters;
051        @Nullable
052        private final Class<T> resultSetRowType;
053        @NonNull
054        private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
055        @NonNull
056        private final ZoneId timeZone;
057        @NonNull
058        private final Queue<@NonNull AutoCloseable> cleanupOperations;
059
060        protected StatementContext(@NonNull Builder builder) {
061                requireNonNull(builder);
062
063                this.statement = builder.statement;
064                this.parameters = builder.parameters == null
065                                ? List.of()
066                                : Collections.unmodifiableList(new ArrayList<>(builder.parameters));
067                this.resultSetRowType = builder.resultSetRowType;
068                this.databaseTypeSupplier = builder.databaseTypeSupplier;
069                this.timeZone = builder.timeZone;
070                this.cleanupOperations = new ConcurrentLinkedQueue<>();
071        }
072
073        @Override
074        public int hashCode() {
075                return Objects.hash(getStatement(), getParameters(), getResultSetRowType(), getDatabaseType(), getTimeZone());
076        }
077
078        @Override
079        public boolean equals(Object object) {
080                if (this == object)
081                        return true;
082
083                if (!(object instanceof StatementContext))
084                        return false;
085
086                StatementContext statementContext = (StatementContext) object;
087
088                return Objects.equals(statementContext.getStatement(), getStatement())
089                                && Objects.equals(statementContext.getParameters(), getParameters())
090                                && Objects.equals(statementContext.getResultSetRowType(), getResultSetRowType())
091                                && Objects.equals(statementContext.getDatabaseType(), getDatabaseType())
092                                && Objects.equals(statementContext.getTimeZone(), getTimeZone());
093        }
094
095        @Override
096        public String toString() {
097                List<String> components = new ArrayList<>(3);
098
099                components.add(format("statement=%s", getStatement()));
100
101                if (getParameters().size() > 0)
102                        components.add(format("parameters=%s", getParameters()));
103
104                Class<T> resultSetRowType = getResultSetRowType().orElse(null);
105
106                if (resultSetRowType != null)
107                        components.add(format("resultSetRowType=%s", resultSetRowType));
108
109                components.add(format("databaseType=%s", getDatabaseType().name()));
110                components.add(format("timeZone=%s", getTimeZone().getId()));
111
112                return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", ")));
113        }
114
115        @NonNull
116        public Statement getStatement() {
117                return this.statement;
118        }
119
120        @NonNull
121        public List<@Nullable Object> getParameters() {
122                return this.parameters;
123        }
124
125        @NonNull
126        public Optional<Class<T>> getResultSetRowType() {
127                return Optional.ofNullable(this.resultSetRowType);
128        }
129
130        @NonNull
131        public DatabaseType getDatabaseType() {
132                return this.databaseTypeSupplier.get();
133        }
134
135        @NonNull
136        public ZoneId getTimeZone() {
137                return this.timeZone;
138        }
139
140        void addCleanupOperation(@NonNull AutoCloseable cleanupOperation) {
141                requireNonNull(cleanupOperation);
142                this.cleanupOperations.add(cleanupOperation);
143        }
144
145        @NonNull
146        Queue<@NonNull AutoCloseable> getCleanupOperations() {
147                return this.cleanupOperations;
148        }
149
150        @NonNull
151        public static <T> Builder<T> with(@NonNull Statement statement,
152                                                                                                                                                @NonNull Database database) {
153                requireNonNull(statement);
154                requireNonNull(database);
155
156                return new Builder<>(statement, database);
157        }
158
159        /**
160         * Builder used to construct instances of {@link StatementContext}.
161         * <p>
162         * This class is intended for use by a single thread.
163         *
164         * @author <a href="https://www.revetkn.com">Mark Allen</a>
165         * @since 2.0.0
166         */
167        @NotThreadSafe
168        public static class Builder<T> {
169                @NonNull
170                private final Statement statement;
171                @NonNull
172                private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
173                @NonNull
174                private final ZoneId timeZone;
175                @Nullable
176                private List<@Nullable Object> parameters;
177                @Nullable
178                private Class<T> resultSetRowType;
179
180                private Builder(@NonNull Statement statement,
181                                                                                @NonNull Database database) {
182                        requireNonNull(statement);
183                        requireNonNull(database);
184
185                        this.statement = statement;
186                        this.databaseTypeSupplier = database::getDatabaseType;
187                        this.timeZone = database.getTimeZone();
188                }
189
190                @NonNull
191                public Builder parameters(@Nullable List<@Nullable Object> parameters) {
192                        this.parameters = parameters;
193                        return this;
194                }
195
196                @NonNull
197                public Builder parameters(Object @Nullable ... parameters) {
198                        this.parameters = parameters == null ? null : Arrays.asList(parameters);
199                        return this;
200                }
201
202                @NonNull
203                public Builder resultSetRowType(Class<T> resultSetRowType) {
204                        this.resultSetRowType = resultSetRowType;
205                        return this;
206                }
207
208                @NonNull
209                public StatementContext build() {
210                        return new StatementContext<>(this);
211                }
212        }
213}