/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.workspace.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.opencastproject.util.IoSupport.withResource;
import static org.opencastproject.util.data.functions.Misc.chuck;
import org.opencastproject.util.FileSupport;
import org.opencastproject.util.data.Effect;
import org.opencastproject.util.data.Function;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.util.UUID;
public class FileReadDeleteTest {
private static final String FILE = "/opencast_header.gif";
private static final Logger logger = LoggerFactory.getLogger(FileReadDeleteTest.class);
private final Object start = new Object();
private volatile long totalRead = 0;
private long expectedSize = 0;
@Before
public void setUp() {
totalRead = 0;
expectedSize = resourceAsFile(FILE).length();
}
@Test
public void testFileDeletionWhileReadingIo() throws Exception {
testFileDeletionWhileReading(readIo);
}
@Test
public void testFileDeletionWhileReadingNio() throws Exception {
testFileDeletionWhileReading(readNio);
}
private void testFileDeletionWhileReading(final Function<FileInputStream, Function<Long, Long>> mkReader)
throws Exception {
final File source = resourceAsFile(FILE);
final File work = FileSupport.copy(source, new File(source.getParentFile(), UUID.randomUUID().toString() + ".tmp"));
assertTrue("Work file could not be created", work.exists());
try {
final Thread readerThread = new Thread(mkRunnable(work, mkReader));
synchronized (start) {
readerThread.start();
// wait for reader
start.wait();
}
assertEquals("Reader already finished", 0, totalRead);
assertTrue("File could not be deleted", work.delete());
assertFalse("File still exists", work.exists());
logger.debug("Work file deleted");
// wait for reader to complete
readerThread.join();
assertEquals("File not completely read", expectedSize, totalRead);
} finally {
// cleanup
FileSupport.delete(work);
assertFalse(work.exists());
}
}
/** Create a runnable containing a reader created from a <code>readerMaker</code>. */
private Runnable mkRunnable(final File file, final Function<FileInputStream, Function<Long, Long>> readerMaker) {
return new Runnable() {
@Override public void run() {
try {
withResource(new FileInputStream(file), mkReaderFrom(readerMaker));
} catch (FileNotFoundException e) {
chuck(e);
}
}
};
}
/**
* Create a read effect from a <code>readerMaker</code> function.
* This read effect will be used to consume a file input stream.
*/
private Effect<FileInputStream> mkReaderFrom(final Function<FileInputStream, Function<Long, Long>> readerMaker) {
return new Effect<FileInputStream>() {
@Override public void run(final FileInputStream in) {
logger.debug("Start reading");
long total = 0L;
long read;
final Function<Long, Long> readFile = readerMaker.apply(in);
try {
while ((read = readFile.apply(total)) > 0) {
total = total + read;
logger.debug("Read " + total);
if (total > 3000) {
synchronized (start) {
start.notifyAll();
}
}
Thread.sleep(100);
}
totalRead = total;
logger.debug("File completely read " + total);
} catch (Throwable e) {
e.printStackTrace();
}
}
};
}
/** NIO based file reader. */
private Function<FileInputStream, Function<Long, Long>> readNio = new Function<FileInputStream, Function<Long, Long>>() {
@Override public Function<Long, Long> apply(final FileInputStream in) {
final FileChannel channel = in.getChannel();
final Sink sink = new Sink();
//
return new Function.X<Long, Long>() {
@Override public Long xapply(Long total) throws Exception {
return channel.transferTo(total, 1024, sink);
}
};
}
};
/** Normal IO based file reader. */
private Function<FileInputStream, Function<Long, Long>> readIo = new Function<FileInputStream, Function<Long, Long>>() {
@Override public Function<Long, Long> apply(final FileInputStream in) {
final byte[] buffer = new byte[1024];
//
return new Function.X<Long, Long>() {
@Override public Long xapply(Long total) throws Exception {
return (long) in.read(buffer);
}
};
}
};
private File resourceAsFile(String resource) {
try {
return new File(this.getClass().getResource(resource).toURI());
} catch (URISyntaxException e) {
return chuck(e);
}
}
private static class Sink implements WritableByteChannel {
private boolean closed = false;
@Override public int write(ByteBuffer byteBuffer) throws IOException {
return byteBuffer.limit();
}
@Override public boolean isOpen() {
return !closed;
}
@Override public void close() throws IOException {
closed = true;
}
}
}