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}