/*
* Copyright (C) 2013 johnlindsay
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package whitebox.geospatialfiles.shapefile.attributes;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import static whitebox.geospatialfiles.shapefile.attributes.AttributeTable.SIG_DBASE_III;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.BOOLEAN;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.DATE;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.FLOAT;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.MEMO;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.NUMERIC;
import static whitebox.geospatialfiles.shapefile.attributes.DBFField.DBFDataType.STRING;
/**
*
* @author johnlindsay
*/
public class AttributeTable {
private String fileName;
private boolean isDirty = false;
protected String characterSetName = "8859_1";
protected final int END_OF_DATA = 0x1A;
/**
* Used to create an AttributeTable object when the DBF file already exists.
*
* @param fileName String containing the full file name and directory.
*/
public AttributeTable(String fileName) throws IOException {
this.signature = SIG_DBASE_III;
this.terminator1 = 0x0D;
this.fileName = fileName;
initialize();
}
/**
* Used to create an AttributeTable object when the DBF file does not
* already exist. A new DBF file will be created and initialized with the
* specified fields but it will not contain any records.
*
* @param fileName String containing the full file name and directory.
* @param fields Array of DBFField type.
* @param destroy Option to destroy an existing DBF file.
*/
public AttributeTable(String fileName, DBFField[] fields, boolean destroy) throws DBFException, IOException {
this.signature = SIG_DBASE_III;
this.terminator1 = 0x0D;
this.fileName = fileName;
createDBFFile(new File(fileName), destroy);
setFields(fields);
write();
initialize();
}
/**
* Verifies the existence of or creates a valid DBF file on disk which can
* then have fields and records added or removed.
*
* @param dbfFile. The file passed in should be a valid DBF file or
* non-existent.
* @exception Throws DBFException if the passed in file does exist but not a
* valid DBF file, or if an IO error occurs.
*/
private void createDBFFile(File dbfFile, boolean destroy) throws DBFException, IOException {
if (destroy == true) {
if (dbfFile.exists()) {
try {
dbfFile.delete();
} catch (Exception e) {
}
}
}
try (RandomAccessFile raf = new RandomAccessFile(dbfFile, "rw")) {
if (dbfFile.length() == 0) {
writeHeader(raf);
} else {
readHeader();
}
} catch (FileNotFoundException e) {
throw new DBFException("Specified file is not found. " + e.getMessage());
}
}
// properties
public String getFileName() {
return fileName;
}
public int getCurrentRecord() {
return currentRecord;
}
public void setCurrentRecord(int currentRecord) {
this.currentRecord = currentRecord;
}
/*
If the library is used in a non-latin environment use this method to set
corresponding character set. More information:
http://www.iana.org/assignments/character-sets
Also see the documentation of the class java.nio.charset.Charset
*/
public String getCharactersetName() {
return this.characterSetName;
}
public void setCharactersetName(String characterSetName) {
this.characterSetName = characterSetName;
}
public int getNumberOfRecords() {
return numberOfRecords;
}
// public methods
/**
* Returns the asked Field. In case of an invalid index, it returns a
* ArrayIndexOutofboundsException.
*
* @param index. Index of the field. Index of the first field is zero.
*/
public DBFField getField(int index) {
return this.fieldArray[index];
}
public void updateFieldName(int index, String name) {
this.fieldArray[index].setName(name);
try {
this.write();
} catch (Exception e) {
// do nothing.
}
}
/**
* Retrieves all fields in this database.
*
* @return DBFField array
*/
public DBFField[] getAllFields() {
return this.fieldArray;
}
int fieldCount;
/**
* Returns the number of field in the DBF.
*/
public int getFieldCount() {
if (this.fieldArray != null) {
return this.fieldArray.length;
}
return -1;
}
/**
* Used to determine whether the table has been modified since it was last
* saved.
*
* @return boolean
*/
public boolean isTableDirty() {
return isDirty;
}
/**
* Returns a String array of fields.
*
* @return String array
*/
public String[] getAttributeTableFieldNames() {
int numberOfFields = this.getFieldCount();
String[] ret = new String[numberOfFields];
for (int i = 0; i < numberOfFields; i++) {
DBFField field = this.getField(i);
ret[i] = field.getName();
}
return ret;
}
/**
* Sets fields for new files only.
*/
private void setFields(DBFField[] fields)
throws DBFException {
if (this.fieldArray != null) {
throw new DBFException("Fields has already been set");
}
if (fields == null || fields.length == 0) {
throw new DBFException("Should have at least one field");
}
for (int i = 0; i < fields.length; i++) {
if (fields[i] == null) {
throw new DBFException("Field " + (i + 1) + " is null");
}
}
this.fieldArray = fields;
try (RandomAccessFile raf = new RandomAccessFile(this.fileName, "rw")) {
writeHeader(raf);
} catch (IOException e) {
throw new DBFException("Error accesing file");
}
}
/**
* Appends a new field at the end of the attribute table. Notice that the
* dbf file must already exist.
*
* @param field the DBFField to append.
* @throws DBFException
*/
public void addField(DBFField field) throws DBFException {
addField(field, this.fieldCount);
}
/**
* Adds a DBFField after the specified index. Index 0 specifies before the
* first field and index {@link fieldCount} specifies after the last field.
*
* @param field. An initialized DBFField object.
* @param insertAt. The index to insert field.
* @throws DBFException
*/
public void addField(DBFField field, int insertAt) throws DBFException {
if (field == null) {
throw new DBFException("New field name is empty");
} else if (insertAt < 0 || insertAt > this.fieldCount) {
throw new DBFException("Param insertAt is out of table range");
} else if (this.fileName.isEmpty()) {
throw new DBFException("DBF file name not specified");
} else if (!(new File(this.fileName).exists())) {
throw new DBFException("DBF file does not exist");
}
try {
// create a temporary file to house the new dbf
String fileNameCopy = this.fileName.replace(".dbf", "_copy.dbf");
if (new File(fileNameCopy).exists()) {
new File(fileNameCopy).delete();
}
DBFField[] outFields = new DBFField[this.fieldCount + 1];
DBFField[] inFields = getAllFields();
// Copy all fields before insertAt
System.arraycopy(inFields, 0, outFields, 0, insertAt);
// Copy new field
outFields[insertAt] = field;
// If we're not at the end, copy the rest of the fields
if (insertAt < this.fieldCount) {
System.arraycopy(inFields, insertAt, outFields, insertAt + 1, this.fieldCount - insertAt);
}
AttributeTable newTable = new AttributeTable(fileNameCopy, outFields, true); // used to set up the dbf copy
for (int a = 0; a < this.numberOfRecords; a++) {
Object[] inRec = getRecord(a);
Object[] outRec = new Object[this.fieldCount + 1];
// Record data for new field is left null
System.arraycopy(inRec, 0, outRec, 0, insertAt);
System.arraycopy(inRec, insertAt, outRec, insertAt + 1, this.fieldCount - insertAt);
newTable.addRecord(outRec);
}
newTable.write();
File oldFile = new File(this.fileName);
// Rename old file in case something horrible happens
if (oldFile.renameTo(new File(this.fileName.concat(".bak")))) {
File newFile = new File(fileNameCopy);
// Rename new file to old file's name
if (newFile.renameTo(new File(this.fileName))) {
// Delete the backup for oldFile
new File(this.fileName.concat(".bak")).delete();
initialize();
}
}
} catch (Exception e) {
throw new DBFException(e.getMessage());
}
}
/**
* Removes the specified index from the fields. Warning: this method creates
* a new file with less fields and will delete the old one.
*
* @param removeIndex The index to remove from the fieldArray
* @throws DBFException
*/
public void deleteField(int removeIndex) throws DBFException {
// Can't be below 0 and if fieldNum == 0, we can't remove anything
if (removeIndex < 0 || removeIndex >= this.fieldCount) {
throw new DBFException("Param fieldNum is out of table range");
} else if (this.fileName.isEmpty()) {
throw new DBFException("DBF file name not specified");
} else if (!(new File(this.fileName).exists())) {
throw new DBFException("DBF file does not exist");
}
try {
// create a temporary file to house the new dbf
String fileNameCopy = this.fileName.replace(".dbf", "_copy.dbf");
if (new File(fileNameCopy).exists()) {
new File(fileNameCopy).delete();
}
DBFField[] outFields = new DBFField[this.fieldCount - 1];
DBFField[] inFields = getAllFields();
// Copy all fields before fieldNum
System.arraycopy(inFields, 0, outFields, 0, removeIndex);
// Copy all fields after fieldNum
System.arraycopy(inFields, removeIndex + 1, outFields, removeIndex, (this.fieldCount - removeIndex) - 1);
AttributeTable newTable = new AttributeTable(fileNameCopy, outFields, true); // used to set up the dbf copy
for (int a = 0; a < this.numberOfRecords; a++) {
Object[] inRec = getRecord(a);
Object[] outRec = new Object[this.fieldCount - 1];
// Discard the old field
System.arraycopy(inRec, 0, outRec, 0, removeIndex);
System.arraycopy(inRec, removeIndex + 1, outRec, removeIndex, (this.fieldCount - removeIndex) - 1);
newTable.addRecord(outRec);
}
newTable.write();
File oldFile = new File(this.fileName);
// Rename old file in casenew File(oldFile.getPath().concat(".bak")) something horrible happens
if (oldFile.renameTo(new File(this.fileName.concat(".bak")))) {
File newFile = new File(fileNameCopy);
// Rename new file to old file's name
if (newFile.renameTo(new File(this.fileName))) {
// Delete the backup for oldFile
new File(this.fileName.concat(".bak")).delete();
initialize();
}
}
} catch (Exception e) {
throw new DBFException(e.getMessage());
}
}
/**
* Deletes the field with the name fieldName. If multiple fields exist with
* the name, the first in index order will be removed.
*
* @param fieldName String matching the field to be removed
* @throws DBFException
*/
public void deleteField(String fieldName) throws DBFException {
// Find the field with the given name
if (fieldName == null) {
throw new DBFException("fieldName can not be null");
}
DBFField[] fields = getAllFields();
for (int i = 0; i < fields.length; i++) {
if (fieldName.equals(fields[i].getName())) {
deleteField(i);
break;
}
}
}
final private Map<String, Integer> fieldMap = new HashMap<>();
private void initializeFieldMap() {
String[] fieldNames = getAttributeTableFieldNames();
for (int i = 0; i < fieldNames.length; i++) {
fieldMap.put(fieldNames[i], i);
}
}
public byte getFieldType(int fieldNum) {
return fieldArray[fieldNum].getDataType().getSymbol();
}
public byte getFieldType(String fieldName) {
int fieldNum = fieldMap.get(fieldName);
return fieldArray[fieldNum].getDataType().getSymbol();
}
public int getFieldColumnNumberFromName(String fieldName) {
if (fieldMap.containsKey(fieldName)) {
return fieldMap.get(fieldName);
} else {
return - 1;
}
}
public Object getValue(int recNum, int fieldNum) throws DBFException {
currentRecord = recNum;
if (currentRecord < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (currentRecord >= this.numberOfRecords) {
return null;
}
Object recordObjects[] = new Object[this.fieldArray.length];
RandomAccessFile rIn = null;
ByteBuffer buf;
FileChannel inChannel = null;
try {
buf = ByteBuffer.allocate(this.recordLength);
rIn = new RandomAccessFile(this.fileName, "r");
inChannel = rIn.getChannel();
int pos = (32 + (32 * this.fieldArray.length)) + 1 + recNum * this.recordLength;
inChannel.position(pos);
inChannel.read(buf);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.rewind();
if (buf.get() == END_OF_DATA) {
return null;
} // record has been deleted
for (int i = 0; i < this.fieldArray.length; i++) {
switch (this.fieldArray[i].getDataType()) {
case STRING:
byte b_array[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(b_array);
buf.get(b_array);
String str = new String(b_array, characterSetName);
recordObjects[i] = str.trim();
break;
case DATE:
byte t_byte_year[] = new byte[4];
//dataInputStream.read(t_byte_year);
buf.get(t_byte_year);
byte t_byte_month[] = new byte[2];
//dataInputStream.read(t_byte_month);
buf.get(t_byte_month);
byte t_byte_day[] = new byte[2];
//dataInputStream.read(t_byte_day);
buf.get(t_byte_day);
try {
GregorianCalendar calendar = new GregorianCalendar(
Integer.parseInt(new String(t_byte_year)),
Integer.parseInt(new String(t_byte_month)) - 1,
Integer.parseInt(new String(t_byte_day)));
recordObjects[i] = calendar.getTime();
} catch (NumberFormatException e) {
/*
* this field may be empty or may have improper
* value set
*/
recordObjects[i] = null;
}
break;
case FLOAT:
try {
byte t_float[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_float);
buf.get(t_float);
t_float = Utils.trimLeftSpaces(t_float);
if (t_float.length > 0 && !Utils.contains(t_float, (byte) '?')) {
recordObjects[i] = new Double(new String(t_float)); //Float(new String(t_float));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Float: " + e.getMessage());
}
break;
case NUMERIC:
try {
byte t_numeric[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_numeric);
buf.get(t_numeric);
t_numeric = Utils.trimLeftSpaces(t_numeric);
if (t_numeric.length > 0 && !Utils.contains(t_numeric, (byte) '?')) {
recordObjects[i] = new Double(new String(t_numeric));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Number: " + e.getMessage());
}
break;
case BOOLEAN:
byte t_logical = buf.get();
if (t_logical == 'Y' || t_logical == 't' || t_logical == 'T' || t_logical == 't') {
recordObjects[i] = Boolean.TRUE;
} else {
recordObjects[i] = Boolean.FALSE;
}
break;
case MEMO:
// TODO Later
recordObjects[i] = "null";
break;
default:
recordObjects[i] = "null";
}
}
} catch (EOFException e) {
return null;
} catch (IOException e) {
throw new DBFException(e.getMessage());
} finally {
if (rIn != null) {
try {
rIn.close();
} catch (Exception e) {
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (Exception e) {
}
}
}
return recordObjects[fieldNum];
}
public Object getValue(int recNum, String fieldName) throws DBFException {
currentRecord = recNum;
int fieldNum = fieldMap.get(fieldName);
return this.getValue(recNum, fieldNum);
// if (currentRecord < 0) {
// throw new DBFException("Record number is out of bounds.");
// }
// if (currentRecord >= this.numberOfRecords) {
// return null;
// }
//
// Object recordObjects[] = new Object[this.fieldArray.length];
//
// RandomAccessFile rIn = null;
// ByteBuffer buf;
// FileChannel inChannel = null;
//
// try {
// buf = ByteBuffer.allocate(this.recordLength);
//
// rIn = new RandomAccessFile(this.fileName, "r");
//
// inChannel = rIn.getChannel();
//
// int pos = (32 + (32 * this.fieldArray.length)) + 1 + recNum * this.recordLength;
// inChannel.position(pos);
// inChannel.read(buf);
//
// buf.order(ByteOrder.LITTLE_ENDIAN);
// buf.rewind();
//
// if (buf.get() == END_OF_DATA) {
// return null;
// } // record has been deleted
//
// for (int i = 0; i < this.fieldArray.length; i++) {
//
// switch (this.fieldArray[i].getDataType()) {
//
// case STRING:
//
// byte b_array[] = new byte[this.fieldArray[i].getFieldLength()];
// //dataInputStream.read(b_array);
// buf.get(b_array);
// String str = new String(b_array, characterSetName);
// recordObjects[i] = str.trim();
// break;
//
// case DATE:
//
// byte t_byte_year[] = new byte[4];
// //dataInputStream.read(t_byte_year);
// buf.get(t_byte_year);
//
// byte t_byte_month[] = new byte[2];
// //dataInputStream.read(t_byte_month);
// buf.get(t_byte_month);
//
// byte t_byte_day[] = new byte[2];
// //dataInputStream.read(t_byte_day);
// buf.get(t_byte_day);
//
// try {
//
// GregorianCalendar calendar = new GregorianCalendar(
// Integer.parseInt(new String(t_byte_year)),
// Integer.parseInt(new String(t_byte_month)) - 1,
// Integer.parseInt(new String(t_byte_day)));
//
// recordObjects[i] = calendar.getTime();
// } catch (NumberFormatException e) {
// /*
// * this field may be empty or may have improper
// * value set
// */
// recordObjects[i] = null;
// }
//
// break;
//
// case FLOAT:
//
// try {
//
// byte t_float[] = new byte[this.fieldArray[i].getFieldLength()];
// //dataInputStream.read(t_float);
// buf.get(t_float);
//
// t_float = Utils.trimLeftSpaces(t_float);
// if (t_float.length > 0 && !Utils.contains(t_float, (byte) '?')) {
//
// recordObjects[i] = new Double(new String(t_float)); //Float(new String(t_float));
// } else {
//
// recordObjects[i] = null;
// }
// } catch (NumberFormatException e) {
//
// throw new DBFException("Failed to parse Float: " + e.getMessage());
// }
//
// break;
//
// case NUMERIC:
//
// try {
//
// byte t_numeric[] = new byte[this.fieldArray[i].getFieldLength()];
// //dataInputStream.read(t_numeric);
// buf.get(t_numeric);
//
// t_numeric = Utils.trimLeftSpaces(t_numeric);
//
// if (t_numeric.length > 0 && !Utils.contains(t_numeric, (byte) '?')) {
//
// recordObjects[i] = new Double(new String(t_numeric));
// } else {
//
// recordObjects[i] = null;
// }
// } catch (NumberFormatException e) {
//
// throw new DBFException("Failed to parse Number: " + e.getMessage());
// }
//
// break;
//
// case BOOLEAN:
//
// byte t_logical = buf.get();
// if (t_logical == 'Y' || t_logical == 't' || t_logical == 'T' || t_logical == 't') {
//
// recordObjects[i] = Boolean.TRUE;
// } else {
//
// recordObjects[i] = Boolean.FALSE;
// }
// break;
//
// case MEMO:
// // TODO Later
// recordObjects[i] = "null";
// break;
//
// default:
// recordObjects[i] = "null";
// }
// }
// } catch (EOFException e) {
// return null;
// } catch (IOException e) {
// throw new DBFException(e.getMessage());
// } finally {
// if (rIn != null) {
// try {
// rIn.close();
// } catch (Exception e) {
// }
// }
// if (inChannel != null) {
// try {
// inChannel.close();
// } catch (Exception e) {
// }
// }
// }
//
// return recordObjects[fieldNum];
}
public void setValue(int recordNumber, int fieldNum, Object data) throws DBFException {
if (recordNumber < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (recordNumber > this.numberOfRecords) {
throw new DBFException("Record number is out of bounds.");
}
Object[] rowData = getRecord(recordNumber);
rowData[fieldNum] = data;
updateRecord(recordNumber, rowData);
}
public void setValue(int recordNumber, String fieldName, Object data) throws DBFException {
int fieldNum = getFieldColumnNumberFromName(fieldName);
if (fieldNum == -1) {
throw new DBFException("Field name not found.");
}
setValue(recordNumber, fieldNum, data);
}
private int currentRecord = -1;
/**
* Reads the returns the <i>n</i>th row in the DBF stream.
*
* @param n Record number.
* @return The <i>n</i>th record as an Object array. Types of the elements
* these arrays follow the convention mentioned in the class description.
* @throws DBFException
*/
public Object[] getRecord(int n) throws DBFException {
currentRecord = n;
if (currentRecord < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (currentRecord >= this.numberOfRecords) {
return null;
}
Object recordObjects[] = new Object[this.fieldArray.length];
RandomAccessFile rIn = null;
ByteBuffer buf;
FileChannel inChannel = null;
try {
buf = ByteBuffer.allocate(this.recordLength);
rIn = new RandomAccessFile(this.fileName, "r");
inChannel = rIn.getChannel();
int pos = (32 + (32 * this.fieldArray.length)) + 1 + n * this.recordLength;
inChannel.position(pos);
inChannel.read(buf);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.rewind();
if (buf.get() == END_OF_DATA) {
return null;
} // record has been deleted
for (int i = 0; i < this.fieldArray.length; i++) {
switch (this.fieldArray[i].getDataType()) {
case STRING:
byte b_array[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(b_array);
buf.get(b_array);
recordObjects[i] = new String(b_array, characterSetName);
break;
case DATE:
byte t_byte_year[] = new byte[4];
//dataInputStream.read(t_byte_year);
buf.get(t_byte_year);
byte t_byte_month[] = new byte[2];
//dataInputStream.read(t_byte_month);
buf.get(t_byte_month);
byte t_byte_day[] = new byte[2];
//dataInputStream.read(t_byte_day);
buf.get(t_byte_day);
try {
GregorianCalendar calendar = new GregorianCalendar(
Integer.parseInt(new String(t_byte_year)),
Integer.parseInt(new String(t_byte_month)) - 1,
Integer.parseInt(new String(t_byte_day)));
recordObjects[i] = calendar.getTime();
} catch (NumberFormatException e) {
/*
* this field may be empty or may have improper
* value set
*/
recordObjects[i] = null;
}
break;
case FLOAT:
try {
byte t_float[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_float);
buf.get(t_float);
t_float = Utils.trimLeftSpaces(t_float);
if (t_float.length > 0 && !Utils.contains(t_float, (byte) '?')
&& !Utils.contains(t_float, (byte) '*')) {
recordObjects[i] = new Double(new String(t_float)); //Float(new String(t_float));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Float: " + e.getMessage());
}
break;
case NUMERIC:
try {
byte t_numeric[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_numeric);
buf.get(t_numeric);
t_numeric = Utils.trimLeftSpaces(t_numeric);
if (t_numeric.length > 0 && !Utils.contains(t_numeric, (byte) '?')
&& !Utils.contains(t_numeric, (byte) '*')) {
recordObjects[i] = new Double(new String(t_numeric));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Number: " + e.getMessage());
}
break;
case BOOLEAN:
byte t_logical = buf.get();
if (t_logical == 'Y' || t_logical == 't' || t_logical == 'T' || t_logical == 't') {
recordObjects[i] = Boolean.TRUE;
} else {
recordObjects[i] = Boolean.FALSE;
}
break;
case MEMO:
// TODO Later
recordObjects[i] = "null";
break;
default:
recordObjects[i] = "null";
}
}
} catch (EOFException e) {
return null;
} catch (IOException e) {
throw new DBFException(e.getMessage());
} finally {
if (rIn != null) {
try {
rIn.close();
} catch (Exception e) {
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (Exception e) {
}
}
}
return recordObjects;
}
/**
* Reads the records from <i>startingRecord</i> to <i>endingRecord</i> from
* the table.
*
* @param startingRecord the first record read (zero-based).
* @param endingRecord the last record read (note the range is inclusive of
* endingRecord).
* @return an Object[] array where each element is another Object[] array of
* record values.
* @throws DBFException
*/
public Object[] getRecords(int startingRecord, int endingRecord) throws DBFException {
if (startingRecord < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (endingRecord >= this.numberOfRecords) {
endingRecord = this.numberOfRecords - 1;
}
currentRecord = endingRecord;
int numRecsRead = endingRecord - startingRecord + 1;
Object returnRecords[] = new Object[numRecsRead];
RandomAccessFile rIn = null;
ByteBuffer buf;
FileChannel inChannel = null;
try {
int numBytesToRead = this.recordLength * numRecsRead;
buf = ByteBuffer.allocate(numBytesToRead);
rIn = new RandomAccessFile(this.fileName, "r");
inChannel = rIn.getChannel();
int pos = (32 + (32 * this.fieldArray.length)) + 1 + startingRecord * this.recordLength;
inChannel.position(pos);
inChannel.read(buf);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.rewind();
for (int n = startingRecord; n <= endingRecord; n++) {
if (buf.get() == END_OF_DATA) {
return null;
} // record has been deleted
Object recordObjects[] = new Object[this.fieldArray.length];
for (int i = 0; i < this.fieldArray.length; i++) {
switch (this.fieldArray[i].getDataType()) {
case STRING:
byte b_array[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(b_array);
buf.get(b_array);
recordObjects[i] = new String(b_array, characterSetName);
break;
case DATE:
byte t_byte_year[] = new byte[4];
//dataInputStream.read(t_byte_year);
buf.get(t_byte_year);
byte t_byte_month[] = new byte[2];
//dataInputStream.read(t_byte_month);
buf.get(t_byte_month);
byte t_byte_day[] = new byte[2];
//dataInputStream.read(t_byte_day);
buf.get(t_byte_day);
try {
GregorianCalendar calendar = new GregorianCalendar(
Integer.parseInt(new String(t_byte_year)),
Integer.parseInt(new String(t_byte_month)) - 1,
Integer.parseInt(new String(t_byte_day)));
recordObjects[i] = calendar.getTime();
} catch (NumberFormatException e) {
/*
* this field may be empty or may have improper
* value set
*/
recordObjects[i] = null;
}
break;
case FLOAT:
try {
byte t_float[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_float);
buf.get(t_float);
t_float = Utils.trimLeftSpaces(t_float);
if (t_float.length > 0 && !Utils.contains(t_float, (byte) '?')) {
recordObjects[i] = new Float(new String(t_float));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Float: " + e.getMessage());
}
break;
case NUMERIC:
try {
byte t_numeric[] = new byte[this.fieldArray[i].getFieldLength()];
//dataInputStream.read(t_numeric);
buf.get(t_numeric);
t_numeric = Utils.trimLeftSpaces(t_numeric);
if (t_numeric.length > 0 && !Utils.contains(t_numeric, (byte) '?')) {
recordObjects[i] = new Double(new String(t_numeric));
} else {
recordObjects[i] = null;
}
} catch (NumberFormatException e) {
throw new DBFException("Failed to parse Number: " + e.getMessage());
}
break;
case BOOLEAN:
byte t_logical = buf.get();
if (t_logical == 'Y' || t_logical == 't' || t_logical == 'T' || t_logical == 't') {
recordObjects[i] = Boolean.TRUE;
} else {
recordObjects[i] = Boolean.FALSE;
}
break;
case MEMO:
// TODO Later
recordObjects[i] = new String("null");
break;
default:
recordObjects[i] = new String("null");
}
}
returnRecords[n - startingRecord] = recordObjects;
}
} catch (EOFException e) {
return null;
} catch (IOException e) {
throw new DBFException(e.getMessage());
} finally {
if (rIn != null) {
try {
rIn.close();
} catch (Exception e) {
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (Exception e) {
}
}
}
return returnRecords;
}
/**
* Reads the returns the next row in the DBF stream. @returns The next row
* as an Object array. Types of the elements these arrays follow the
* convention mentioned in the class description.
*/
public Object[] nextRecord() throws DBFException {
currentRecord++;
if (currentRecord < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (currentRecord >= this.numberOfRecords) {
return null;
}
return getRecord(currentRecord);
}
ArrayList recordData = new ArrayList();
/**
* Add a record to the dbf. Note that this method actually writes the record
* to a temporary ArrayList and that the file will not be updated until the
* write() method is called.
*
* @param values an Object array containing the record values for each
* field.
* @throws DBFException
*/
public void addRecord(Object[] values) throws DBFException {
if (this.fieldArray == null) {
throw new DBFException("Fields should be set before adding records");
}
if (values == null) {
throw new DBFException("Null cannot be added as row");
}
if (values.length != this.fieldArray.length) {
throw new DBFException("Invalid record. Invalid number of fields in row");
}
for (int i = 0; i < this.fieldArray.length; i++) {
if (values[i] == null) {
// null values are not checked
continue;
}
Class equivalentClass = this.fieldArray[i].getDataType().getEquivalentClass();
// Check if values[i] is an instance or subclassed instance of the field's expected type
if (!(equivalentClass.isAssignableFrom(values[i].getClass()))) { //!(values[i].getClass().isAssignableFrom(equivalentClass))) {
throw new DBFException("Invalid value for field " + i);
}
}
recordData.add(values);
isDirty = true;
}
/**
* Replace an existing record. Note that this method actually writes the
* record to a temporary buffer and that the file will not be updated until
* the write() method is called.
*
* @param values an Object array containing the record values for each
* field.
* @throws DBFException
*/
public void changeRecord(int index, Object[] values) throws DBFException {
if (this.fieldArray == null) {
throw new DBFException("Fields should be set before adding records");
}
if (values == null) {
throw new DBFException("Null cannot be added as row");
}
if (values.length != this.fieldArray.length) {
throw new DBFException("Invalid record. Invalid number of fields in row");
}
for (int i = 0; i < this.fieldArray.length; i++) {
if (values[i] == null) {
// null values are not checked
continue;
}
Class equivalentClass = this.fieldArray[i].getDataType().getEquivalentClass();
// Check if values[i] is an instance or subclassed instance of the field's expected type
if (!(values[i].getClass().isAssignableFrom(equivalentClass))) {
throw new DBFException("Invalid value for field " + i);
}
}
recordData.add(index, values);
}
public void updateRecord(int recordNumber, Object[] rowData) throws DBFException {
if (recordNumber < 0) {
throw new DBFException("Record number is out of bounds.");
}
if (recordNumber > this.numberOfRecords) {
throw new DBFException("Record number is out of bounds.");
}
if (recordNumber == this.numberOfRecords) { // append it to the end of the file
addRecord(rowData);
}
RandomAccessFile raf = null;
ByteBuffer buf;
try {
buf = ByteBuffer.allocate(this.recordLength);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.rewind();
buf.put((byte) ' ');
for (int j = 0; j < this.fieldArray.length; j++) { /*
* iterate throught fields
*/
switch (this.fieldArray[j].getDataType()) {
case STRING:
if (rowData[j] != null) {
String str_value = rowData[j].toString();
buf.put(Utils.textPadding(str_value, characterSetName, this.fieldArray[j].getFieldLength()));
} else {
buf.put(Utils.textPadding("", this.characterSetName, this.fieldArray[j].getFieldLength()));
}
break;
case DATE:
if (rowData[j] != null) {
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime((Date) rowData[j]);
buf.put(String.valueOf(calendar.get(Calendar.YEAR)).getBytes());
buf.put(Utils.textPadding(String.valueOf(calendar.get(Calendar.MONTH) + 1), this.characterSetName, 2, Utils.ALIGN_RIGHT, (byte) '0'));
buf.put(Utils.textPadding(String.valueOf(calendar.get(Calendar.DAY_OF_MONTH)), this.characterSetName, 2, Utils.ALIGN_RIGHT, (byte) '0'));
} else {
buf.put(" ".getBytes());
}
break;
case FLOAT:
if (rowData[j] != null) {
buf.put(Utils.doubleFormating((Double) rowData[j], this.characterSetName, this.fieldArray[j].getFieldLength(), this.fieldArray[j].getDecimalCount()));
} else {
buf.put(Utils.textPadding("?", this.characterSetName, this.fieldArray[j].getFieldLength(), Utils.ALIGN_RIGHT));
}
break;
case NUMERIC:
if (rowData[j] != null) {
buf.put(
Utils.doubleFormating((Double) rowData[j], this.characterSetName, this.fieldArray[j].getFieldLength(), this.fieldArray[j].getDecimalCount()));
} else {
buf.put(
Utils.textPadding("?", this.characterSetName, this.fieldArray[j].getFieldLength(), Utils.ALIGN_RIGHT));
}
break;
case BOOLEAN:
if (rowData[j] != null) {
if ((Boolean) rowData[j] == Boolean.TRUE) {
buf.put((byte) 'T');
} else {
buf.put((byte) 'F');
}
} else {
buf.put((byte) '?');
}
break;
case MEMO:
break;
default:
throw new DBFException("Unknown field type " + this.fieldArray[j].getDataType());
}
}
raf = new RandomAccessFile(this.fileName, "rw");
int pos = (32 + (32 * this.fieldArray.length)) + 1 + recordNumber * this.recordLength;
raf.seek(pos);
raf.write(buf.array());
} catch (IOException e) {
throw new DBFException(e.getMessage());
} finally {
isDirty = true;
if (raf != null) {
try {
raf.close();
} catch (Exception e) {
}
}
}
}
/**
* Removes the record of index recordNumber from the record data.
*
* @param recordNumber. Index of record to remove
* @throws DBFException
*/
public void deleteRecord(int recordNumber) throws DBFException {
if (recordNumber < 0 || recordNumber >= recordData.size()) {
throw new DBFException("Record number outside of table range.");
}
recordData.remove(recordNumber);
isDirty = true;
}
// private methods
private void initialize() throws IOException {
readHeader();
fieldCount = this.fieldArray.length;
initializeFieldMap();
}
private void writeRecord(RandomAccessFile raf, Object[] values) throws DBFException {
//RandomAccessFile raf = null;
ByteBuffer buf;
try {
numberOfRecords++;
buf = ByteBuffer.allocate(this.recordLength);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.rewind();
buf.put((byte) ' ');
for (int j = 0; j < this.fieldArray.length; j++) {
/*
* iterate throught fields
*/
switch (this.fieldArray[j].getDataType()) {
case STRING:
if (values[j] != null) {
String str_value = values[j].toString();
buf.put(Utils.textPadding(str_value, characterSetName, this.fieldArray[j].getFieldLength()));
} else {
buf.put(Utils.textPadding("", this.characterSetName, this.fieldArray[j].getFieldLength()));
}
break;
case DATE:
if (values[j] != null) {
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime((Date) values[j]);
StringBuffer t_sb = new StringBuffer();
buf.put(String.valueOf(calendar.get(Calendar.YEAR)).getBytes());
buf.put(Utils.textPadding(String.valueOf(calendar.get(Calendar.MONTH) + 1), this.characterSetName, 2, Utils.ALIGN_RIGHT, (byte) '0'));
buf.put(Utils.textPadding(String.valueOf(calendar.get(Calendar.DAY_OF_MONTH)), this.characterSetName, 2, Utils.ALIGN_RIGHT, (byte) '0'));
} else {
buf.put(" ".getBytes());
}
break;
case FLOAT:
if (values[j] != null) {
buf.put(Utils.doubleFormating((Double) values[j], this.characterSetName, this.fieldArray[j].getFieldLength(), this.fieldArray[j].getDecimalCount()));
} else {
buf.put(Utils.textPadding("?", this.characterSetName, this.fieldArray[j].getFieldLength(), Utils.ALIGN_RIGHT));
}
break;
case NUMERIC:
if (values[j] != null) {
buf.put(
Utils.doubleFormating((Double) values[j], this.characterSetName, this.fieldArray[j].getFieldLength(), this.fieldArray[j].getDecimalCount()));
} else {
buf.put(
Utils.textPadding("?", this.characterSetName, this.fieldArray[j].getFieldLength(), Utils.ALIGN_RIGHT));
}
break;
case BOOLEAN:
if (values[j] != null) {
if ((Boolean) values[j] == Boolean.TRUE) {
buf.put((byte) 'T');
} else {
buf.put((byte) 'F');
}
} else {
buf.put((byte) '?');
}
break;
// case MEMO:
//
// break;
default:
throw new DBFException("Unknown field type " + this.fieldArray[j].getDataType());
}
}
// see if there is an end-of-data byte at the end of the file.
long length = raf.length();
raf.seek(length - 1);
byte lastByte = raf.readByte();
if (lastByte == END_OF_DATA) {
// over-write the END_OF_DATA byte to append new records.
raf.seek(length - 1);
} else {
raf.seek(length);
}
//raf.seek(raf.length());
raf.write(buf.array());
//raf.seek(raf.length());
} catch (IOException e) {
throw new DBFException(e.getMessage());
}
}
public final void write() throws DBFException {
try (RandomAccessFile raf = new RandomAccessFile(this.fileName, "rw")) {
if (!recordData.isEmpty()) {
for (int i = 0; i < recordData.size(); i++) {
Object[] t_values = (Object[]) recordData.get(i);
writeRecord(raf, t_values);
}
raf.writeByte(END_OF_DATA);
recordData.clear();
}
// update the file header
writeHeader(raf);
raf.close();
isDirty = false;
} catch (IOException e) {
throw new DBFException(e.getMessage());
}
}
// HEADER FILE
static final byte SIG_DBASE_III = (byte) 0x03;
/* DBF structure start here */
byte signature; /* 0 */
byte year; /* 1 */
byte month; /* 2 */
byte day; /* 3 */
int numberOfRecords; /* 4-7 */
short headerLength; /* 8-9 */
short recordLength; /* 10-11 */
short reserv1; /* 12-13 */
byte incompleteTransaction; /* 14 */
byte encryptionFlag; /* 15 */
int freeRecordThread; /* 16-19 */
int reserv2; /* 20-23 */
int reserv3; /* 24-27 */
byte mdxFlag; /* 28 */
byte languageDriver; /* 29 */
short reserv4; /* 30-31 */
DBFField[] fieldArray; /* each 32 bytes */
byte terminator1; /* n+1 */
/* DBF structure ends here */
void readHeader() throws IOException {
try (DataInputStream dataInput = new DataInputStream(new BufferedInputStream(new FileInputStream(fileName)))) {
signature = dataInput.readByte(); /* 0 */
year = dataInput.readByte(); /* 1 */
month = dataInput.readByte(); /* 2 */
day = dataInput.readByte(); /* 3 */
numberOfRecords = Utils.readLittleEndianInt(dataInput); /* 4-7 */
headerLength = Utils.readLittleEndianShort(dataInput); /* 8-9 */
recordLength = Utils.readLittleEndianShort(dataInput); /* 10-11 */
reserv1 = Utils.readLittleEndianShort(dataInput); /* 12-13 */
incompleteTransaction = dataInput.readByte(); /* 14 */
encryptionFlag = dataInput.readByte(); /* 15 */
freeRecordThread = Utils.readLittleEndianInt(dataInput); /* 16-19 */
reserv2 = dataInput.readInt(); /* 20-23 */
reserv3 = dataInput.readInt(); /* 24-27 */
mdxFlag = dataInput.readByte(); /* 28 */
languageDriver = dataInput.readByte(); /* 29 */
reserv4 = Utils.readLittleEndianShort(dataInput); /* 30-31 */
ArrayList al_fields = new ArrayList();
DBFField field = DBFField.createField(dataInput); /* 32 each */
while (field != null) {
al_fields.add(field);
field = DBFField.createField(dataInput);
}
fieldArray = new DBFField[al_fields.size()];
for (int i = 0; i < fieldArray.length; i++) {
fieldArray[i] = (DBFField) al_fields.get(i);
}
} // try with resource auto closes
}
void writeHeader(RandomAccessFile raf) throws IOException {
//DataOutputStream dataOutput = new DataOutputStream(new FileOutputStream(this.fileName));
raf.seek(0);
raf.writeByte(signature); /* 0 */
GregorianCalendar calendar = new GregorianCalendar();
year = (byte) (calendar.get(Calendar.YEAR) - 1900);
month = (byte) (calendar.get(Calendar.MONTH) + 1);
day = (byte) (calendar.get(Calendar.DAY_OF_MONTH));
raf.writeByte(year); /* 1 */
raf.writeByte(month); /* 2 */
raf.writeByte(day); /* 3 */
raf.writeInt(Utils.littleEndian(numberOfRecords)); /* 4-7 */
headerLength = findHeaderLength();
raf.writeShort(Utils.littleEndian(headerLength)); /* 8-9 */
recordLength = findRecordLength();
raf.writeShort(Utils.littleEndian(recordLength)); /* 10-11 */
raf.writeShort(Utils.littleEndian(reserv1)); /* 12-13 */
raf.writeByte(incompleteTransaction); /* 14 */
raf.writeByte(encryptionFlag); /* 15 */
raf.writeInt(Utils.littleEndian(freeRecordThread));/* 16-19 */
raf.writeInt(Utils.littleEndian(reserv2)); /* 20-23 */
raf.writeInt(Utils.littleEndian(reserv3)); /* 24-27 */
raf.writeByte(mdxFlag); /* 28 */
raf.writeByte(languageDriver); /* 29 */
raf.writeShort(Utils.littleEndian(reserv4)); /* 30-31 */
if (fieldArray != null) {
for (int i = 0; i < fieldArray.length; i++) {
fieldArray[i].write(raf);
}
}
raf.writeByte(terminator1); /* n+1 */
//raf.flush();
//raf.close();
}
private short findHeaderLength() {
int nfields = fieldArray == null ? 0 : fieldArray.length;
return (short) (1
+ 3
+ 4
+ 2
+ 2
+ 2
+ 1
+ 1
+ 4
+ 4
+ 4
+ 1
+ 1
+ 2
+ (32 * nfields)
+ 1);
}
private short findRecordLength() {
if (fieldArray == null) {
return 0;
}
int recordLength = 0;
for (int i = 0; i < fieldArray.length; i++) {
recordLength += fieldArray[i].getFieldLength();
}
return (short) (recordLength + 1);
}
}