Bug 58480: Work around problem where on Windows systems a Mapped Buffer can still lock a file even if the Channel was closed properly. Use reflection as DirectBuffer is in package sun.com and thus likely to go away with Java 9.

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1708609 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Dominik Stadler 2015-10-14 14:53:41 +00:00
parent d15ef65b8f
commit a00240f212
4 changed files with 272 additions and 68 deletions

View File

@ -1372,15 +1372,14 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss
public void write(OutputStream stream)
throws IOException
{
byte[] bytes = getBytes();
NPOIFSFileSystem fs = new NPOIFSFileSystem();
try {
// For tracking what we've written out, used if we're
// going to be preserving nodes
List<String> excepts = new ArrayList<String>(1);
// Write out the Workbook stream
fs.createDocument(new ByteArrayInputStream(bytes), "Workbook");
fs.createDocument(new ByteArrayInputStream(getBytes()), "Workbook");
// Write out our HPFS properties, if we have them
writeProperties(fs, excepts);
@ -1402,6 +1401,9 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss
fs.getRoot().setStorageClsid(this.directory.getStorageClsid());
}
fs.writeFilesystem(stream);
} finally {
fs.close();
}
}
/**

View File

@ -22,10 +22,14 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.util.IOUtils;
@ -38,6 +42,14 @@ public class FileBackedDataSource extends DataSource {
// remember file base, which needs to be closed too
private RandomAccessFile srcFile;
// Buffers which map to a file-portion are not closed automatically when the Channel is closed
// therefore we need to keep the list of mapped buffers and do some ugly reflection to try to
// clean the buffer during close().
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=58480,
// http://stackoverflow.com/questions/3602783/file-access-synchronized-on-java-object and
// http://bugs.java.com/view_bug.do?bug_id=4724038 for related discussions
private List<ByteBuffer> buffersToClean = new ArrayList<ByteBuffer>();
public FileBackedDataSource(File file) throws FileNotFoundException {
this(newSrcFile(file, "r"), true);
}
@ -91,6 +103,9 @@ public class FileBackedDataSource extends DataSource {
// Ready it for reading
dst.position(0);
// remember the buffer for cleanup if necessary
buffersToClean.add(dst);
// All done
return dst;
}
@ -115,6 +130,13 @@ public class FileBackedDataSource extends DataSource {
@Override
public void close() throws IOException {
// also ensure that all buffers are unmapped so we do not keep files locked on Windows
// We consider it a bug if a Buffer is still in use now!
for(ByteBuffer buffer : buffersToClean) {
unmap(buffer);
}
buffersToClean.clear();
if (srcFile != null) {
// see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4796385
srcFile.close();
@ -129,4 +151,30 @@ public class FileBackedDataSource extends DataSource {
}
return new RandomAccessFile(file, mode);
}
// need to use reflection to avoid depending on the sun.nio internal API
// unfortunately this might break silently with newer/other Java implementations,
// but we at least have unit-tests which will indicate this when run on Windows
private static void unmap(ByteBuffer bb) {
Class<?> fcClass = bb.getClass();
try {
// invoke bb.cleaner().clean(), but do not depend on sun.nio
// interfaces
Method cleanerMethod = fcClass.getDeclaredMethod("cleaner");
cleanerMethod.setAccessible(true);
Object cleaner = cleanerMethod.invoke(bb);
Method cleanMethod = cleaner.getClass().getDeclaredMethod("clean");
cleanMethod.invoke(cleaner);
} catch (NoSuchMethodException e) {
// e.printStackTrace();
} catch (SecurityException e) {
// e.printStackTrace();
} catch (IllegalAccessException e) {
// e.printStackTrace();
} catch (IllegalArgumentException e) {
// e.printStackTrace();
} catch (InvocationTargetException e) {
// e.printStackTrace();
}
}
}

View File

@ -56,6 +56,9 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.formula.ptg.Area3DPtg;
import org.apache.poi.ss.usermodel.BaseTestWorkbook;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.TempFile;
@ -1194,4 +1197,50 @@ public final class TestHSSFWorkbook extends BaseTestWorkbook {
assertTrue("Should find some images via Client or Child anchors, but did not find any at all", found);
}
@Test
public void testRewriteFileBug58480() throws Exception {
final File file = new File(
"build/HSSFWorkbookTest-testWriteScenario.xls");
// create new workbook
{
final Workbook workbook = new HSSFWorkbook();
final Sheet sheet = workbook.createSheet("foo");
final Row row = sheet.createRow(1);
row.createCell(1).setCellValue("bar");
writeAndCloseWorkbook(workbook, file);
}
// edit the workbook
{
NPOIFSFileSystem fs = new NPOIFSFileSystem(file, false);
try {
DirectoryNode root = fs.getRoot();
final Workbook workbook = new HSSFWorkbook(root, true);
final Sheet sheet = workbook.getSheet("foo");
sheet.getRow(1).createCell(2).setCellValue("baz");
writeAndCloseWorkbook(workbook, file);
} finally {
fs.close();
}
}
}
private void writeAndCloseWorkbook(Workbook workbook, File file)
throws IOException, InterruptedException {
final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
workbook.write(bytesOut);
workbook.close();
final byte[] byteArray = bytesOut.toByteArray();
bytesOut.close();
final FileOutputStream fileOut = new FileOutputStream(file);
fileOut.write(byteArray);
fileOut.close();
}
}

View File

@ -20,10 +20,17 @@
package org.apache.poi.poifs.nio;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import org.apache.poi.POIDataSamples;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.TempFile;
import junit.framework.TestCase;
@ -39,7 +46,105 @@ public class TestDataSource extends TestCase
FileBackedDataSource ds = new FileBackedDataSource(f);
try {
checkDataSource(ds, false);
} finally {
ds.close();
}
// try a second time
ds = new FileBackedDataSource(f);
try {
checkDataSource(ds, false);
} finally {
ds.close();
}
}
public void testFileWritable() throws Exception {
File temp = TempFile.createTempFile("TestDataSource", ".test");
try {
writeDataToFile(temp);
FileBackedDataSource ds = new FileBackedDataSource(temp, false);
try {
checkDataSource(ds, true);
} finally {
ds.close();
}
// try a second time
ds = new FileBackedDataSource(temp, false);
try {
checkDataSource(ds, true);
} finally {
ds.close();
}
writeDataToFile(temp);
} finally {
assertTrue(temp.exists());
assertTrue("Could not delete file " + temp, temp.delete());
}
}
public void testRewritableFile() throws Exception {
File temp = TempFile.createTempFile("TestDataSource", ".test");
try {
writeDataToFile(temp);
FileBackedDataSource ds = new FileBackedDataSource(temp, true);
try {
ByteBuffer buf = ds.read(0, 10);
assertNotNull(buf);
buf = ds.read(8, 0x400);
assertNotNull(buf);
} finally {
ds.close();
}
// try a second time
ds = new FileBackedDataSource(temp, true);
try {
ByteBuffer buf = ds.read(0, 10);
assertNotNull(buf);
buf = ds.read(8, 0x400);
assertNotNull(buf);
} finally {
ds.close();
}
writeDataToFile(temp);
} finally {
assertTrue(temp.exists());
assertTrue(temp.delete());
}
}
private void writeDataToFile(File temp) throws FileNotFoundException, IOException {
OutputStream str = new FileOutputStream(temp);
try {
InputStream in = data.openResourceAsStream("Notes.ole2");
try {
IOUtils.copy(in, str);
} finally {
in.close();
}
} finally {
str.close();
}
}
private void checkDataSource(FileBackedDataSource ds, boolean writeable) throws IOException {
assertEquals(writeable, ds.isWriteable());
assertNotNull(ds.getChannel());
// rewriting changes the size
if(writeable) {
assertTrue("Had: " + ds.size(), ds.size() == 8192 || ds.size() == 8198);
} else {
assertEquals(8192, ds.size());
}
// Start of file
ByteBuffer bs;
@ -75,10 +180,10 @@ public class TestDataSource extends TestCase
// Can't go off the end
try {
bs = ds.read(4, 8192);
if(!writeable) {
fail("Shouldn't be able to read off the end of the file");
} catch(IllegalArgumentException e) {}
} finally {
ds.close();
}
} catch (IllegalArgumentException e) {
}
}