/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-2012, 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.
*
* This file is based on an origional contained in the GISToolkit project:
* http://gistoolkit.sourceforge.net/
*/
package org.geotoolkit.data.dbf;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.sis.util.logging.Logging;
/**
* A DbaseFileReader is used to read a dbase III format file. <br>
* The general use of this class is: <CODE><PRE>
*
* FileChannel in = new FileInputStream("thefile.dbf").getChannel();
* DbaseFileReader r = new DbaseFileReader( in );
* Object[] fields = new Object[r.getHeader().getNumFields()];
* while (r.hasNext()) {
* Row row = r.next();
* row.readAll(fields);
* //do stuff
* }
* r.close();
*
* </PRE></CODE>
* For consumers who wish to be a bit more selective with their reading
* of rows, the read(column) method has been added.
* Remember that the Row object is always the same.
* The values are parsed as they are read, so it pays to copy them out (as each
* call to Row.read() will result in an expensive String parse).
*
* @author Ian Schneider
* @author Johann Sorel (Geomatys)
* @module
*/
public final class DbaseFileReader implements Closeable{
/**
* Logger.
*/
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.dbf");
public static final Charset DEFAULT_STRING_CHARSET = Charset.forName("ISO-8859-1");
public final class Row {
public Object read(final int column) throws IOException {
final int offset = header.getFieldOffset(column);
final DbaseField field = fieldReaders[column];
prepareFieldRead(field, offset);
return field.read(charBuffer);
}
public Object[] readAll(Object[] entry) throws IOException {
if(entry == null){
entry = new Object[fieldReaders.length];
}else if (entry.length < fieldReaders.length) {
throw new ArrayIndexOutOfBoundsException();
}
int fieldOffset = 1; //1 to skip the delete flag
for (int x = 0; x < fieldReaders.length; x++) {
final DbaseField field = fieldReaders[x];
prepareFieldRead(field, fieldOffset);
entry[x] = field.read(charBuffer);
fieldOffset += field.fieldLength;
}
return entry;
}
}
protected final DbaseFileHeader header;
protected final ByteBuffer buffer;
protected final ReadableByteChannel channel;
protected final CharBuffer charBuffer; //char buffer cache
private final CharsetDecoder decoder;
private final DbaseField[] fieldReaders;
private int cnt = 0;
private final Row row = new Row();
private Row next = null;
protected boolean useMemoryMappedBuffer;
protected boolean randomAccessEnabled;
/**
* Creates a new instance of DBaseFileReader
*
* @param dbfChannel The readable channel to use.
* @param useMemoryMappedBuffer
* @param charset
* @throws IOException If an error occurs while initializing.
*/
public DbaseFileReader(final ReadableByteChannel dbfChannel,
final boolean useMemoryMappedBuffer, Charset charset) throws IOException {
if(charset == null) charset = DEFAULT_STRING_CHARSET;
this.channel = dbfChannel;
this.useMemoryMappedBuffer = useMemoryMappedBuffer;
this.randomAccessEnabled = (channel instanceof FileChannel);
this.header = new DbaseFileHeader();
this.header.readHeader(channel);
// create the ByteBuffer
// if we have a FileChannel, lets map it
if (channel instanceof FileChannel && this.useMemoryMappedBuffer) {
final FileChannel fc = (FileChannel) channel;
this.buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
this.buffer.position((int) fc.position());
} else {
// Force useMemoryMappedBuffer to false
this.useMemoryMappedBuffer = false;
// Some other type of channel
// start with a 8K buffer, should be more than adequate
int size = 8 * 1024;
// if for some reason its not, resize it
size = header.getRecordLength() > size ? header.getRecordLength() : size;
buffer = ByteBuffer.allocateDirect(size);
// fill it and reset
fill(buffer, channel);
buffer.flip();
}
// The entire file is in little endian
buffer.order(ByteOrder.LITTLE_ENDIAN);
// Set up some buffers and lookups for efficiency
fieldReaders = new DbaseField[header.getNumFields()];
for (int i = 0, ii = header.getNumFields(); i < ii; i++) {
fieldReaders[i] = header.getField(i);
}
charBuffer = CharBuffer.allocate(header.getRecordLength() - 1);
decoder = charset.newDecoder();
}
protected void fill(final ByteBuffer buffer, final ReadableByteChannel channel)
throws IOException {
int r = buffer.remaining();
// channel reads return -1 when EOF or other error
// because they a non-blocking reads, 0 is a valid return value!!
while (buffer.remaining() > 0 && r != -1) {
r = channel.read(buffer);
}
if (r == -1) {
buffer.limit(buffer.position());
}
}
/**
* Fill buffer if remaining is smaller then one record size.
* @throws IOException
*/
private void bufferCheck() throws IOException {
buffer.limit(buffer.capacity());
if (!buffer.isReadOnly() && buffer.remaining() < header.getRecordLength()) {
buffer.compact();
fill(buffer, channel);
buffer.position(0);
}
}
/**
* Get the header from this file. The header is read upon instantiation.
*
* @return The header associated with this file or null if an error
* occurred.
*/
public DbaseFileHeader getHeader() {
return header;
}
/**
* Query the reader as to whether there is another record.
*
* @return True if more records exist, false otherwise.
*/
public boolean hasNext() {
return cnt < header.getNumRecords();
}
/**
* @return Row, always same instance
* @throws IOException
*/
public Row next() throws IOException {
checkNext();
final Row r = next;
next = null;
return r;
}
private void checkNext() throws IOException{
if(next!=null)return;
if(cnt != 0){
//move cursor to next record if it's not the first
buffer.position(buffer.position()+header.getRecordLength());
}
prepareNext();
}
/**
* fill buffer with current record, skip it if it's deleted
* @throws IOException
*/
private void prepareNext() throws IOException {
boolean foundRecord = false;
while (!foundRecord) {
bufferCheck();
// read the deleted flag
char deleted = (char) buffer.get();
if (deleted == '*') {
//record was deleted, move to next one, -1 for the delete flag we just read
buffer.position(buffer.position()+header.getRecordLength()-1);
continue;
}
buffer.position(buffer.position()-1);
foundRecord = true;
next = row;
}
cnt++;
}
private void prepareFieldRead(final DbaseField field, final int fieldOffset) throws CharacterCodingException{
//prepare byte buffer
final int previousposition = buffer.position();
final int previouslimit = buffer.limit();
decoder.reset();
charBuffer.clear();
buffer.position(previousposition+fieldOffset);
buffer.limit(buffer.position()+field.fieldLength);
CoderResult result = decoder.decode(buffer, charBuffer, true);
if(result == CoderResult.OVERFLOW){
result.throwException();
} else if(result != CoderResult.UNDERFLOW) {
try {
result.throwException();
} catch (Exception e) {
LOGGER.log(Level.INFO, e.getMessage(), e);
}
}
result = decoder.flush(charBuffer);
if(CoderResult.UNDERFLOW != result){
result.throwException();
}
buffer.limit(previouslimit);
buffer.position(previousposition);
charBuffer.flip();
}
/**
* Transfer, by bytes, the next record to the writer.
* @param writer
* @throws IOException
*/
public void transferTo(final DbaseFileWriter writer) throws IOException {
bufferCheck();
buffer.limit(buffer.position() + header.getRecordLength());
writer.channel.write(buffer);
buffer.limit(buffer.capacity());
cnt++;
}
/**
* Navigate to the given record index.
*
* @param recno
* @throws IOException
* @throws UnsupportedOperationException
*/
public void goTo(final int recno) throws IOException, UnsupportedOperationException {
if (randomAccessEnabled) {
final long newPosition = header.getHeaderLength()
+ header.getRecordLength() * (long)(recno - 1);
if (useMemoryMappedBuffer) {
buffer.position((int)newPosition);
} else {
final FileChannel fc = (FileChannel) channel;
fc.position(newPosition);
buffer.limit(buffer.capacity());
buffer.position(0);
fill(buffer, channel);
buffer.position(0);
}
prepareNext();
} else {
throw new UnsupportedOperationException("Random access not enabled!");
}
}
/**
* If this method return true, then the index navigation (goto method) can be used.
* @return true if source is a FileChannel
*/
public boolean IsRandomAccessEnabled() {
return this.randomAccessEnabled;
}
/**
* Clean up all resources associated with this reader.<B>Highly recomended.</B>
*
* @throws IOException If an error occurs.
*/
@Override
public void close() throws IOException {
if (channel.isOpen()) {
channel.close();
}
}
@Override
public boolean isClosed() {
return !channel.isOpen();
}
}