import java.io.*;
import java.util.*;
import java.text.*;
import java.nio.charset.*;
import java.math.BigDecimal;
public class SmDBF implements Closeable {
private final SimpleDateFormat DFormat = new SimpleDateFormat("yyyyMMdd");
private final SimpleDateFormat TFormat = new SimpleDateFormat("yyyyMMddHHmmss");
private Charset charset = StandardCharsets.ISO_8859_1;
private File file;
private RandomAccessFile raf = null;
private byte[] emptyRec = null;
private String[] name;
private char[] type;
private int[] length, lengthDecimal, offset;
private int headerLength, recordLength, recCount = 0, currentRec = 0;
private boolean modified = false, bof = true, eof = true;
public SmDBF(File file) throws IOException {
this.file = file;
if (!file.isFile()) throw new IOException("file does not exist: " + file.getName());
raf = new RandomAccessFile(file, "rw");
try {
if (raf.length() < 65) corrupted();
recCount = readRecCount();
setCurrentRec(1);
raf.seek(8);
headerLength = read() + (read() << 8);
recordLength = read() + (read() << 8);
int fc = (headerLength - 33) / 32;
raf.seek((fc + 1) * 32);
if (raf.length() < raf.getFilePointer() + 1 || read() != 13 || raf.length() < recCount * recordLength + headerLength) corrupted();
name = new String[fc];
type = new char[name.length];
length = new int[name.length];
lengthDecimal = new int[name.length];
offset = new int[name.length];
int sm = 1;
for (int i = 0; i < name.length; i++) {
raf.seek(i * 32 + 32);
byte[] bytes = new byte[11];
read(bytes);
int z = 0;
while (z < bytes.length && bytes[z] != 0) z++;
name[i] = new String(bytes, 0, z).trim().toUpperCase();
type[i] = Character.toUpperCase((char)read());
raf.skipBytes(4);
length[i] = read();
if (type[i] == 'C') { length[i] += read() << 8; lengthDecimal[i] = 0; }
else lengthDecimal[i] = read();
offset[i] = sm;
sm += length[i];
}
if (sm != recordLength) corrupted();
checkStructure();
} catch (Exception e) { close(); throw e; }
}
private void corrupted() throws IOException { throw new IOException("file is not valid or is corrupted"); }
public SmDBF(File file, Object[][] fields) throws IOException {
if (fields.length == 0) throw new IOException("array is empty");
this.file = file;
name = new String[fields.length];
type = new char[fields.length];
length = new int[fields.length];
lengthDecimal = new int[fields.length];
offset = new int[fields.length];
headerLength = (fields.length + 1) * 32 + 1;
recordLength = 1;
for (int i = 0; i < fields.length; i++) {
name[i] = ((String)fields[i][0]).trim().toUpperCase();
if (name[i].length() == 0 || name[i].length() > 10) throw new IOException("invalid field length: " + name[i]);
if (!checkFieldName(name[i])) throw new IOException("invalid field name: " + name[i]);
type[i] = Character.toUpperCase((char)fields[i][1]);
length[i] = (int)fields[i][2];
lengthDecimal[i] = (int)fields[i][3];
offset[i] = recordLength;
recordLength += length[i];
}
checkStructure();
raf = new RandomAccessFile(file, "rw");
try {
raf.setLength(0L);
byte[] bytes = new byte[32];
Arrays.fill(bytes, (byte)0);
bytes[0] = 3;
byte[] dt = getDate();
bytes[1] = dt[0];
bytes[2] = dt[1];
bytes[3] = dt[2];
bytes[8] = (byte)(0xff & headerLength);
bytes[9] = (byte)(0xff & headerLength >> 8);
bytes[10] = (byte)(0xff & recordLength);
bytes[11] = (byte)(0xff & recordLength >> 8);
raf.write(bytes);
for (int r = 0; r < name.length; r++) {
Arrays.fill(bytes, (byte)0);
String s = name[r];
for (int i = 0; i < s.length() && i < 10; i++) bytes[i] = (byte)s.charAt(i);
bytes[11] = (byte)(0xff & type[r]);
bytes[16] = (byte)(0xff & length[r]);
if (type[r] == 'C') bytes[17] = (byte)(0xff & length[r] >> 8);
else bytes[17] = (byte)(0xff & lengthDecimal[r]);
raf.write(bytes);
}
raf.write(13);
raf.write(26);
} catch (Exception e) { close(); throw e; }
}
@Override
public void close() throws IOException {
if (raf != null) {
if (modified) try { raf.seek(1); raf.write(getDate()); } catch (Exception e) { }
raf.close();
raf = null;
}
}
private void checkStructure() throws IOException {
if (headerLength > 0xFFFF || recordLength > 0xFFFF) throw new IOException("invalid header length");
for (int i = 0; i < name.length; i++) {
for (int j = i + 1; j < name.length; j++) if(name[i].equals(name[j])) throw new IOException("field is duplicate: "+name[i]);
if (type[i] != 'C' && type[i] != 'N' && type[i] != 'F' && type[i] != 'L' && type[i] != 'D' && type[i] != 'T' && type[i] != 'M') throw new IOException("field type should be D,T,C,M,N,F,L: " + name[i]);
if (type[i] == 'L' && length[i] != 1) throw new IOException("field length should be 1: " + name[i]);
if (type[i] == 'D' && length[i] != 8) throw new IOException("field length should be 8: " + name[i]);
if (type[i] == 'T' && length[i] != 14) throw new IOException("field length should be 14: " + name[i]);
if (type[i] == 'M' && length[i] != 10) throw new IOException("field length should be 10: " + name[i]);
if (length[i] < 0 || lengthDecimal[i] < 0) throw new IOException("length or lengthDecimal is negative " + name[i]);
if (length[i] == 0) throw new IOException("field length should be greater than 0: " + name[i]);
if (type[i] != 'N' && type[i] != 'F' && lengthDecimal[i] > 0) throw new IOException("lengthDecimal should be 0: " + name[i]);
if ((type[i] == 'N' || type[i] == 'F') && length[i] > 0xFF) throw new IOException("invalid field length: " + name[i]);
if ((type[i] == 'N' || type[i] == 'F') && lengthDecimal[i] > 0 && lengthDecimal[i] > length[i] - 2) throw new IOException("invalid decimal length: " + name[i]);
}
}
private byte[] getDate() {
byte[] bytes = new byte[3];
Calendar calendar = Calendar.getInstance();
bytes[0] = (byte)(calendar.get(Calendar.YEAR) - 1900);
bytes[1] = (byte)(calendar.get(Calendar.MONTH) + 1);
bytes[2] = (byte)calendar.get(Calendar.DAY_OF_MONTH);
return bytes;
}
public void append() throws IOException {
if (emptyRec == null) {
emptyRec = new byte[recordLength];
Arrays.fill(emptyRec, (byte)32);
}
append(emptyRec);
}
public void append(byte[] bytes) throws IOException {
if (bytes.length != recordLength) throw new IOException("bad array length");
recCount++; //recCount = readRecCount() + 1;
currentRec = recCount;
writeRecord(bytes);
raf.write(26);
writeRecCount(recCount);
setCurrentRec(recCount);
}
private void writeRecCount(int rc) throws IOException {
raf.seek(4);
for (int i = 0; i < 4; i++) raf.write((byte)(0xff & rc >>> i * 8));
if (!modified) modified = true;
}
public void zap() throws IOException {
raf.seek(headerLength);
raf.write(26);
raf.setLength(raf.getFilePointer());
recCount = 0;
writeRecCount(0);
setCurrentRec(0);
}
public void pack() throws IOException {
byte[] bytes = new byte[recordLength];
bytes[0] = 32;
currentRec = 0;
int i = 0;
//recCount = readRecCount();
while (i < recCount) {
raf.seek(i * recordLength + headerLength);
int d = read();
i++;
if (d != 42) {
currentRec++;
if (i > currentRec) {
read(bytes, 1, recordLength - 1);
writeRecord(bytes);
}
}
}
if (recCount > currentRec) {
raf.seek(currentRec * recordLength + headerLength);
raf.write(26);
raf.setLength(raf.getFilePointer());
recCount = currentRec;
writeRecCount(recCount);
}
setCurrentRec(currentRec);
}
public boolean bof() { return bof; }
public boolean eof() { return eof; }
public void skip() { setCurrentRec(currentRec + 1); }
public void skip(int n) { setCurrentRec(currentRec + n); }
public void goTop() { setCurrentRec(1); }
public void goEnd() { setCurrentRec(recCount); }
public void goToRec(int n) { setCurrentRec(n); }
private void setCurrentRec(int n) {
if (recCount == 0) { currentRec = 0; bof = eof = true; }
else if (n < 1) { currentRec = 0; bof = true; eof = false; }
else if (n > recCount) { currentRec = recCount + 1; bof = false; eof = true; }
else { currentRec = n; bof = eof = false; }
}
public int getRecCount() { return recCount; }
public int getCurrentRec() { return currentRec; }
public int getFieldCount() { return name.length; }
public int getHeaderLength() { return headerLength; }
public int getRecordLength() { return recordLength; }
public boolean isModified() { return modified; }
public boolean isClosed() { return raf == null; }
public int getFieldNumber(String s) {
s = s.trim().toUpperCase();
for (int i = 0; i < name.length; i++) if (s.equals(name[i])) return i + 1;
throw new IllegalArgumentException("field not found: " + s);
}
public String getFieldName(int n) { checkFieldNumber(n); return name[n - 1]; }
public char getFieldType(int n) { checkFieldNumber(n); return type[n - 1]; }
public int getFieldLength(int n) { checkFieldNumber(n); return length[n - 1]; }
public int getFieldLengthDecimal(int n) { checkFieldNumber(n); return lengthDecimal[n - 1]; }
private void checkFieldNumber(int n) {
if (n < 1 || n > name.length) throw new IndexOutOfBoundsException("field number is out of bounds: " + n);
}
private void checkCurrentRec() {
if (currentRec < 1 || currentRec > recCount) throw new IndexOutOfBoundsException("current record is out of bounds: " + currentRec);
}
public void set(String s, Object obj) throws IOException { set(getFieldNumber(s), obj); }
public void set(int n, Object obj) throws IOException {
checkFieldNumber(n);
checkCurrentRec();
byte[] bytes;
int i = n - 1;
if (obj == null) {
bytes = new byte[length[i]];
Arrays.fill(bytes, (byte)32);
} else if (type[i] == 'C' && obj instanceof String) {
bytes = new byte[length[i]];
byte[] byff = rtrim((String)obj).getBytes(charset);
if (byff.length > length[i]) throw new IOException("field overflow: " + name[i]);
for (int j = 0; j < bytes.length; j++)
bytes[j] = j < byff.length ? byff[j] : (byte)32;
} else if (type[i] == 'L' && obj instanceof Boolean) {
bytes = new byte[] {(byte)(((Boolean)obj).booleanValue() ? 84 : 70)};
} else if ((type[i] == 'D' || type[i] == 'T') && obj instanceof Date) {
Date date = (Date)obj;
bytes = (type[i] == 'D' ? DFormat : TFormat).format(date).getBytes();
} else if ((type[i] == 'N' || type[i] == 'F' || type[i] == 'M') && obj instanceof BigDecimal) {
BigDecimal v = (BigDecimal)obj;
if (lengthDecimal[i] == 0) {
String f = "%"+length[i]+"d";
bytes = String.format(f, v.toBigInteger()).getBytes();
} else {
String f = "%"+length[i]+"."+lengthDecimal[i]+"f";
Locale lc = new Locale.Builder().build();
bytes = String.format(lc, f, v).getBytes();
}
if (bytes.length > length[i]) throw new IOException("field overflow: " + name[i]);
} else {
String s = type[i] == 'C' ? "String" : type[i] == 'L' ? "Boolean" :
(type[i] == 'D' || type[i] == 'T') ? "Date" :
(type[i] == 'N' || type[i] == 'F' || type[i] == 'M') ? "BigDecimal" : "";
throw new IOException("demands " + s + " or null: " + name[i]);
}
raf.seek((currentRec - 1) * recordLength + headerLength + offset[i]);
raf.write(bytes);
if (!modified) modified = true;
}
public Object get(String s) throws IOException { return get(getFieldNumber(s)); }
public Object get(int n) throws IOException {
checkFieldNumber(n);
checkCurrentRec();
int i = n - 1;
byte[] bytes = new byte[length[i]];
raf.seek((currentRec - 1) * recordLength + headerLength + offset[i]);
read(bytes);
if (type[i] == 'C') {
return new String(bytes, charset);
} else if (type[i] == 'L') {
byte b = bytes[0];
if (b == 'T' || b == 't' || b == 'Y' || b == 'y') return Boolean.valueOf(true);
if (b == 'F' || b == 'f' || b == 'N' || b == 'n') return Boolean.valueOf(false);
} else if (type[i] == 'D' || type[i] == 'T') {
try { return (type[i] == 'D' ? DFormat : TFormat).parse(new String(bytes)); } catch (Exception e) { }
} else if (type[i] == 'N' || type[i] == 'F' || type[i] == 'M') {
try { return new BigDecimal(new String(bytes).trim()); } catch (Exception e) { }
}
return null;
}
public void delete() throws IOException { setDeleted(true); }
public void recall() throws IOException { setDeleted(false); }
public void setDeleted(boolean b) throws IOException {
checkCurrentRec();
raf.seek((currentRec - 1) * recordLength + headerLength);
raf.write((byte)(b ? 42 : 32));
if (!modified) modified = true;
}
public boolean isDeleted() throws IOException {
checkCurrentRec();
raf.seek((currentRec - 1) * recordLength + headerLength);
return read() == 42;
}
public void writeRecord(byte[] bytes) throws IOException {
checkCurrentRec();
if (bytes.length != recordLength) throw new IOException("bad record length");
raf.seek((currentRec - 1) * recordLength + headerLength);
raf.write(bytes);
if (!modified) modified = true;
}
public void readRecord(byte[] bytes) throws IOException {
checkCurrentRec();
if (bytes.length != recordLength) throw new IOException("bad record length");
raf.seek((currentRec - 1) * recordLength + headerLength);
read(bytes);
}
public int readRecCount() throws IOException {
raf.seek(4);
int rc = 0;
for (int i = 0; i < 4; i++) rc += read() << i * 8;
return rc;
}
private void read(byte[] b) throws IOException {
if (b.length != raf.read(b)) throw new EOFException();
}
private void read(byte[] b, int off, int len) throws IOException {
if (len != raf.read(b, off, len)) throw new EOFException();
}
private int read() throws IOException {
int ch = raf.read();
if (ch == -1) throw new EOFException();
return ch;
}
public void setCharset(String s) { charset = Charset.forName(s); }
public Object[][] getStructure() {
Object[][] fields = new Object[name.length][];
for (int i = 0; i < name.length; i++) {
fields[i] = new Object[] { name[i], type[i], length[i], lengthDecimal[i] };
}
return fields;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("File: " + file.getName() + ", ");
for (int i = 0; i < name.length; i++) {
if (i > 0) sb.append(", ");
sb.append("{" + name[i] + ", " + type[i] + ", " + length[i] + ", " + lengthDecimal[i] + "}");
}
sb.append(", Header: " + headerLength + "b, Record: " + recordLength + "b");
sb.append(", Fields: " + name.length + ", Records: " + recCount);
return sb.toString();
}
public static boolean checkFieldName(String name) {
return name.matches("([A-Z]|_+[A-Z0-9])[A-Z0-9_]*");
}
public static String typeToString(char t) {
return t == 'C' ? "Character" : t == 'L' ? "Logical" : t == 'M' ? "Memo" :
t == 'D' ? "Date" : t == 'T' ? "DateTime" : t == 'N' ? "Numeric" : t == 'F' ? "Float" : "";
}
public static String rtrim(String s) {
int i = s.length() - 1;
while (i >= 0 && s.charAt(i) <= ' ') i--;
return s.substring(0, i + 1);
}
public static String ltrim(String s) {
int i = 0;
while (i < s.length() && s.charAt(i) <= ' ') i++;
return s.substring(i);
}
} // End of class SmDBF