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 java.sql.SQLException;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Optional;
027import java.util.stream.Collectors;
028
029import static java.lang.String.format;
030
031/**
032 * Thrown when an error occurs when interacting with a {@link Database}.
033 * <p>
034 * If the {@code cause} of this exception is a {@link SQLException}, the {@link #getErrorCode()} and {@link #getSqlState()}
035 * accessors are shorthand for retrieving the corresponding {@link SQLException} values.
036 *
037 * @author <a href="https://www.revetkn.com">Mark Allen</a>
038 * @since 1.0.0
039 */
040@NotThreadSafe
041public class DatabaseException extends RuntimeException {
042        @Nullable
043        private final Integer errorCode;
044        @Nullable
045        private final String sqlState;
046        @Nullable
047        private final String column;
048        @Nullable
049        private final String constraint;
050        @Nullable
051        private final String datatype;
052        @Nullable
053        private final String detail;
054        @Nullable
055        private final String file;
056        @Nullable
057        private final String hint;
058        @Nullable
059        private final Integer internalPosition;
060        @Nullable
061        private final String internalQuery;
062        @Nullable
063        private final Integer line;
064        @Nullable
065        private final String dbmsMessage;
066        @Nullable
067        private final Integer position;
068        @Nullable
069        private final String routine;
070        @Nullable
071        private final String schema;
072        @Nullable
073        private final String severity;
074        @Nullable
075        private final String table;
076        @Nullable
077        private final String where;
078
079        /**
080         * Creates a {@code DatabaseException} with the given {@code message}.
081         *
082         * @param message a message describing this exception
083         */
084        public DatabaseException(@Nullable String message) {
085                this(message, null);
086        }
087
088        /**
089         * Creates a {@code DatabaseException} which wraps the given {@code cause}.
090         *
091         * @param cause the cause of this exception
092         */
093        public DatabaseException(@Nullable Throwable cause) {
094                this(cause == null ? null : cause.getMessage(), cause);
095        }
096
097        /**
098         * Creates a {@code DatabaseException} which wraps the given {@code cause}.
099         *
100         * @param message a message describing this exception
101         * @param cause   the cause of this exception
102         */
103        public DatabaseException(@Nullable String message,
104                                                                                                         @Nullable Throwable cause) {
105                super(message, cause);
106
107                Integer errorCode = null;
108                String sqlState = null;
109                String column = null;
110                String constraint = null;
111                String datatype = null;
112                String detail = null;
113                String file = null;
114                String hint = null;
115                Integer internalPosition = null;
116                String internalQuery = null;
117                Integer line = null;
118                String dbmsMessage = null;
119                Integer position = null;
120                String routine = null;
121                String schema = null;
122                String severity = null;
123                String table = null;
124                String where = null;
125
126                if (cause != null) {
127                        // Special handling for Postgres
128                        if ("org.postgresql.util.PSQLException".equals(cause.getClass().getName())) {
129                                org.postgresql.util.PSQLException psqlException = (org.postgresql.util.PSQLException) cause;
130                                org.postgresql.util.ServerErrorMessage serverErrorMessage = psqlException.getServerErrorMessage();
131
132                                if (serverErrorMessage != null) {
133                                        errorCode = psqlException.getErrorCode();
134                                        column = serverErrorMessage.getColumn();
135                                        constraint = serverErrorMessage.getConstraint();
136                                        datatype = serverErrorMessage.getDatatype();
137                                        detail = serverErrorMessage.getDetail();
138                                        file = serverErrorMessage.getFile();
139                                        hint = serverErrorMessage.getHint();
140                                        internalQuery = serverErrorMessage.getInternalQuery();
141                                        dbmsMessage = serverErrorMessage.getMessage();
142                                        routine = serverErrorMessage.getRoutine();
143                                        schema = serverErrorMessage.getSchema();
144                                        severity = serverErrorMessage.getSeverity();
145                                        sqlState = serverErrorMessage.getSQLState();
146                                        table = serverErrorMessage.getTable();
147                                        where = serverErrorMessage.getWhere();
148                                        internalPosition = serverErrorMessage.getInternalPosition();
149                                        line = serverErrorMessage.getLine();
150                                        position = serverErrorMessage.getPosition();
151                                }
152                        } else if (cause instanceof SQLException) {
153                                SQLException sqlException = (SQLException) cause;
154                                errorCode = sqlException.getErrorCode();
155                                sqlState = sqlException.getSQLState();
156                        }
157                }
158
159                this.errorCode = errorCode;
160                this.sqlState = sqlState;
161                this.column = column;
162                this.constraint = constraint;
163                this.datatype = datatype;
164                this.detail = detail;
165                this.file = file;
166                this.hint = hint;
167                this.internalPosition = internalPosition;
168                this.internalQuery = internalQuery;
169                this.line = line;
170                this.dbmsMessage = dbmsMessage;
171                this.position = position;
172                this.routine = routine;
173                this.schema = schema;
174                this.severity = severity;
175                this.table = table;
176                this.where = where;
177        }
178
179        @Override
180        public String toString() {
181                List<String> components = new ArrayList<>(20);
182
183                if (getMessage() != null && getMessage().trim().length() > 0)
184                        components.add(format("message=%s", getMessage().trim()));
185
186                if (getErrorCode().isPresent())
187                        components.add(format("errorCode=%s", getErrorCode().get()));
188                if (getSqlState().isPresent())
189                        components.add(format("sqlState=%s", getSqlState().get()));
190                if (getColumn().isPresent())
191                        components.add(format("column=%s", getColumn().get()));
192                if (getConstraint().isPresent())
193                        components.add(format("constraint=%s", getConstraint().get()));
194                if (getDatatype().isPresent())
195                        components.add(format("datatype=%s", getDatatype().get()));
196                if (getDetail().isPresent())
197                        components.add(format("detail=%s", getDetail().get()));
198                if (getFile().isPresent())
199                        components.add(format("file=%s", getFile().get()));
200                if (getHint().isPresent())
201                        components.add(format("hint=%s", getHint().get()));
202                if (getInternalPosition().isPresent())
203                        components.add(format("internalPosition=%s", getInternalPosition().get()));
204                if (getInternalQuery().isPresent())
205                        components.add(format("internalQuery=%s", getInternalQuery().get()));
206                if (getLine().isPresent())
207                        components.add(format("line=%s", getLine().get()));
208                if (getDbmsMessage().isPresent())
209                        components.add(format("dbmsMessage=%s", getDbmsMessage().get()));
210                if (getPosition().isPresent())
211                        components.add(format("position=%s", getPosition().get()));
212                if (getRoutine().isPresent())
213                        components.add(format("routine=%s", getRoutine().get()));
214                if (getSchema().isPresent())
215                        components.add(format("schema=%s", getSchema().get()));
216                if (getSeverity().isPresent())
217                        components.add(format("severity=%s", getSeverity().get()));
218                if (getTable().isPresent())
219                        components.add(format("table=%s", getTable().get()));
220                if (getWhere().isPresent())
221                        components.add(format("where=%s", getWhere().get()));
222
223                return format("%s: %s", getClass().getName(), components.stream().collect(Collectors.joining(", ")));
224        }
225
226        /**
227         * Shorthand for {@link SQLException#getErrorCode()} if this exception was caused by a {@link SQLException}.
228         *
229         * @return the value of {@link SQLException#getErrorCode()}, or empty if not available
230         */
231        @NonNull
232        public Optional<Integer> getErrorCode() {
233                return Optional.ofNullable(this.errorCode);
234        }
235
236        /**
237         * Shorthand for {@link SQLException#getSQLState()} if this exception was caused by a {@link SQLException}.
238         *
239         * @return the value of {@link SQLException#getSQLState()}, or empty if not available
240         */
241        @NonNull
242        public Optional<String> getSqlState() {
243                return Optional.ofNullable(this.sqlState);
244        }
245
246        /**
247         * @return the value of the offending {@code column}, or empty if not available
248         * @since 1.0.12
249         */
250        @NonNull
251        public Optional<String> getColumn() {
252                return Optional.ofNullable(this.column);
253        }
254
255        /**
256         * @return the value of the offending {@code constraint}, or empty if not available
257         * @since 1.0.12
258         */
259        @NonNull
260        public Optional<String> getConstraint() {
261                return Optional.ofNullable(this.constraint);
262        }
263
264        /**
265         * @return the value of the offending {@code datatype}, or empty if not available
266         * @since 1.0.12
267         */
268        @NonNull
269        public Optional<String> getDatatype() {
270                return Optional.ofNullable(this.datatype);
271        }
272
273        /**
274         * @return the value of the offending {@code detail}, or empty if not available
275         * @since 1.0.12
276         */
277        @NonNull
278        public Optional<String> getDetail() {
279                return Optional.ofNullable(this.detail);
280        }
281
282        /**
283         * @return the value of the offending {@code file}, or empty if not available
284         * @since 1.0.12
285         */
286        @NonNull
287        public Optional<String> getFile() {
288                return Optional.ofNullable(this.file);
289        }
290
291        /**
292         * @return the value of the error {@code hint}, or empty if not available
293         * @since 1.0.12
294         */
295        @NonNull
296        public Optional<String> getHint() {
297                return Optional.ofNullable(this.hint);
298        }
299
300        /**
301         * @return the value of the offending {@code internalPosition}, or empty if not available
302         * @since 1.0.12
303         */
304        @NonNull
305        public Optional<Integer> getInternalPosition() {
306                return Optional.ofNullable(this.internalPosition);
307        }
308
309        /**
310         * @return the value of the offending {@code internalQuery}, or empty if not available
311         * @since 1.0.12
312         */
313        @NonNull
314        public Optional<String> getInternalQuery() {
315                return Optional.ofNullable(this.internalQuery);
316        }
317
318        /**
319         * @return the value of the offending {@code line}, or empty if not available
320         * @since 1.0.12
321         */
322        @NonNull
323        public Optional<Integer> getLine() {
324                return Optional.ofNullable(this.line);
325        }
326
327        /**
328         * @return the value of the error {@code dbmsMessage}, or empty if not available
329         * @since 1.0.12
330         */
331        @NonNull
332        public Optional<String> getDbmsMessage() {
333                return Optional.ofNullable(this.dbmsMessage);
334        }
335
336        /**
337         * @return the value of the offending {@code position}, or empty if not available
338         * @since 1.0.12
339         */
340        @NonNull
341        public Optional<Integer> getPosition() {
342                return Optional.ofNullable(this.position);
343        }
344
345        /**
346         * @return the value of the offending {@code routine}, or empty if not available
347         * @since 1.0.12
348         */
349        @NonNull
350        public Optional<String> getRoutine() {
351                return Optional.ofNullable(this.routine);
352        }
353
354        /**
355         * @return the value of the offending {@code schema}, or empty if not available
356         * @since 1.0.12
357         */
358        @NonNull
359        public Optional<String> getSchema() {
360                return Optional.ofNullable(this.schema);
361        }
362
363        /**
364         * @return the error {@code severity}, or empty if not available
365         * @since 1.0.12
366         */
367        @NonNull
368        public Optional<String> getSeverity() {
369                return Optional.ofNullable(this.severity);
370        }
371
372        /**
373         * @return the value of the offending {@code table}, or empty if not available
374         * @since 1.0.12
375         */
376        @NonNull
377        public Optional<String> getTable() {
378                return Optional.ofNullable(this.table);
379        }
380
381        /**
382         * @return the value of the offending {@code where}, or empty if not available
383         * @since 1.0.12
384         */
385        @NonNull
386        public Optional<String> getWhere() {
387                return Optional.ofNullable(this.where);
388        }
389}