@@ -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