/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2010, Geomatys
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotoolkit.data.shapefile.shx;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.logging.Level;
import org.geotoolkit.data.shapefile.shp.ShapefileHeader;
import static org.geotoolkit.data.shapefile.ShapefileFeatureStoreFactory.*;
import org.geotoolkit.data.dbf.Closeable;
/**
* ShxReader parser for .shx files.<br>
*
* For details on the index file, see <br>
* <a href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf"><b>"ESRI(r)
* Shapefile - A Technical Description"</b><br> * <i>'An ESRI White Paper .
* May 1997'</i></a>
*
* @author Ian Schneider
* @author Johann Sorel (Geomatys)
* @module
*/
public final class ShxReader implements Closeable{
private static final int RECS_IN_BUFFER = 2000;
/**
* Stores the creation stack trace if assertion are enable.
*/
protected Throwable creationStack;
private final FileChannel channel;
private final ByteBuffer buffer;
private final boolean useMemoryMappedBuffer;
private final ShapefileHeader header;
private int channelOffset;
private int lastIndex = -1;
private int recOffset;
private int recLen;
private int[] content;
private volatile boolean closed = false;
/**
* Load the index file from the given channel.
*
* @param shpFiles The channel to read from.
* @throws IOException If an error occurs.
*/
public ShxReader(final ReadableByteChannel shxChannel, final boolean useMemoryMappedBuffer)
throws IOException {
// init the tracer if we need to debug a connection leak
assert (creationStack = new IllegalStateException().fillInStackTrace()) != null;
this.useMemoryMappedBuffer = useMemoryMappedBuffer;
final ReadableByteChannel byteChannel = shxChannel;
try {
header = readHeader(byteChannel);
//windows do not handle memory mapped buffer correctly
//the buffer is released by the GC very late, which causes some file locks to remain.
// if (byteChannel instanceof FileChannel) {
//
// this.channel = (FileChannel) byteChannel;
// if (useMemoryMappedBuffer) {
// LOGGER.finest("Memory mapping file...");
// this.buffer = this.channel.map(FileChannel.MapMode.READ_ONLY,
// 0, this.channel.size());
//
// this.channelOffset = 0;
// } else {
// LOGGER.finest("Reading from file...");
// this.buffer = ByteBuffer.allocateDirect(8 * RECS_IN_BUFFER);
// this.channelOffset = 0;
// }
//
// } else {
this.channel = null;
this.buffer = null;
LOGGER.finest("Loading all shx...");
readRecords(byteChannel);
byteChannel.close();
// }
} catch (Throwable e) {
if (byteChannel != null) {
byteChannel.close();
}
throw new IOException(e);
}
}
/**
* Get the header of this index file.
*
* @return The header of the index file.
*/
public ShapefileHeader getHeader() {
return header;
}
private void check() {
if (closed) {
throw new IllegalStateException("Index file has been closed");
}
}
private static ShapefileHeader readHeader(final ReadableByteChannel channel) throws IOException {
final ByteBuffer buffer = ByteBuffer.allocateDirect(100);
while (buffer.remaining() > 0) {
channel.read(buffer);
}
buffer.flip();
return ShapefileHeader.read(buffer, true);
}
private void readRecords(final ReadableByteChannel channel) throws IOException {
check();
final int remaining = (header.getFileLength() * 2) - 100;
final ByteBuffer buffer = ByteBuffer.allocateDirect(remaining);
buffer.order(ByteOrder.BIG_ENDIAN);
while (buffer.remaining() > 0) {
channel.read(buffer);
}
buffer.flip();
content = new int[remaining / 4]; // 2 integer for each record
final IntBuffer ints = buffer.asIntBuffer();
ints.get(content);
}
private void readRecord(final int index) throws IOException {
check();
final int pos = 100 + index * 8;
if (!useMemoryMappedBuffer) {
if (pos - channelOffset < 0
|| channelOffset + buffer.limit() <= pos
|| lastIndex == -1) {
LOGGER.finest("Filling buffer...");
channelOffset = pos;
channel.position(pos);
buffer.clear();
channel.read(buffer);
buffer.flip();
}
}
buffer.position(pos - channelOffset);
recOffset = buffer.getInt();
recLen = buffer.getInt();
lastIndex = index;
}
@Override
public void close() throws IOException {
closed = true;
if (channel != null && channel.isOpen()) {
channel.close();
}
this.content = null;
}
@Override
public boolean isClosed() {
return closed;
}
/**
* {@inheritDoc }
*/
@Override
protected void finalize() throws Throwable {
if (!closed) {
LOGGER.log(Level.WARNING,
"UNCLOSED ITERATOR : There is code leaving shx reader open, "
+ "this may cause memory leaks or data integrity problems !");
if (creationStack != null) {
LOGGER.log(Level.WARNING,
"The unclosed shx reader originated on this stack trace", creationStack);
}
this.close();
}
super.finalize();
}
/**
* Get the number of records in this index.
*
* @return The number of records.
*/
public int getRecordCount() {
return (header.getFileLength() * 2 - 100) / 8;
}
/**
* Get the offset of the record (in 16-bit words).
*
* @param index The index, from 0 to getRecordCount - 1
* @return The offset in 16-bit words.
* @throws IOException
*/
public int getOffset(final int index) throws IOException {
if (this.channel != null) {
if (this.lastIndex != index) {
this.readRecord(index);
}
return this.recOffset;
} else {
return content[2 * index];
}
}
/**
* Get the offset of the record (in real bytes, not 16-bit words).
*
* @param index The index, from 0 to getRecordCount - 1
* @return The offset in bytes.
* @throws IOException
*/
public int getOffsetInBytes(final int index) throws IOException {
return this.getOffset(index) * 2;
}
/**
* Get the content length of the given record in bytes, not 16 bit words.
*
* @param index The index, from 0 to getRecordCount - 1
* @return The lengh in bytes of the record.
* @throws IOException
*/
public int getContentLength(final int index) throws IOException {
if (this.channel != null) {
if (this.lastIndex != index) {
this.readRecord(index);
}
return this.recLen;
} else {
return content[2 * index + 1];
}
}
}