/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.data.shapefile.dbf;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.geotools.data.shapefile.StreamLogging;
import org.geotools.resources.NIOUtilities;
/**
* A DbaseFileReader is used to read a dbase III format file. The general use of
* this class is: <CODE><PRE>
* DbaseFileHeader header = ...
* WritableFileChannel out = new FileOutputStream("thefile.dbf").getChannel();
* DbaseFileWriter w = new DbaseFileWriter(header,out);
* while ( moreRecords ) {
* w.write( getMyRecord() );
* }
* w.close();
* </PRE></CODE> You must supply the <CODE>moreRecords</CODE> and
* <CODE>getMyRecord()</CODE> logic...
*
* @author Ian Schneider
*
* @source $URL$
*/
public class DbaseFileWriter {
private DbaseFileHeader header;
private DbaseFileWriter.FieldFormatter formatter;
WritableByteChannel channel;
private ByteBuffer buffer;
/**
* The null values to use for each column. This will be accessed only when
* null values are actually encountered, but it is allocated in the ctor
* to save time and memory.
*/
private final byte[][] nullValues;
private StreamLogging streamLogger = new StreamLogging("Dbase File Writer");
private Charset charset;
private TimeZone timeZone;
/**
* Create a DbaseFileWriter using the specified header and writing to the
* given channel.
*
* @param header
* The DbaseFileHeader to write.
* @param out
* The Channel to write to.
* @throws IOException
* If errors occur while initializing.
*/
public DbaseFileWriter(DbaseFileHeader header, WritableByteChannel out)
throws IOException {
this(header, out, null, null);
}
/**
* Create a DbaseFileWriter using the specified header and writing to the
* given channel.
*
* @param header
* The DbaseFileHeader to write.
* @param out
* The Channel to write to.
* @throws IOException
* If errors occur while initializing.
*/
public DbaseFileWriter(DbaseFileHeader header, WritableByteChannel out, Charset charset)
throws IOException {
this(header, out, charset, null);
}
/**
* Create a DbaseFileWriter using the specified header and writing to the
* given channel.
*
* @param header
* The DbaseFileHeader to write.
* @param out
* The Channel to write to.
* @param charset The charset the dbf is (will be) encoded in
* @throws IOException
* If errors occur while initializing.
*/
public DbaseFileWriter(DbaseFileHeader header, WritableByteChannel out, Charset charset, TimeZone timeZone)
throws IOException {
header.writeHeader(out);
this.header = header;
this.channel = out;
this.charset = charset == null ? Charset.defaultCharset() : charset;
this.timeZone = timeZone == null ? TimeZone.getDefault() : timeZone;
this.formatter = new DbaseFileWriter.FieldFormatter(this.charset, this.timeZone);
streamLogger.open();
// As the 'shapelib' osgeo project does, we use specific values for
// null cells. We can set up these values for each column once, in
// the constructor, to save time and memory.
nullValues = new byte[header.getNumFields()][];
for (int i = 0; i < nullValues.length; i++) {
char nullChar;
switch (header.getFieldType(i)) {
case 'C':
case 'c':
case 'M':
case 'G':
nullChar = '\0';
break;
case 'L':
case 'l':
nullChar = '?';
break;
case 'N':
case 'n':
case 'F':
case 'f':
nullChar = '*';
break;
case 'D':
case 'd':
nullChar = '0';
break;
case '@':
// becomes day 0 time 0.
nullChar = '\0';
break;
default:
// catches at least 'D', and 'd'
nullChar = '0';
break;
}
nullValues[i] = new byte[header.getFieldLength(i)];
Arrays.fill(nullValues[i], (byte)nullChar);
}
buffer = NIOUtilities.allocate(header.getRecordLength());
}
private void write() throws IOException {
buffer.position(0);
int r = buffer.remaining();
while ((r -= channel.write(buffer)) > 0) {
; // do nothing
}
}
/**
* Write a single dbase record.
*
* @param record
* The entries to write.
* @throws IOException
* If IO error occurs.
* @throws DbaseFileException
* If the entry doesn't comply to the header.
*/
public void write(Object[] record) throws IOException, DbaseFileException {
if (record.length != header.getNumFields()) {
throw new DbaseFileException("Wrong number of fields "
+ record.length + " expected " + header.getNumFields());
}
buffer.position(0);
// put the 'not-deleted' marker
buffer.put((byte) ' ');
byte[] bytes;
for (int i = 0; i < header.getNumFields(); i++) {
// convert this column to bytes
if (record[i] == null) {
bytes = nullValues[i];
} else {
bytes = fieldBytes(record[i], i);
// if the returned array is not the proper length
// write a null instead; this will only happen
// when the formatter handles a value improperly.
if (bytes.length != nullValues[i].length) {
bytes = nullValues[i];
}
}
buffer.put(bytes);
}
write();
}
/**
* Called to convert the given object to bytes.
*
* @param obj
* The value to convert; never null.
* @param col
* The column this object will be encoded into.
* @return The bytes of a string representation of the given object in the
* current character encoding.
* @throws UnsupportedEncodingException Thrown if the current charset is unsupported.
*/
private byte[] fieldBytes(Object obj, final int col)
throws UnsupportedEncodingException {
String o;
final int fieldLen = header.getFieldLength(col);
switch (header.getFieldType(col)) {
case 'C':
case 'c':
o = formatter.getFieldString(fieldLen, obj.toString());
break;
case 'L':
case 'l':
if (obj instanceof Boolean) {
o = ((Boolean)obj).booleanValue() ? "T" : "F";
} else {
o = "?";
}
break;
case 'M':
case 'G':
o = formatter.getFieldString(fieldLen, obj.toString());
break;
case 'N':
case 'n':
// int?
if (header.getFieldDecimalCount(col) == 0) {
o = formatter.getFieldString(fieldLen, 0, (Number)obj);
break;
}
case 'F':
case 'f':
o = formatter.getFieldString(fieldLen,
header.getFieldDecimalCount(col),
(Number)obj);
break;
case 'D':
case 'd':
if (obj instanceof java.util.Calendar) {
o = formatter.getFieldString(((Calendar) obj).getTime());
} else {
o = formatter.getFieldString((Date) obj);
}
break;
case '@':
o = formatter.getFieldStringDateTime((Date)obj);
if (Boolean.getBoolean("org.geotools.shapefile.datetime")) {
// Adding the charset to getBytes causes the output to
// get altered for the '@: Timestamp' field.
// And using String.getBytes returns a different array
// in 64-bit platforms so we get chars and cast to byte
// one element at a time.
char[] carr = o.toCharArray();
byte[] barr = new byte[carr.length];
for (int i = 0; i < carr.length; i++) {
barr[i] = (byte)carr[i];
}
return barr;
}
break;
default:
throw new RuntimeException("Unknown type "
+ header.getFieldType(col));
}
// convert the string to bytes with the given charset.
return o.getBytes(charset.name());
}
/**
* Release resources associated with this writer. <B>Highly recommended</B>
*
* @throws IOException
* If errors occur.
*/
public void close() throws IOException {
// IANS - GEOT 193, bogus 0x00 written. According to dbf spec, optional
// eof 0x1a marker is, well, optional. Since the original code wrote a
// 0x00 (which is wrong anyway) lets just do away with this :)
// - produced dbf works in OpenOffice and ArcExplorer java, so it must
// be okay.
// buffer.position(0);
// buffer.put((byte) 0).position(0).limit(1);
// write();
if (channel != null && channel.isOpen()) {
channel.close();
streamLogger.close();
}
if(buffer != null) {
NIOUtilities.clean(buffer, false);
}
buffer = null;
channel = null;
formatter = null;
}
/** Utility for formatting Dbase fields. */
public static class FieldFormatter {
private StringBuffer buffer = new StringBuffer(255);
private NumberFormat numFormat = NumberFormat.getNumberInstance(Locale.US);
private Calendar calendar;
private final long MILLISECS_PER_DAY = 24*60*60*1000;
private String emptyString;
private static final int MAXCHARS = 255;
private Charset charset;
public FieldFormatter(Charset charset, TimeZone timeZone) {
// Avoid grouping on number format
numFormat.setGroupingUsed(false);
// build a 255 white spaces string
StringBuffer sb = new StringBuffer(MAXCHARS);
sb.setLength(MAXCHARS);
for (int i = 0; i < MAXCHARS; i++) {
sb.setCharAt(i, ' ');
}
this.charset = charset;
this.calendar = Calendar.getInstance(timeZone, Locale.US);
emptyString = sb.toString();
}
public String getFieldString(int size, String s) {
try {
buffer.replace(0, size, emptyString);
buffer.setLength(size);
// international characters must be accounted for so size != length.
int maxSize = size;
if (s != null) {
buffer.replace(0, size, s);
int currentBytes = s.substring(0, Math.min(size, s.length()))
.getBytes(charset.name()).length;
if (currentBytes > size) {
char[] c = new char[1];
for (int index = size - 1; currentBytes > size; index--) {
c[0] = buffer.charAt(index);
String string = new String(c);
buffer.deleteCharAt(index);
currentBytes -= string.getBytes().length;
maxSize--;
}
} else {
if (s.length() < size) {
maxSize = size - (currentBytes - s.length());
for (int i = s.length(); i < size; i++) {
buffer.append(' ');
}
}
}
}
buffer.setLength(maxSize);
return buffer.toString();
} catch(UnsupportedEncodingException e) {
throw new RuntimeException("This error should never occurr", e);
}
}
public String getFieldString(Date d) {
if (d != null) {
buffer.delete(0, buffer.length());
calendar.setTime(d);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // returns 0
// based month?
int day = calendar.get(Calendar.DAY_OF_MONTH);
if (year < 1000) {
if (year >= 100) {
buffer.append("0");
} else if (year >= 10) {
buffer.append("00");
} else {
buffer.append("000");
}
}
buffer.append(year);
if (month < 10) {
buffer.append("0");
}
buffer.append(month);
if (day < 10) {
buffer.append("0");
}
buffer.append(day);
} else {
buffer.setLength(8);
buffer.replace(0, 8, emptyString);
}
buffer.setLength(8);
return buffer.toString();
}
public String getFieldStringDateTime(Date d) {
// Sanity check
if (d == null) return null;
final long difference = d.getTime() - DbaseFileHeader.MILLIS_SINCE_4713;
final int days = (int) (difference / MILLISECS_PER_DAY);
final int time = (int) (difference % MILLISECS_PER_DAY);
try{
ByteArrayOutputStream o_bytes = new ByteArrayOutputStream();
DataOutputStream o_stream;
o_stream = new DataOutputStream(new BufferedOutputStream(o_bytes));
o_stream.writeInt(days);
o_stream.writeInt(time);
o_stream.flush();
byte[] bytes = o_bytes.toByteArray();
// Cast the byte values to char as a workaround for erroneous byte
// array retrieval in 64-bit machines
char[] out = {
// Days, after reverse.
(char) bytes[3], (char) bytes[2],(char) bytes[1], (char) bytes[0],
// Time in millis, after reverse.
(char) bytes[7], (char) bytes[6], (char) bytes[5], (char) bytes[4],
};
return new String(out);
}catch(IOException e){
// This is always just a int serialization,
// there is no way to recover from here.
return null;
}
}
public String getFieldString(int size, int decimalPlaces, Number n) {
buffer.delete(0, buffer.length());
if (n != null) {
numFormat.setMaximumFractionDigits(decimalPlaces);
numFormat.setMinimumFractionDigits(decimalPlaces);
numFormat.format(n, buffer, new FieldPosition(
NumberFormat.INTEGER_FIELD));
}
int diff = size - buffer.length();
if (diff >= 0) {
while (diff-- > 0) {
buffer.insert(0, ' ');
}
} else {
buffer.setLength(size);
}
return buffer.toString();
}
}
}