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