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