Skip to content

Commit d3b13b8

Browse files
committed
fix: avoid ClassCastException in Try when an undeclared RuntimeException occurs
orThrow()/orElse()/orElseApply() used the generically inferred checked exception type as a functional-interface parameter type. Due to type erasure, an implicit checkcast to that inferred type was generated at the call site, so any undeclared unchecked exception (e.g. a RuntimeException surfacing from a jackcess bug) was reported as a misleading ClassCastException instead of the actual failure. Use Throwable for these parameters instead. Adjust call sites accordingly: where the concrete exception type can no longer be inferred from context, cast explicitly (Metadata, AbstractBaseTest, UcanaccessBaseTest) or widen the accepted function type (UcanaccessStatement.tryCatch).
1 parent 0d2d2c9 commit d3b13b8

6 files changed

Lines changed: 46 additions & 23 deletions

File tree

src/main/java/net/ucanaccess/converters/Metadata.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public List<String> getColumnNames(String tableName) throws SQLException {
282282
result.add(rs.getString(COLUMN_NAME));
283283
}
284284
return !SYSTEM_SUBQUERY.equals(tableName) ? result : null;
285-
}).orThrow(ex -> ex);
285+
}).orThrow(ex -> (SQLException) ex);
286286
}
287287

288288
public String getColumnName(String escapedTableName, String escapedColumnName) throws SQLException {
@@ -297,7 +297,7 @@ public String getColumnName(String escapedTableName, String escapedColumnName) t
297297
}
298298
}
299299
return null;
300-
}).orThrow(ex -> ex);
300+
}).orThrow(ex -> (SQLException) ex);
301301
}
302302

303303
public String getEscapedColumnName(String tableName, String columnName) throws SQLException {
@@ -306,15 +306,15 @@ public String getEscapedColumnName(String tableName, String columnName) throws S
306306
ps.setString(2, columnName);
307307
ResultSet rs = ps.executeQuery();
308308
return rs.next() ? rs.getString(ESCAPED_COLUMN_NAME) : null;
309-
}).orThrow(ex -> ex);
309+
}).orThrow(ex -> (SQLException) ex);
310310
}
311311

312312
public String getEscapedTableName(String tableName) throws SQLException {
313313
return Try.withResources(() -> conn.prepareStatement(SELECT_TABLE_ESCAPED), ps -> {
314314
ps.setString(1, tableName);
315315
ResultSet rs = ps.executeQuery();
316316
return rs.next() ? rs.getString(ESCAPED_TABLE_NAME) : null;
317-
}).orThrow(ex -> ex);
317+
}).orThrow(ex -> (SQLException) ex);
318318
}
319319

320320
public boolean isAutoIncrement(String tableName, String columnName) throws SQLException {
@@ -323,7 +323,7 @@ public boolean isAutoIncrement(String tableName, String columnName) throws SQLEx
323323
ps.setString(2, columnName);
324324
ResultSet rs = ps.executeQuery();
325325
return rs.next() && rs.getBoolean(IS_AUTOINCREMENT);
326-
}).orThrow(ex -> ex);
326+
}).orThrow(ex -> (SQLException) ex);
327327
}
328328

329329
public boolean isCurrency(String tableName, String columnName) throws SQLException {
@@ -332,30 +332,30 @@ public boolean isCurrency(String tableName, String columnName) throws SQLExcepti
332332
ps.setString(2, columnName);
333333
ResultSet rs = ps.executeQuery();
334334
return rs.next() && rs.getBoolean(IS_CURRENCY);
335-
}).orThrow(ex -> ex);
335+
}).orThrow(ex -> (SQLException) ex);
336336
}
337337

338338
public Integer getTableId(String escapedName) throws SQLException {
339339
return Try.withResources(() -> conn.prepareStatement(SELECT_TABLE_METADATA), ps -> {
340340
ps.setString(1, escapedName);
341341
ResultSet rs = ps.executeQuery();
342342
return rs.next() ? rs.getInt(TABLE_ID) : -1;
343-
}).orThrow(ex -> ex);
343+
}).orThrow(ex -> (SQLException) ex);
344344
}
345345

346346
public String getTableName(String escapedName) throws SQLException {
347347
return Try.withResources(() -> conn.prepareStatement(SELECT_TABLE_METADATA), ps -> {
348348
ps.setString(1, escapedName);
349349
ResultSet rs = ps.executeQuery();
350350
return rs.next() ? rs.getString(TABLE_NAME) : null;
351-
}).orThrow(ex -> ex);
351+
}).orThrow(ex -> (SQLException) ex);
352352
}
353353

354354
public void dropTable(String tableName) throws SQLException {
355355
Try.withResources(() -> conn.prepareStatement(DROP_TABLE), ps -> {
356356
ps.setString(1, tableName);
357357
ps.execute();
358-
}).orThrow(ex -> ex);
358+
}).orThrow(ex -> (SQLException) ex);
359359
}
360360

361361
public void columnDef(String tableName, String columnName, String def) throws SQLException {
@@ -364,15 +364,15 @@ public void columnDef(String tableName, String columnName, String def) throws SQ
364364
ps.setString(2, columnName);
365365
ps.setString(3, tableName);
366366
ps.execute();
367-
}).orThrow(ex -> ex);
367+
}).orThrow(ex -> (SQLException) ex);
368368
}
369369

370370
public void calculatedField(String tableName, String columnName) throws SQLException {
371371
Try.withResources(() -> conn.prepareStatement(UPDATE_IS_GENERATEDCOLUMN), ps -> {
372372
ps.setString(1, columnName);
373373
ps.setString(2, tableName);
374374
ps.execute();
375-
}).orThrow(ex -> ex);
375+
}).orThrow(ex -> (SQLException) ex);
376376
}
377377

378378
public void rename(String oldTableName, String newTableName, String newEscapedTableName) throws SQLException {
@@ -381,7 +381,7 @@ public void rename(String oldTableName, String newTableName, String newEscapedTa
381381
ps.setString(2, newEscapedTableName);
382382
ps.setString(3, oldTableName);
383383
ps.executeUpdate();
384-
}).orThrow(ex -> ex);
384+
}).orThrow(ex -> (SQLException) ex);
385385
}
386386

387387
}

src/main/java/net/ucanaccess/jdbc/UcanaccessStatement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ protected static final <R, T extends Throwable> R tryCatch(IThrowingSupplier<R,
394394
return tryCatch(catchable, UcanaccessSQLException::wrap);
395395
}
396396

397-
protected static final <R, T extends Throwable> R tryCatch(IThrowingSupplier<R, T> catchable, Function<T, UcanaccessSQLException> exSupplier) throws UcanaccessSQLException {
397+
protected static final <R, T extends Throwable> R tryCatch(IThrowingSupplier<R, T> catchable, Function<Throwable, UcanaccessSQLException> exSupplier) throws UcanaccessSQLException {
398398
return Try.catching(catchable).orThrow(exSupplier);
399399
}
400400

src/main/java/net/ucanaccess/util/Try.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ public V orElse(V other) {
246246
* If an exception occurs during execution of the throwing consumer, that exception is thrown.
247247
* @param consumer consumer to execute in case of exception. The exception object is passed to the consumer.
248248
*/
249-
public void orElse(IThrowingConsumer<EC, Throwable> consumer) {
249+
public void orElse(IThrowingConsumer<Throwable, Throwable> consumer) {
250250
if (hasThrown()) {
251251
catching(() -> consumer.accept(t)).orThrow();
252252
}
@@ -258,7 +258,7 @@ public void orElse(IThrowingConsumer<EC, Throwable> consumer) {
258258
* @param function function to execute in case of exception. The exception object is passed to the function.
259259
* @return the successful value or value returned by the function
260260
*/
261-
public V orElseApply(IThrowingFunction<EC, V, Throwable> function) {
261+
public V orElseApply(IThrowingFunction<Throwable, V, Throwable> function) {
262262
if (hasThrown()) {
263263
return catching(() -> function.apply(t)).orThrow();
264264
}
@@ -306,7 +306,7 @@ public V orThrow() {
306306
* @return the successful value
307307
* @throws T2 the exception returned by {@code function}
308308
*/
309-
public <T2 extends Throwable> V orThrow(Function<EC, T2> function) throws T2 {
309+
public <T2 extends Throwable> V orThrow(Function<Throwable, T2> function) throws T2 {
310310
Objects.requireNonNull(function, "Function required");
311311
if (hasThrown()) {
312312
Throwable t2 = function.apply(t);

src/test/java/net/ucanaccess/test/AbstractBaseTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ protected static File copyFile(Path source, File target) {
107107
UcanaccessRuntimeException.requireNonNull(source, "Source file required");
108108
UcanaccessRuntimeException.requireNonNull(target, "Target file required");
109109
Try.catching(() -> Files.copy(source, target.toPath(), StandardCopyOption.REPLACE_EXISTING))
110-
.orThrow(e -> new UncheckedIOException("Failed to copy '" + source + "' to '" + target + "'", e));
110+
.orThrow(e -> new UncheckedIOException("Failed to copy '" + source + "' to '" + target + "'", (IOException) e));
111111
return target;
112112
}
113113

114114
protected static File copyFile(InputStream in, File target) {
115115
UcanaccessRuntimeException.requireNonNull(in, "Input stream required");
116116
UcanaccessRuntimeException.requireNonNull(target, "Target file required");
117117
Try.catching(() -> Files.copy(in, target.toPath(), StandardCopyOption.REPLACE_EXISTING))
118-
.orThrow(e -> new UncheckedIOException("Failed to copy to '" + target + "'", e));
118+
.orThrow(e -> new UncheckedIOException("Failed to copy to '" + target + "'", (IOException) e));
119119
return target;
120120
}
121121

src/test/java/net/ucanaccess/test/UcanaccessBaseTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,10 @@ private void diffResultSets(ResultSet resultSet, ResultSet verifyResultSet, Char
284284
+ objExpected + " in [" + query + ']');
285285
} else {
286286
if (objActual instanceof Blob) {
287-
byte[] barrActual = Try.withResources(((Blob) objActual)::getBinaryStream, InputStream::readAllBytes).orThrow(UncheckedIOException::new);
288-
byte[] barrExpected = Try.withResources(((Blob) objExpected)::getBinaryStream, InputStream::readAllBytes).orThrow(UncheckedIOException::new);
287+
byte[] barrActual = Try.withResources(((Blob) objActual)::getBinaryStream, InputStream::readAllBytes)
288+
.orThrow(e -> new UncheckedIOException((IOException) e));
289+
byte[] barrExpected = Try.withResources(((Blob) objExpected)::getBinaryStream, InputStream::readAllBytes)
290+
.orThrow(e -> new UncheckedIOException((IOException) e));
289291
for (int y = 0; y < barrExpected.length; y++) {
290292
assertEquals(barrExpected[y], barrActual[y], "Byte mismatch at position " + y + " in column " + col + " in blob");
291293
}
@@ -442,14 +444,14 @@ protected File createTempFile(String prefix) {
442444
File f = createTempFileName(prefix);
443445

444446
getLogger().log(Level.DEBUG, "Creating temp file {0}", f);
445-
Try.catching(() -> Files.createFile(f.toPath())).orThrow(UncheckedIOException::new);
447+
Try.catching(() -> Files.createFile(f.toPath())).orThrow(e -> new UncheckedIOException((IOException) e));
446448

447449
f.deleteOnExit();
448450
return f;
449451
}
450452

451453
void createNewDatabase(FileFormat fileFormat, File dbFile) {
452-
Try.withResources(() -> DatabaseBuilder.create(fileFormat, dbFile), Database::flush).orThrow(UncheckedIOException::new);
454+
Try.withResources(() -> DatabaseBuilder.create(fileFormat, dbFile), Database::flush).orThrow(e -> new UncheckedIOException((IOException) e));
453455
getLogger().log(Level.INFO, "Access {0} database created: {1}", fileFormat.name(), dbFile.getAbsolutePath());
454456
}
455457

src/test/java/net/ucanaccess/util/TryTest.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ void testSupplierThrowsCheckedException() {
7171
assertEquals(-1L, tryCatch.orElseApply(t -> -1L));
7272
assertEquals(-1L, tryCatch.orElseGet(() -> -1L));
7373
assertSame(ex1, assertThrows(IOException.class, tryCatch::orThrow));
74-
UncheckedIOException unioex = assertThrows(UncheckedIOException.class, () -> tryCatch.orThrow(UncheckedIOException::new));
74+
UncheckedIOException unioex = assertThrows(UncheckedIOException.class,
75+
() -> tryCatch.orThrow(t -> new UncheckedIOException((IOException) t)));
7576
assertSame(ex1, unioex.getCause());
7677

7778
Try<Boolean, Throwable> mappedtc = tryCatch.map(size -> {
@@ -102,4 +103,24 @@ void testSupplierThrowsRuntimeException() {
102103
assertSame(exception, rtex.getCause());
103104
}
104105

106+
@Test
107+
void testSupplierThrowsUndeclaredRuntimeExceptionDoesNotThrowClassCastException() {
108+
// regression test: the code block is statically inferred to only throw IOException,
109+
// but actually throws an unrelated, undeclared RuntimeException at runtime (e.g. a bug
110+
// deeper in some library). orThrow()/orElse()/orElseApply() must not fail with a
111+
// ClassCastException while trying to treat that exception as an IOException
112+
Try<Integer, IOException> tryCatch = Try.catching(() -> {
113+
int[] tooSmall = new int[2];
114+
return tooSmall[5];
115+
});
116+
117+
assertTrue(tryCatch.hasThrown());
118+
assertInstanceOf(ArrayIndexOutOfBoundsException.class, tryCatch.getException());
119+
120+
assertEquals(-1, tryCatch.orElseApply(t -> -1));
121+
122+
RuntimeException wrapped = assertThrows(RuntimeException.class, () -> tryCatch.orThrow(RuntimeException::new));
123+
assertInstanceOf(ArrayIndexOutOfBoundsException.class, wrapped.getCause());
124+
}
125+
105126
}

0 commit comments

Comments
 (0)