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