diff --git a/src/uws/service/file/LocalUWSFileManager.java b/src/uws/service/file/LocalUWSFileManager.java index 080b80dcbda775e92c36b00c64849b804475ef27..de2e93685ab078e8912e55fd30b74fe6ce74e66c 100644 --- a/src/uws/service/file/LocalUWSFileManager.java +++ b/src/uws/service/file/LocalUWSFileManager.java @@ -2,20 +2,20 @@ package uws.service.file; /* * This file is part of UWSLibrary. - * + * * UWSLibrary is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * UWSLibrary is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. - * + * * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. - * + * * Copyright 2012-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ @@ -47,30 +47,32 @@ import uws.job.ErrorSummary; import uws.job.Result; import uws.job.UWSJob; import uws.job.user.JobOwner; +import uws.service.file.io.OutputStreamWithCloseAction; +import uws.service.file.io.RotateFileAction; import uws.service.log.UWSLog.LogLevel; import uws.service.request.UploadFile; /** * <p>All UWS files are stored in the local machine into the specified directory.</p> - * + * * <p> * The name of the log file, the result files and the backup files may be customized by overriding the following functions: * {@link #getLogFileName(uws.service.log.UWSLog.LogLevel, String)}, {@link #getResultFileName(Result, UWSJob)}, {@link #getBackupFileName(JobOwner)} and {@link #getBackupFileName()}. * </p> - * + * * <p> * By default, results and backups are grouped by owner/user and owners/users are grouped thanks to {@link DefaultOwnerGroupIdentifier}. * By using the appropriate constructor, you can change these default behaviors. * </p> - * + * * <p> * A log file rotation is set by default so that avoiding a too big log file after several months/years of use. * By default the rotation is done every month on the 1st at 6am. This frequency can be changed easily thanks to the function * {@link #setLogRotationFreq(String)}. * </p> - * + * * @author Grégory Mantelet (CDS;ARI) - * @version 4.2 (09/2017) + * @version 4.4 (07/2018) */ public class LocalUWSFileManager implements UWSFileManager { @@ -106,12 +108,12 @@ public class LocalUWSFileManager implements UWSFileManager { * There will be one directory for each owner ID and owner directories will be grouped * thanks to {@link DefaultOwnerGroupIdentifier}. * </p> - * + * * @param root UWS root directory. * * @throws NullPointerException If the given root directory is <i>null</i>. * @throws UWSException If the given file is not a directory or has not the READ and WRITE permissions. - * + * * @see #LocalUWSFileManager(File, boolean, boolean, OwnerGroupIdentifier) */ public LocalUWSFileManager(final File root) throws UWSException{ @@ -124,7 +126,7 @@ public class LocalUWSFileManager implements UWSFileManager { * If, according to the third parameter, the owner directories must be grouped, * the {@link DefaultOwnerGroupIdentifier} will be used. * </p> - * + * * @param root UWS root directory. * @param oneDirectoryForEachUser <i>true</i> to create one directory for each owner ID, <i>false</i> otherwise. * @param groupUserDirectories <i>true</i> to group user directories, <i>false</i> otherwise. @@ -132,7 +134,7 @@ public class LocalUWSFileManager implements UWSFileManager { * * @throws NullPointerException If the given root directory is <i>null</i>. * @throws UWSException If the given file is not a directory or has not the READ and WRITE permissions. - * + * * @see #LocalUWSFileManager(File, boolean, boolean, OwnerGroupIdentifier) */ public LocalUWSFileManager(final File root, final boolean oneDirectoryForEachUser, final boolean groupUserDirectories) throws UWSException{ @@ -141,7 +143,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Builds a {@link UWSFileManager} which manages all UWS files in the given directory. - * + * * @param root UWS root directory. * @param oneDirectoryForEachUser <i>true</i> to create one directory for each owner ID, <i>false</i> otherwise. * @param groupUserDirectories <i>true</i> to group user directories, <i>false</i> otherwise. @@ -189,7 +191,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Gets the directory of the given owner. - * + * * @param owner A job owner. * @return Its directory. */ @@ -210,9 +212,9 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Removes the owner directory if there is no more file in it (except the backup file which is no more required). - * + * * @param owner The user whose the directory must be removed. - * + * * @throws IOException If there is an error while removing the owner directory. */ protected void cleanOwnerDirectory(final JobOwner owner) throws IOException{ @@ -248,7 +250,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Get the frequency of the log file rotation * in a human readable way. - * + * * @return A human readable frequency of the log file rotation. */ public final String getLogRotationFreq(){ @@ -257,12 +259,12 @@ public class LocalUWSFileManager implements UWSFileManager { /** * <p>Set the frequency at which a rotation of the log file must be done.</p> - * + * * <p> * "rotation" means here, to close the currently used log file, to rename it so that suffixing it * with the date at which the first log has been written in it, and to create a new log file. * </p> - * + * * <p>The frequency string must respect the following syntax:</p> * <ul> * <li>'D' hh mm : daily schedule at hh:mm</li> @@ -273,12 +275,12 @@ public class LocalUWSFileManager implements UWSFileManager { * </ul> * <p><i>Where: hh = integer between 0 and 23, mm = integer between 0 and 59, dd (for 'W') = integer between 1 and 7 (1:sunday, 2:monday, ..., 7:saturday), * dd (for 'M') = integer between 1 and 31.</i></p> - * + * * <p><i><b>Warning:</b> * The frequency type is case sensitive! Then you should particularly pay attention at the case * when using the frequency types 'M' (monthly) and 'm' (every minute). * </p> - * + * * <p> * Parsing errors are not thrown but "resolved" silently. The "solution" depends of the error. * 2 cases of errors are considered: @@ -287,7 +289,7 @@ public class LocalUWSFileManager implements UWSFileManager { * <li><b>Frequency type mismatch:</b> It happens when the first character is not one of the expected (D, W, M, h, m). * That means: bad case (i.e. 'd' rather than 'D'), another character. * In this case, the frequency will be: <b>daily at 00:00</b>.</li> - * + * * <li><b>Parameter(s) missing or incorrect:</b> With the "daily" frequency ('D'), at least 2 parameters must be provided ; * 3 for "weekly" ('W') and "monthly" ('M') ; only 1 for "hourly" ('h') ; none for "every minute" ('m'). * This number of parameters is a minimum: only the n first parameters will be considered while @@ -296,7 +298,7 @@ public class LocalUWSFileManager implements UWSFileManager { * <b>all parameters will be set to their default value</b> * (which is 0 for all parameter except dd for which it is 1).</li> * </ul> - * + * * <p>Examples:</p> * <ul> * <li><i>"" or NULL</i> = every day at 00:00</li> @@ -309,7 +311,7 @@ public class LocalUWSFileManager implements UWSFileManager { * <li><i>"M 32 6 30"</i> = every month on the 1st at 00:00, because with 'M' dd must respect the rule: 1 ≤ dd ≤ 31</li> * <li><i>"M 5 6 30 12"</i> = every month on the 5th at 06:30, because at least 3 parameters are expected and so considered: "12" and eventual other parameters are ignored</li> * </ul> - * + * * @param interval Interval between two log rotations. */ public final void setLogRotationFreq(final String interval){ @@ -318,12 +320,12 @@ public class LocalUWSFileManager implements UWSFileManager { /** * <p>Gets the name of the UWS log file.</p> - * + * * <p>By default: {@link #DEFAULT_LOG_FILE_NAME}.</p> - * + * * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR, FATAL). * @param context Context of the message to log (UWS, HTTP, THREAD, JOB, ...). - * + * * @return The name of the UWS log file. */ protected String getLogFileName(final LogLevel level, final String context){ @@ -332,12 +334,12 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Gets the UWS log file. - * + * * @param level Level of the message to log (DEBUG, INFO, WARNING, ERROR, FATAL). * @param context Context of the message to log (UWS, HTTP, THREAD, JOB, ...). - * + * * @return The UWS log file. - * + * * @see #getLogFileName(uws.service.log.UWSLog.LogLevel, String) */ protected File getLogFile(final LogLevel level, final String context){ @@ -419,11 +421,11 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Create a File instance from the given upload file description. * This function is able to deal with location as URI and as file path. - * + * * @param upload Description of an uploaded file. - * + * * @return The corresponding File object. - * + * * @since 4.1 */ protected final File getFile(final UploadFile upload){ @@ -510,18 +512,21 @@ public class LocalUWSFileManager implements UWSFileManager { if (output != null){ try{ output.close(); - }catch(IOException ioe){} + }catch(IOException ioe){ + } } if (input != null){ try{ input.close(); - }catch(IOException ioe){} + }catch(IOException ioe){ + } } // In case of problem, the copy must be deleted: if (!done && copy.exists()){ try{ copy.delete(); - }catch(SecurityException ioe){} + }catch(SecurityException ioe){ + } } } } @@ -533,12 +538,12 @@ public class LocalUWSFileManager implements UWSFileManager { * <p>Gets the name of the file in which the given result is/must be written.</p> * <p>By default: jobID + "_" + resultID + "." + {@link UWSToolBox#getFileExtension(String) getFileExtension(resultMIMEType)}</p> * <p><i><u>note:</u> there is no file extension if the MIME type of the result is unknown !</i></p> - * + * * @param result The result whose the file name is asked. * @param job The job which owns the given result. - * + * * @return Name of the file corresponding to the given result. - * + * * @see UWSToolBox#getFileExtension(String) */ protected String getResultFileName(final Result result, final UWSJob job){ @@ -558,12 +563,12 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Gets the file corresponding to the given result. - * + * * @param result The result whose the file is asked. * @param job The job which owns the given result. - * + * * @return The file corresponding to the given result. - * + * * @see #getOwnerDirectory(JobOwner) * @see #getResultFileName(Result, UWSJob) */ @@ -610,10 +615,10 @@ public class LocalUWSFileManager implements UWSFileManager { /** * <p>Gets the name of the file in which the described error is/must be written.</p> * <p>By default: jobID + "_ERROR.log"</p> - * + * * @param error The description of the error whose the file name is asked. * @param job The job which owns the given error. - * + * * @return Name of the file corresponding to the described error. */ protected String getErrorFileName(final ErrorSummary error, final UWSJob job){ @@ -622,12 +627,12 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Gets the file corresponding to the described error. - * + * * @param error The error whose the file is asked. * @param job The job which owns the given error. - * + * * @return The file corresponding to the described error. - * + * * @see #getOwnerDirectory(JobOwner) * @see #getErrorFileName(ErrorSummary, UWSJob) */ @@ -674,11 +679,11 @@ public class LocalUWSFileManager implements UWSFileManager { /** * <p>Gets the name of the backup file of the given job owner (~ UWS user).</p> * <p>By default: ownerID + ".backup"</p> - * + * * @param owner The job owner whose the name of the backup file is asked. - * + * * @return The name of the backup file of the given owner. - * + * * @throws IllegalArgumentException If the given owner is <i>null</i> or an empty string. */ protected String getBackupFileName(final JobOwner owner) throws IllegalArgumentException{ @@ -708,7 +713,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * <p>Gets the name of the UWS general backup file.</p> * <p>By default: {@link #DEFAULT_BACKUP_FILE_NAME}</p> - * + * * @return The name of the UWS general backup file. */ protected String getBackupFileName(){ @@ -724,8 +729,9 @@ public class LocalUWSFileManager implements UWSFileManager { @Override public OutputStream getBackupOutput() throws IOException{ File backupFile = new File(rootDirectory, getBackupFileName()); + File tempBackupFile = new File(rootDirectory, getBackupFileName() + ".temp-" + System.currentTimeMillis()); createParentDir(backupFile); - return new FileOutputStream(backupFile); + return new OutputStreamWithCloseAction(new FileOutputStream(tempBackupFile), new RotateFileAction(tempBackupFile, backupFile)); } /* ************** */ @@ -734,9 +740,9 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Creates the parent directory(ies) if it(they) does/do not exist. - * + * * @param f The file whose the parent directory must exist after the call of this function. - * + * * @return <i>true</i> if the parent directory now exists, <i>false</i> otherwise. */ protected boolean createParentDir(final File f){ @@ -749,7 +755,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Lets iterating on all user backup files. * The {@link #next()} function creates and returns the {@link InputStream} for the next backup file. - * + * * @author Grégory Mantelet (CDS) * @version 05/2012 */ @@ -807,7 +813,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * If the file whose the input stream must be created and returned does not exist * or has not the READ permission, <i>null</i> will be returned. - * + * * @see java.util.Iterator#next() */ @Override @@ -836,7 +842,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Filter which lets returning only the directories. - * + * * @author Grégory Mantelet (CDS) * @version 05/2012 */ @@ -849,7 +855,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Filter which lets returning only the backup file(s) of the specified user/owner. - * + * * @author Gr&ecaute;gory Mantelet (CDS) * @version 05/2012 */ @@ -859,7 +865,7 @@ public class LocalUWSFileManager implements UWSFileManager { /** * Sets the ID of the user whose the backup file must be returned. * If <i>null</i>, all the found backup files will be returned EXCEPT the backup file for the whole UWS. - * + * * @param ownerID ID of the user whose the backup file must be returned. (MAY BE NULL) */ public void setOwnerID(final String ownerID){ diff --git a/src/uws/service/file/io/CloseAction.java b/src/uws/service/file/io/CloseAction.java new file mode 100644 index 0000000000000000000000000000000000000000..fca47862c6f0325d73bc7cf3a2803be4092e162f --- /dev/null +++ b/src/uws/service/file/io/CloseAction.java @@ -0,0 +1,44 @@ +package uws.service.file.io; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + */ + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Action(s) executed when the function {@link OutputStreamWithCloseAction#close() close()} + * is called on an {@link OutputStreamWithCloseAction} instance, after the + * wrapped {@link OutputStream} has been successfully closed. + * + * @author Grégory Mantelet (CDS) + * @version 4.4 (07/2018) + * @since 4.4 + */ +public interface CloseAction { + + /** + * This function is executed after the wrapped {@link OutputStream} of + * an {@link OutputStreamWithCloseAction} has been successfully closed. + * + * @throws IOException If any error prevents this {@link CloseAction} to + * run. + */ + public void run() throws IOException; +} \ No newline at end of file diff --git a/src/uws/service/file/io/OutputStreamWithCloseAction.java b/src/uws/service/file/io/OutputStreamWithCloseAction.java new file mode 100644 index 0000000000000000000000000000000000000000..b7e4a084913a35637b224392b840b1e358fea768 --- /dev/null +++ b/src/uws/service/file/io/OutputStreamWithCloseAction.java @@ -0,0 +1,102 @@ +package uws.service.file.io; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + */ + +import java.io.IOException; +import java.io.OutputStream; + +/** + * This {@link OutputStream} wraps another {@link OutputStream}. It forwards all + * write requests to this inner {@link OutputStream}. The only difference lies + * in its {@link #close()} function which runs a given {@link CloseAction} just + * after having called {@link OutputStream#close() close()} on the inner + * {@link OutputStream} successfully. + * + * @author Grégory Mantelet (CDS) + * @version 4.4 (07/2018) + * @since 4.4 + * + * @see CloseAction + */ +public class OutputStreamWithCloseAction extends OutputStream { + + /** Wrapped {@link OutputStream}. */ + private final OutputStream output; + + /** Action(s) to run after the wrapped {@link OutputStream} has been + * successfully closed. + * + * <p><i> + * It can be <code>null</code>. In such case, the wrapped {@link OutputStream} + * will be closed and nothing else will be done. + * </i></p> **/ + private final CloseAction closeAction; + + /** + * Create an {@link OutputStreamWithCloseAction} instance. + * + * @param output The {@link OutputStream} to wrap. + * <i>MANDATORY</i> + * @param action The action(s) to run after the given {@link OutputStream} + * has been successfully closed. + * <i>OPTIONAL</i> + * + * @throws NullPointerException If the given {@link OutputStream} is missing. + */ + public OutputStreamWithCloseAction(final OutputStream output, final CloseAction action) throws NullPointerException{ + if (output == null) + throw new NullPointerException("Missing OutputStream to wrap!"); + else + this.output = output; + + this.closeAction = action; + } + + @Override + public void write(final byte[] b) throws IOException{ + output.write(b); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException{ + output.write(b, off, len); + } + + @Override + public void write(final int b) throws IOException{ + output.write(b); + } + + @Override + public void flush() throws IOException{ + output.flush(); + } + + @Override + public void close() throws IOException{ + // Close the output stream: + output.close(); + + // Once close, run the close action: + if (closeAction != null) + closeAction.run(); + } + +} \ No newline at end of file diff --git a/src/uws/service/file/io/RotateFileAction.java b/src/uws/service/file/io/RotateFileAction.java new file mode 100644 index 0000000000000000000000000000000000000000..1283037831cb6f4bdb6318326b9677af53aefaec --- /dev/null +++ b/src/uws/service/file/io/RotateFileAction.java @@ -0,0 +1,90 @@ +package uws.service.file.io; + +/* + * This file is part of UWSLibrary. + * + * UWSLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * UWSLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2018 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + */ + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * This close action rename a given file into another one. The target file is + * deleted if it already exists. + * + * @author Grégory Mantelet (CDS) + * @version 4.4 (07/2018) + * @since 4.4 + */ +public class RotateFileAction implements CloseAction { + + /** File to rotate (i.e. rename). */ + private final File sourceFile; + + /** File into which {@link #sourceFile} must be renamed. */ + private final File targetFile; + + /** + * Create the rotate action. + * + * @param sourceFile File to rotate. + * @param targetFile File into which sourceFile must be renamed. + * + * @throws NullPointerException If any of the given file is missing. + * @throws IllegalArgumentException If the source file does not exist, + * or if it is not a regular file, + * or if the target file exists but is not + * a regular file. + */ + public RotateFileAction(final File sourceFile, final File targetFile) throws NullPointerException, IllegalArgumentException{ + // Ensure the source file exists and is a regular file: + if (sourceFile == null) + throw new NullPointerException("Missing source file!"); + else if (!sourceFile.exists()) + throw new IllegalArgumentException("The source file \"" + sourceFile.getAbsolutePath() + "\" does not exist!"); + else if (sourceFile.isDirectory()) + throw new IllegalArgumentException("The source file \"" + sourceFile.getAbsolutePath() + "\" is a directory instead of a regular file!"); + + // Check that if the target file exists, it is a regular file: + if (targetFile == null) + throw new NullPointerException("Missing target file!"); + else if (targetFile.exists() && targetFile.isDirectory()) + throw new IllegalArgumentException("The target file \"" + targetFile.getAbsolutePath() + "\" is a directory instead of a regular file!"); + + this.sourceFile = sourceFile; + this.targetFile = targetFile; + } + + @Override + public void run() throws IOException{ + // Delete the target file if it already exists: + try{ + Files.deleteIfExists(targetFile.toPath()); + }catch(IOException ioe){ + throw new IOException("Impossible to perform the file rotation! Cause: the former file can not be deleted.", ioe); + } + + // Finally rename the source file into the given target file: + try{ + Files.move(sourceFile.toPath(), targetFile.toPath()); + }catch(IOException ioe){ + throw new IOException("Impossible to perform the file rotation! Cause: [" + ioe.getClass() + "] " + ioe.getMessage(), ioe); + } + } + +} diff --git a/test/uws/service/file/io/TestOutputStreamWithCloseAction.java b/test/uws/service/file/io/TestOutputStreamWithCloseAction.java new file mode 100644 index 0000000000000000000000000000000000000000..ba1a1570ec275f86c3e987419481a6904829c4e0 --- /dev/null +++ b/test/uws/service/file/io/TestOutputStreamWithCloseAction.java @@ -0,0 +1,103 @@ +package uws.service.file.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TestOutputStreamWithCloseAction { + final String TMP_DIR = System.getProperty("java.io.tmpdir"); + + File srcFile; + File destFile; + + @Before + public void setUp() throws Exception{ + final String SUFFIX = "" + System.currentTimeMillis(); + srcFile = new File(TMP_DIR, "taptest_stream_src_" + SUFFIX); + destFile = new File(TMP_DIR, "taptest_stream_dest_" + SUFFIX); + } + + @After + public void tearDown() throws Exception{ + if (srcFile != null){ + srcFile.delete(); + srcFile = null; + } + if (destFile != null){ + destFile.delete(); + destFile = null; + } + } + + @Test + public void testOutputStreamWithCloseAction(){ + /* CASE: Missing OutputStream to wrap => ERROR */ + OutputStreamWithCloseAction out = null; + try{ + out = new OutputStreamWithCloseAction(null, null); + fail("This construction should have failed because no OutputStream has been provided!"); + }catch(Exception ex){ + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing OutputStream to wrap!", ex.getMessage()); + }finally{ + if (out != null){ + try{ + out.close(); + }catch(IOException ioe){ + } + } + } + } + + @Test + public void testClose(){ + OutputStreamWithCloseAction out = null; + try{ + // Open the stream toward the temp file: + out = new OutputStreamWithCloseAction(new FileOutputStream(srcFile), new RotateFileAction(srcFile, destFile)); + + out.write("File\n".getBytes()); + out.flush(); + out.write("successfully written!".getBytes()); + + // Close the stream, and so, rotate the files: + out.close(); + + // Check the files have been rotated: + assertFalse(srcFile.exists()); + assertTrue(destFile.exists()); + + // Check the content of the dest file: + try(InputStream input = new FileInputStream(destFile)){ + byte[] buffer = new byte[256]; + int nbRead = 0; + nbRead = input.read(buffer); + assertEquals(26, nbRead); + assertEquals("File\nsuccessfully written!", new String(buffer, 0, nbRead)); + } + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("Unexpected error! (see the console for more details)"); + }finally{ + if (out != null){ + try{ + out.close(); + }catch(IOException ioe){ + } + } + } + } + +} diff --git a/test/uws/service/file/io/TestRotateFileAction.java b/test/uws/service/file/io/TestRotateFileAction.java new file mode 100644 index 0000000000000000000000000000000000000000..abec8ca3bea9140c7e05d97bb801ebffc38598fb --- /dev/null +++ b/test/uws/service/file/io/TestRotateFileAction.java @@ -0,0 +1,161 @@ +package uws.service.file.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TestRotateFileAction { + + final String TMP_DIR = System.getProperty("java.io.tmpdir"); + + File srcFile; + File destFile; + + @Before + public void setUp() throws Exception{ + final String SUFFIX = "" + System.currentTimeMillis(); + srcFile = new File(TMP_DIR, "taptest_rotate_src_" + SUFFIX); + destFile = new File(TMP_DIR, "taptest_rotate_dest_" + SUFFIX); + } + + @After + public void tearDown() throws Exception{ + if (srcFile != null){ + srcFile.delete(); + srcFile = null; + } + if (destFile != null){ + destFile.delete(); + destFile = null; + } + } + + @Test + public void testConstructor(){ + /* CASE: No source file => ERROR */ + try{ + new RotateFileAction(null, null); + fail("This construction should have failed because no source file has been provided!"); + }catch(Exception ex){ + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing source file!", ex.getMessage()); + } + /* CASE: The source file does not exist => ERROR */ + try{ + assertFalse(srcFile.exists()); + new RotateFileAction(srcFile, null); + fail("This construction should have failed because the source file does not exist."); + }catch(Exception ex){ + assertEquals(IllegalArgumentException.class, ex.getClass()); + assertEquals("The source file \"" + srcFile.getAbsolutePath() + "\" does not exist!", ex.getMessage()); + } + /* CASE: The source file is a directory => ERROR */ + try{ + assertTrue(srcFile.mkdir()); + new RotateFileAction(srcFile, null); + fail("This construction should have failed because the source file is a directory!"); + }catch(Exception ex){ + assertEquals(IllegalArgumentException.class, ex.getClass()); + assertEquals("The source file \"" + srcFile.getAbsolutePath() + "\" is a directory instead of a regular file!", ex.getMessage()); + }finally{ + srcFile.delete(); + } + /* CASE: No dest file => ERROR */ + try{ + assertTrue(srcFile.createNewFile()); + new RotateFileAction(srcFile, null); + fail("This construction should have failed because the dest file is missing!"); + }catch(Exception ex){ + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing target file!", ex.getMessage()); + }finally{ + srcFile.delete(); + } + /* CASE: the dest file exists and is a directory => ERROR */ + try{ + assertTrue(srcFile.createNewFile()); + assertTrue(destFile.mkdir()); + new RotateFileAction(srcFile, destFile); + fail("This construction should have failed because the dest file is a directory!"); + }catch(Exception ex){ + assertEquals(IllegalArgumentException.class, ex.getClass()); + assertEquals("The target file \"" + destFile.getAbsolutePath() + "\" is a directory instead of a regular file!", ex.getMessage()); + }finally{ + srcFile.delete(); + destFile.delete(); + } + /* CASE: Everything is OK! */ + try{ + assertTrue(srcFile.createNewFile()); + assertFalse(destFile.exists()); + new RotateFileAction(srcFile, destFile); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("Unexpected error while construction! (see console for more details)"); + } + } + + @Test + public void testRun(){ + /* CASE: Destination does not exist yet */ + try{ + // Create a source file: + assertTrue(srcFile.createNewFile()); + + // Ensure the dest file does exist yet: + assertFalse(destFile.exists()); + + // Run the action: + CloseAction action = new RotateFileAction(srcFile, destFile); + action.run(); + + /* Check the source file does not exist anymore + * BUT the dest one yes: */ + assertFalse(srcFile.exists()); + assertTrue(destFile.exists()); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("Unexpected IO error! (see console for more details)"); + }finally{ + srcFile.delete(); + destFile.delete(); + } + + /* CASE: Destination already exists */ + try{ + // Create a source file: + assertTrue(srcFile.createNewFile()); + + // Sleep for 1 second (so that the last modified date can be different): + Thread.sleep(1000); + + // Already create the dest: + assertTrue(destFile.createNewFile()); + long lastModified = destFile.lastModified(); + + // Run the action: + CloseAction action = new RotateFileAction(srcFile, destFile); + action.run(); + + /* Check the source file does not exist anymore + * AND the destination file does still exist + * BUT with a different lastModified date: */ + assertFalse(srcFile.exists()); + assertTrue(destFile.exists()); + assertNotEquals(lastModified, destFile.lastModified()); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("Unexpected IO error! (see console for more details)"); + } + } + +}