/* * 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.MappedByteBuffer; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.text.FieldPosition; import java.text.NumberFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; 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: * http://svn.geotools.org/geotools/trunk/gt/modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/dbf/DbaseFileWriter.java $ */ public class DbaseFileWriter { private DbaseFileHeader header; private DbaseFileWriter.FieldFormatter formatter; WritableByteChannel channel; private ByteBuffer buffer; private final Number NULL_NUMBER = new Integer(0); private final String NULL_STRING = ""; private final Date NULL_DATE = new Date(); private StreamLogging streamLogger = new StreamLogging("Dbase File Writer"); private Charset charset; /** * 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); } /** * 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) throws IOException { header.writeHeader(out); this.header = header; this.channel = out; this.charset = charset == null ? Charset.defaultCharset() : charset; this.formatter = new DbaseFileWriter.FieldFormatter(this.charset); streamLogger.open(); init(); } private void init() throws IOException { buffer = ByteBuffer.allocateDirect(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) ' '); for (int i = 0; i < header.getNumFields(); i++) { String fieldString = fieldString(record[i], i); if (header.getFieldLength(i) != fieldString.getBytes(charset.name()).length) { buffer.put(new byte[header.getFieldLength(i)]); } else { if ( Boolean.getBoolean("org.geotools.shapefile.datetime") && header.getFieldType(i) == '@') { // Adding the charset to getBytes causes the output to // get altered for the '@: Timestamp' field. // And using getBytes returns a different array in 64-bit platforms // so we expect chars and cast to byte just before writing. for (char c: fieldString.toCharArray()){ buffer.put((byte) c); } }else{ buffer.put(fieldString.getBytes(charset.name())); } } } write(); } private String fieldString(Object obj, final int col) { String o; final int fieldLen = header.getFieldLength(col); switch (header.getFieldType(col)) { case 'C': case 'c': o = formatter.getFieldString(fieldLen, obj == null ? NULL_STRING : obj.toString()); break; case 'L': case 'l': o = (obj == null ? " " : obj == Boolean.TRUE ? "T" : "F"); // o = formatter.getFieldString( // fieldLen, // o // ); break; case 'M': case 'G': o = formatter.getFieldString(fieldLen, obj == null ? NULL_STRING : obj.toString()); break; case 'N': case 'n': // int? if (header.getFieldDecimalCount(col) == 0) { o = formatter.getFieldString(fieldLen, 0, (Number) (obj == null ? NULL_NUMBER : obj)); break; } case 'F': case 'f': o = formatter.getFieldString(fieldLen, header .getFieldDecimalCount(col), (Number) (obj == null ? NULL_NUMBER : obj)); break; case 'D': case 'd': o = formatter .getFieldString((Date) (obj == null ? NULL_DATE : obj)); break; case '@': o = formatter.getFieldStringDateTime( (Date) (obj == null ? new Date(NULL_DATE.getTime()): obj) ); break; default: throw new RuntimeException("Unknown type " + header.getFieldType(col)); } return o; } /** * 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.isOpen()) { channel.close(); streamLogger.close(); } if (buffer instanceof MappedByteBuffer) { NIOUtilities.clean(buffer); } 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 = Calendar.getInstance(Locale.US); 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) { // 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; 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(); } } }