Skip to content

Commit 503d52a

Browse files
hive-botclaude
andcommitted
fix: report local I/O failures of translation memory file helpers as DeepLException
importTranslationMemoryFromFilepath() and exportTranslationMemoryToFilepath() wrap local I/O failures in DeepLException with the original as cause; a missing input file still throws IllegalArgumentException. The export to a File checks that the output path is free and its directory exists before the export job is created. waitUntilTranslationMemoryJobDone() cuts the last poll wait short at the timeout instead of overrunning it by up to 5 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 963240a commit 503d52a

4 files changed

Lines changed: 209 additions & 41 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
262262
(`Character.isWhitespace()`) like spoken terms, checks target terms set
263263
through `replaceAll()` or `Map.Entry.setValue()`, and names an invalid
264264
character in hexadecimal (`U+001F`).
265+
- `importTranslationMemoryFromFilepath()` and
266+
`exportTranslationMemoryToFilepath()` now throw `DeepLException`, with the
267+
original exception as cause, for local I/O failures such as an unreadable
268+
input file or a failed write, instead of a raw `IOException`.
269+
- `exportTranslationMemoryToFilepath()` with a `File` now checks that the output
270+
path is free and its directory exists before creating the export job, and
271+
throws `DeepLException` otherwise.
272+
- `waitUntilTranslationMemoryJobDone()` with a timeout, and the import and
273+
export helpers that use it, now cut the last poll wait short at the timeout
274+
instead of overrunning it by up to 5 seconds.
265275
- Translation memory and translation memory job IDs are now URL-encoded in
266276
request paths.
267277
- The style rule and custom instruction methods now URL-encode the style rule

‎README.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,8 +1326,12 @@ class Example { // Continuing class Example from above
13261326
```
13271327

13281328
The optional timeout is the maximum time to wait for the import to finish; omit
1329-
it to wait indefinitely. The job status is polled every 5 seconds, so the
1330-
timeout is not accurate to the millisecond.
1329+
it to wait indefinitely. The job status is polled every 5 seconds, and the last
1330+
wait is cut short at the timeout.
1331+
1332+
A missing input file throws `IllegalArgumentException` before any request is
1333+
sent. Any other local I/O failure, such as an unreadable file, throws
1334+
`DeepLException` with the I/O error as its cause.
13311335

13321336
`ImportTranslationMemoryOptions` groups the import options: `setDisplayName()`,
13331337
`setContentType()` (declared when the job is created and sent with the upload;
@@ -1409,8 +1413,11 @@ class Example { // Continuing class Example from above
14091413
}
14101414
```
14111415

1412-
As with import, the timeout is optional; omit it to wait indefinitely. Instead of
1413-
a `File`, you can pass an `OutputStream`, which is not closed:
1416+
As with import, the timeout is optional; omit it to wait indefinitely. The output
1417+
path is checked before the export job is created: if a file already exists there
1418+
or its directory does not exist, `DeepLException` is thrown. A failure while
1419+
writing the file also throws `DeepLException`, with the I/O error as its cause.
1420+
Instead of a `File`, you can pass an `OutputStream`, which is not closed:
14141421

14151422
```java
14161423
class Example { // Continuing class Example from above

‎deepl-java/src/main/java/com/deepl/api/DeepLClient.java‎

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,8 +2458,7 @@ public TranslationMemoryJob waitUntilTranslationMemoryJobDone(String jobId)
24582458
*
24592459
* @param jobId ID of the job to wait for.
24602460
* @param timeout Maximum time to wait before throwing, or <code>null</code> to wait indefinitely.
2461-
* Note that this is not accurate to the millisecond, as the job status is only polled every 5
2462-
* seconds.
2461+
* The job status is polled every 5 seconds; the last wait is cut short at the timeout.
24632462
* @return {@link TranslationMemoryJob} object with the status of the finished job.
24642463
* @throws InterruptedException If the thread is interrupted during execution of this function.
24652464
* @throws DeepLException If the timeout is exceeded, the job failed or expired, or any error
@@ -2468,20 +2467,34 @@ public TranslationMemoryJob waitUntilTranslationMemoryJobDone(String jobId)
24682467
*/
24692468
public TranslationMemoryJob waitUntilTranslationMemoryJobDone(
24702469
String jobId, @Nullable Duration timeout) throws DeepLException, InterruptedException {
2471-
long startTimeMillis = System.currentTimeMillis();
2470+
Long timeoutNanos = null;
2471+
if (timeout != null) {
2472+
try {
2473+
timeoutNanos = timeout.toNanos();
2474+
} catch (ArithmeticException exception) {
2475+
// Longer than about 292 years: treated as no limit.
2476+
}
2477+
}
2478+
long deadline = timeoutNanos != null ? System.nanoTime() + timeoutNanos : 0;
24722479
TranslationMemoryJob job = getTranslationMemoryJob(jobId);
24732480
while (!job.done()) {
24742481
// The API always returns exactly one result; an empty list would never reach a terminal
24752482
// status and the no-timeout overload would poll forever.
24762483
if (job.getResults().isEmpty()) {
24772484
throw new DeepLException("Translation memory job " + jobId + " returned no results");
24782485
}
2479-
if (timeout != null && System.currentTimeMillis() - startTimeMillis > timeout.toMillis()) {
2480-
throw new DeepLException(
2481-
String.format(
2482-
"Manual timeout of %ds exceeded for translation memory job", timeout.getSeconds()));
2486+
long sleepMillis = TRANSLATION_MEMORY_JOB_POLL_INTERVAL_MILLIS;
2487+
if (timeoutNanos != null) {
2488+
long remainingMillis = (deadline - System.nanoTime()) / 1_000_000;
2489+
if (remainingMillis <= 0) {
2490+
throw new DeepLException(
2491+
String.format(
2492+
"Manual timeout of %ds exceeded for translation memory job",
2493+
timeout.getSeconds()));
2494+
}
2495+
sleepMillis = Math.min(sleepMillis, remainingMillis);
24832496
}
2484-
Thread.sleep(TRANSLATION_MEMORY_JOB_POLL_INTERVAL_MILLIS);
2497+
Thread.sleep(sleepMillis);
24852498
job = getTranslationMemoryJob(jobId);
24862499
}
24872500
if (!job.ok()) {
@@ -2592,10 +2605,12 @@ public void downloadTranslationMemoryExport(TranslationMemoryJob job, OutputStre
25922605
* name.
25932606
* @return {@link TranslationMemoryJob} object for the completed import; its result carries the ID
25942607
* of the new translation memory.
2595-
* @throws IOException If the input file cannot be read.
2608+
* @throws IllegalArgumentException If the input file does not exist.
2609+
* @throws IOException Not thrown; local I/O failures are reported as {@link DeepLException}.
2610+
* Declared for source compatibility.
25962611
* @throws InterruptedException If the thread is interrupted during execution of this function.
2597-
* @throws DeepLException If the import fails, or any error occurs while communicating with the
2598-
* DeepL API.
2612+
* @throws DeepLException If the input file cannot be read (with the I/O error as cause), the
2613+
* import fails, or any error occurs while communicating with the DeepL API.
25992614
*/
26002615
public TranslationMemoryJob importTranslationMemoryFromFilepath(
26012616
File inputFile, @Nullable String displayName)
@@ -2611,23 +2626,23 @@ public TranslationMemoryJob importTranslationMemoryFromFilepath(
26112626
* @param displayName Optional name for the resulting translation memory, defaults to the file
26122627
* name.
26132628
* @param timeout Maximum time to wait for the import to finish, or <code>null</code> to wait
2614-
* indefinitely. Note that this is not accurate to the millisecond, as the job status is only
2615-
* polled every 5 seconds.
2629+
* indefinitely. The job status is polled every 5 seconds; the last wait is cut short at the
2630+
* timeout.
26162631
* @return {@link TranslationMemoryJob} object for the completed import; its result carries the ID
26172632
* of the new translation memory.
2618-
* @throws IOException If the input file cannot be read.
2633+
* @throws IllegalArgumentException If the input file does not exist.
2634+
* @throws IOException Not thrown; local I/O failures are reported as {@link DeepLException}.
2635+
* Declared for source compatibility.
26192636
* @throws InterruptedException If the thread is interrupted during execution of this function.
2620-
* @throws DeepLException If the timeout is exceeded, the import fails, or any error occurs while
2621-
* communicating with the DeepL API.
2637+
* @throws DeepLException If the input file cannot be read (with the I/O error as cause), the
2638+
* timeout is exceeded, the import fails, or any error occurs while communicating with the
2639+
* DeepL API.
26222640
* @see DeepLClient#importTranslationMemoryFromFilepath(File, String)
26232641
*/
26242642
public TranslationMemoryJob importTranslationMemoryFromFilepath(
26252643
File inputFile, @Nullable String displayName, @Nullable Duration timeout)
26262644
throws DeepLException, IOException, InterruptedException {
2627-
if (inputFile == null || !inputFile.exists()) {
2628-
throw new IllegalArgumentException("inputFile must be an existing file");
2629-
}
2630-
byte[] fileContent = Files.readAllBytes(inputFile.toPath());
2645+
byte[] fileContent = readTranslationMemoryFile(inputFile);
26312646
TranslationMemoryImport translationMemoryImport =
26322647
createTranslationMemoryImport(inputFile.getName(), fileContent.length, null, displayName);
26332648
uploadTranslationMemoryFile(translationMemoryImport, fileContent);
@@ -2643,18 +2658,18 @@ public TranslationMemoryJob importTranslationMemoryFromFilepath(
26432658
* parameters), or <code>null</code>.
26442659
* @return {@link TranslationMemoryJob} object for the completed import; its result carries the ID
26452660
* of the new translation memory.
2646-
* @throws IOException If the input file cannot be read.
2661+
* @throws IllegalArgumentException If the input file does not exist.
2662+
* @throws IOException Not thrown; local I/O failures are reported as {@link DeepLException}.
2663+
* Declared for source compatibility.
26472664
* @throws InterruptedException If the thread is interrupted during execution of this function.
2648-
* @throws DeepLException If the timeout is exceeded, the import fails, or any error occurs while
2649-
* communicating with the DeepL API.
2665+
* @throws DeepLException If the input file cannot be read (with the I/O error as cause), the
2666+
* timeout is exceeded, the import fails, or any error occurs while communicating with the
2667+
* DeepL API.
26502668
*/
26512669
public TranslationMemoryJob importTranslationMemoryFromFilepath(
26522670
File inputFile, @Nullable ImportTranslationMemoryOptions options)
26532671
throws DeepLException, IOException, InterruptedException {
2654-
if (inputFile == null || !inputFile.exists()) {
2655-
throw new IllegalArgumentException("inputFile must be an existing file");
2656-
}
2657-
byte[] fileContent = Files.readAllBytes(inputFile.toPath());
2672+
byte[] fileContent = readTranslationMemoryFile(inputFile);
26582673
return importTranslationMemoryFromFilepath(fileContent, inputFile.getName(), options);
26592674
}
26602675

@@ -2722,10 +2737,12 @@ public TranslationMemoryJob importTranslationMemoryFromFilepath(File inputFile)
27222737
* @param translationMemoryId ID of the translation memory to export.
27232738
* @param outputFile File to write the exported translation memory to.
27242739
* @return {@link TranslationMemoryJob} object for the completed export.
2725-
* @throws IOException If the output path is occupied.
2740+
* @throws IOException Not thrown; local I/O failures are reported as {@link DeepLException}.
2741+
* Declared for source compatibility.
27262742
* @throws InterruptedException If the thread is interrupted during execution of this function.
2727-
* @throws DeepLException If the export fails, or any error occurs while communicating with the
2728-
* DeepL API.
2743+
* @throws DeepLException If the output path is occupied or its directory does not exist (checked
2744+
* before the export job is created), writing the file fails (with the I/O error as cause),
2745+
* the export fails, or any error occurs while communicating with the DeepL API.
27292746
*/
27302747
public TranslationMemoryJob exportTranslationMemoryToFilepath(
27312748
String translationMemoryId, File outputFile)
@@ -2740,23 +2757,31 @@ public TranslationMemoryJob exportTranslationMemoryToFilepath(
27402757
* @param translationMemoryId ID of the translation memory to export.
27412758
* @param outputFile File to write the exported translation memory to.
27422759
* @param timeout Maximum time to wait for the export to finish, or <code>null</code> to wait
2743-
* indefinitely. Note that this is not accurate to the millisecond, as the job status is only
2744-
* polled every 5 seconds.
2760+
* indefinitely. The job status is polled every 5 seconds; the last wait is cut short at the
2761+
* timeout.
27452762
* @return {@link TranslationMemoryJob} object for the completed export.
2746-
* @throws IOException If the output path is occupied.
2763+
* @throws IOException Not thrown; local I/O failures are reported as {@link DeepLException}.
2764+
* Declared for source compatibility.
27472765
* @throws InterruptedException If the thread is interrupted during execution of this function.
2748-
* @throws DeepLException If the timeout is exceeded, the export fails, or any error occurs while
2749-
* communicating with the DeepL API.
2766+
* @throws DeepLException If the output path is occupied or its directory does not exist (checked
2767+
* before the export job is created), writing the file fails (with the I/O error as cause),
2768+
* the timeout is exceeded, the export fails, or any error occurs while communicating with the
2769+
* DeepL API.
27502770
* @see DeepLClient#exportTranslationMemoryToFilepath(String, File)
27512771
*/
27522772
public TranslationMemoryJob exportTranslationMemoryToFilepath(
27532773
String translationMemoryId, File outputFile, @Nullable Duration timeout)
27542774
throws DeepLException, IOException, InterruptedException {
2775+
checkExportOutputFile(outputFile);
27552776
TranslationMemoryExport translationMemoryExport =
27562777
createTranslationMemoryExport(translationMemoryId);
27572778
TranslationMemoryJob job =
27582779
waitUntilTranslationMemoryJobDone(translationMemoryExport.getJobId(), timeout);
2759-
downloadTranslationMemoryExport(job, outputFile);
2780+
try {
2781+
downloadTranslationMemoryExport(job, outputFile);
2782+
} catch (IOException exception) {
2783+
throw wrapLocalIOException("Failed to write translation memory export", exception);
2784+
}
27602785
return job;
27612786
}
27622787

@@ -2806,7 +2831,11 @@ public TranslationMemoryJob exportTranslationMemoryToFilepath(
28062831
createTranslationMemoryExport(translationMemoryId);
28072832
TranslationMemoryJob job =
28082833
waitUntilTranslationMemoryJobDone(translationMemoryExport.getJobId(), timeout);
2809-
downloadTranslationMemoryExport(job, outputStream);
2834+
try {
2835+
downloadTranslationMemoryExport(job, outputStream);
2836+
} catch (IOException exception) {
2837+
throw wrapLocalIOException("Failed to write translation memory export", exception);
2838+
}
28102839
return job;
28112840
}
28122841

@@ -2890,6 +2919,53 @@ private static String translationMemoryJobId(TranslationMemoryJob job)
28902919
return job.getJobId();
28912920
}
28922921

2922+
/**
2923+
* Reads a TMX file for import; a missing file is an argument error, raised before any request.
2924+
*/
2925+
private static byte[] readTranslationMemoryFile(@Nullable File inputFile)
2926+
throws DeepLException, InterruptedException {
2927+
if (inputFile == null || !inputFile.exists()) {
2928+
throw new IllegalArgumentException("inputFile must be an existing file");
2929+
}
2930+
try {
2931+
return Files.readAllBytes(inputFile.toPath());
2932+
} catch (IOException exception) {
2933+
throw wrapLocalIOException("Failed to read translation memory file", exception);
2934+
}
2935+
}
2936+
2937+
/** Checks the output file of an export so a bad path fails before the export job is created. */
2938+
private static void checkExportOutputFile(@Nullable File outputFile) throws DeepLException {
2939+
if (outputFile == null) {
2940+
throw new IllegalArgumentException("outputFile must not be null");
2941+
}
2942+
if (outputFile.exists()) {
2943+
throw new DeepLException("File already exists at output path");
2944+
}
2945+
File directory = outputFile.getAbsoluteFile().getParentFile();
2946+
if (directory == null || !directory.isDirectory()) {
2947+
throw new DeepLException("Directory of output path does not exist");
2948+
}
2949+
}
2950+
2951+
/**
2952+
* Wraps a local I/O failure in a {@link DeepLException}; an interrupted I/O operation is reported
2953+
* as {@link InterruptedException} instead, as caller cancellation is never wrapped.
2954+
*/
2955+
private static DeepLException wrapLocalIOException(String message, IOException exception)
2956+
throws InterruptedException {
2957+
boolean interrupted =
2958+
exception instanceof java.nio.channels.ClosedByInterruptException
2959+
|| (exception instanceof InterruptedIOException
2960+
&& !(exception instanceof java.net.SocketTimeoutException));
2961+
if (interrupted) {
2962+
InterruptedException interruptedException = new InterruptedException(exception.getMessage());
2963+
interruptedException.initCause(exception);
2964+
throw interruptedException;
2965+
}
2966+
return new DeepLException(message + ": " + exception.getMessage(), exception);
2967+
}
2968+
28932969
/** Extracts the ID of the given Spoken Terms collection. */
28942970
private static String spokenTermsId(SpokenTerms spokenTerms) throws IllegalArgumentException {
28952971
if (spokenTerms == null) {

0 commit comments

Comments
 (0)