001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2024 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}