001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2002, Centre for Computational Geography
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * This file is based on an origional contained in the GISToolkit project:
018: * http://gistoolkit.sourceforge.net/
019: */
020: package org.geotools.data.shapefile.dbf;
021:
022: import java.io.EOFException;
023: import java.io.IOException;
024: import java.nio.ByteBuffer;
025: import java.nio.ByteOrder;
026: import java.nio.channels.ReadableByteChannel;
027: import java.nio.channels.WritableByteChannel;
028: import java.util.ArrayList;
029: import java.util.Calendar;
030: import java.util.Date;
031: import java.util.List;
032: import java.util.logging.Level;
033: import java.util.logging.Logger;
034:
035: import org.geotools.resources.NIOUtilities;
036:
037: /** Class to represent the header of a Dbase III file.
038: * Creation date: (5/15/2001 5:15:30 PM)
039: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/dbf/DbaseFileHeader.java $
040: */
041: public class DbaseFileHeader {
042: // Constant for the size of a record
043: private static final int FILE_DESCRIPTOR_SIZE = 32;
044:
045: // type of the file, must be 03h
046: private static final byte MAGIC = 0x03;
047:
048: private static final int MINIMUM_HEADER = 33;
049:
050: // Date the file was last updated.
051: private Date date = new Date();
052:
053: private int recordCnt = 0;
054:
055: private int fieldCnt = 0;
056:
057: // set this to a default length of 1, which is enough for one "space"
058: // character which signifies an empty record
059: private int recordLength = 1;
060:
061: // set this to a flagged value so if no fields are added before the write,
062: // we know to adjust the headerLength to MINIMUM_HEADER
063: private int headerLength = -1;
064:
065: private int largestFieldSize = 0;
066:
067: private Logger logger = org.geotools.util.logging.Logging
068: .getLogger("org.geotools.data.shapefile");
069:
070: /**
071: * Class for holding the information assicated with a record.
072: */
073: class DbaseField {
074:
075: // Field Name
076: String fieldName;
077:
078: // Field Type (C N L D or M)
079: char fieldType;
080:
081: // Field Data Address offset from the start of the record.
082: int fieldDataAddress;
083:
084: // Length of the data in bytes
085: int fieldLength;
086:
087: // Field decimal count in Binary, indicating where the decimal is
088: int decimalCount;
089:
090: }
091:
092: // collection of header records.
093: // lets start out with a zero-length array, just in case
094: private DbaseField[] fields = new DbaseField[0];
095:
096: private void read(ByteBuffer buffer, ReadableByteChannel channel)
097: throws IOException {
098: while (buffer.remaining() > 0) {
099: if (channel.read(buffer) == -1) {
100: throw new EOFException("Premature end of file");
101: }
102: }
103: }
104:
105: /** Determine the most appropriate Java Class for representing the data in the
106: * field.
107: * <PRE>
108: * All packages are java.lang unless otherwise specified.
109: * C (Character) -> String
110: * N (Numeric) -> Integer or Double (depends on field's decimal count)
111: * F (Floating) -> Double
112: * L (Logical) -> Boolean
113: * D (Date) -> java.util.Date
114: * Unknown -> String
115: * </PRE>
116: * @param i The index of the field, from 0 to <CODE>getNumFields() - 1</CODE> .
117: * @return A Class which closely represents the dbase field type.
118: */
119: public Class getFieldClass(int i) {
120: Class typeClass = null;
121:
122: switch (fields[i].fieldType) {
123: case 'C':
124: typeClass = String.class;
125: break;
126:
127: case 'N':
128: if (fields[i].decimalCount == 0) {
129: if (fields[i].fieldLength < 10) {
130: typeClass = Integer.class;
131: } else {
132: typeClass = Long.class;
133: }
134: } else {
135: typeClass = Double.class;
136: }
137: break;
138:
139: case 'F':
140: typeClass = Double.class;
141: break;
142:
143: case 'L':
144: typeClass = Boolean.class;
145: break;
146:
147: case 'D':
148: typeClass = Date.class;
149: break;
150:
151: default:
152: typeClass = String.class;
153: break;
154: }
155:
156: return typeClass;
157: }
158:
159: /** Add a column to this DbaseFileHeader.
160: * The type is one of (C N L or D) character, number, logical(true/false), or date.
161: * The Field length is the total length in bytes reserved for this column.
162: * The decimal count only applies to numbers(N), and floating point values (F),
163: * and refers to the number of characters to reserve after the decimal point.
164: * <B>Don't expect miracles from this...</B>
165: * <PRE>
166: * Field Type MaxLength
167: * ---------- ---------
168: * C 254
169: * D 8
170: * F 20
171: * N 18
172: * </PRE>
173: * @param inFieldName The name of the new field, must be less than 10 characters or it
174: * gets truncated.
175: * @param inFieldType A character representing the dBase field, ( see above ).
176: * Case insensitive.
177: * @param inFieldLength The length of the field, in bytes ( see above )
178: * @param inDecimalCount For numeric fields, the number of decimal places to track.
179: * @throws DbaseFileException If the type is not recognized.
180: */
181: public void addColumn(String inFieldName, char inFieldType,
182: int inFieldLength, int inDecimalCount)
183: throws DbaseFileException {
184: if (inFieldLength <= 0) {
185: throw new DbaseFileException("field length <= 0");
186: }
187: if (fields == null) {
188: fields = new DbaseField[0];
189: }
190: int tempLength = 1; // the length is used for the offset, and there is a * for deleted as the first byte
191: DbaseField[] tempFieldDescriptors = new DbaseField[fields.length + 1];
192: for (int i = 0; i < fields.length; i++) {
193: fields[i].fieldDataAddress = tempLength;
194: tempLength = tempLength + fields[i].fieldLength;
195: tempFieldDescriptors[i] = fields[i];
196: }
197: tempFieldDescriptors[fields.length] = new DbaseField();
198: tempFieldDescriptors[fields.length].fieldLength = inFieldLength;
199: tempFieldDescriptors[fields.length].decimalCount = inDecimalCount;
200: tempFieldDescriptors[fields.length].fieldDataAddress = tempLength;
201:
202: // set the field name
203: String tempFieldName = inFieldName;
204: if (tempFieldName == null) {
205: tempFieldName = "NoName";
206: }
207: // Fix for GEOT-42, ArcExplorer will not handle field names > 10 chars
208: // Sorry folks.
209: if (tempFieldName.length() > 10) {
210: tempFieldName = tempFieldName.substring(0, 10);
211: if (logger.isLoggable(Level.WARNING)) {
212: logger
213: .warning("FieldName "
214: + inFieldName
215: + " is longer than 10 characters, truncating to "
216: + tempFieldName);
217: }
218: }
219: tempFieldDescriptors[fields.length].fieldName = tempFieldName;
220:
221: // the field type
222: if ((inFieldType == 'C') || (inFieldType == 'c')) {
223: tempFieldDescriptors[fields.length].fieldType = 'C';
224: if (inFieldLength > 254) {
225: if (logger.isLoggable(Level.FINE)) {
226: logger
227: .fine("Field Length for "
228: + inFieldName
229: + " set to "
230: + inFieldLength
231: + " Which is longer than 254, not consistent with dbase III");
232: }
233: }
234: } else if ((inFieldType == 'S') || (inFieldType == 's')) {
235: tempFieldDescriptors[fields.length].fieldType = 'C';
236: if (logger.isLoggable(Level.WARNING)) {
237: logger
238: .warning("Field type for "
239: + inFieldName
240: + " set to S which is flat out wrong people!, I am setting this to C, in the hopes you meant character.");
241: }
242: if (inFieldLength > 254) {
243: if (logger.isLoggable(Level.FINE)) {
244: logger
245: .fine("Field Length for "
246: + inFieldName
247: + " set to "
248: + inFieldLength
249: + " Which is longer than 254, not consistent with dbase III");
250: }
251: }
252: tempFieldDescriptors[fields.length].fieldLength = 8;
253: } else if ((inFieldType == 'D') || (inFieldType == 'd')) {
254: tempFieldDescriptors[fields.length].fieldType = 'D';
255: if (inFieldLength != 8) {
256: if (logger.isLoggable(Level.FINE)) {
257: logger.fine("Field Length for " + inFieldName
258: + " set to " + inFieldLength
259: + " Setting to 8 digets YYYYMMDD");
260: }
261: }
262: tempFieldDescriptors[fields.length].fieldLength = 8;
263: } else if ((inFieldType == 'F') || (inFieldType == 'f')) {
264: tempFieldDescriptors[fields.length].fieldType = 'F';
265: if (inFieldLength > 20) {
266: if (logger.isLoggable(Level.FINE)) {
267: logger
268: .fine("Field Length for "
269: + inFieldName
270: + " set to "
271: + inFieldLength
272: + " Preserving length, but should be set to Max of 20 not valid for dbase IV, and UP specification, not present in dbaseIII.");
273: }
274: }
275: } else if ((inFieldType == 'N') || (inFieldType == 'n')) {
276: tempFieldDescriptors[fields.length].fieldType = 'N';
277: if (inFieldLength > 18) {
278: if (logger.isLoggable(Level.FINE)) {
279: logger
280: .fine("Field Length for "
281: + inFieldName
282: + " set to "
283: + inFieldLength
284: + " Preserving length, but should be set to Max of 18 for dbase III specification.");
285: }
286: }
287: if (inDecimalCount < 0) {
288: if (logger.isLoggable(Level.FINE)) {
289: logger
290: .fine("Field Decimal Position for "
291: + inFieldName
292: + " set to "
293: + inDecimalCount
294: + " Setting to 0 no decimal data will be saved.");
295: }
296: tempFieldDescriptors[fields.length].decimalCount = 0;
297: }
298: if (inDecimalCount > inFieldLength - 1) {
299: if (logger.isLoggable(Level.WARNING)) {
300: logger.warning("Field Decimal Position for "
301: + inFieldName + " set to " + inDecimalCount
302: + " Setting to " + (inFieldLength - 1)
303: + " no non decimal data will be saved.");
304: }
305: tempFieldDescriptors[fields.length].decimalCount = inFieldLength - 1;
306: }
307: } else if ((inFieldType == 'L') || (inFieldType == 'l')) {
308: tempFieldDescriptors[fields.length].fieldType = 'L';
309: if (inFieldLength != 1) {
310: if (logger.isLoggable(Level.FINE)) {
311: logger
312: .fine("Field Length for "
313: + inFieldName
314: + " set to "
315: + inFieldLength
316: + " Setting to length of 1 for logical fields.");
317: }
318: }
319: tempFieldDescriptors[fields.length].fieldLength = 1;
320: } else {
321: throw new DbaseFileException("Undefined field type "
322: + inFieldType + " For column " + inFieldName);
323: }
324: // the length of a record
325: tempLength = tempLength
326: + tempFieldDescriptors[fields.length].fieldLength;
327:
328: // set the new fields.
329: fields = tempFieldDescriptors;
330: fieldCnt = fields.length;
331: headerLength = MINIMUM_HEADER + 32 * fields.length;
332: recordLength = tempLength;
333: }
334:
335: /** Remove a column from this DbaseFileHeader.
336: * @todo This is really ugly, don't know who wrote it, but it needs fixin...
337: * @param inFieldName The name of the field, will ignore case and trim.
338: * @return index of the removed column, -1 if no found
339: */
340: public int removeColumn(String inFieldName) {
341:
342: int retCol = -1;
343: int tempLength = 1;
344: DbaseField[] tempFieldDescriptors = new DbaseField[fields.length - 1];
345: for (int i = 0, j = 0; i < fields.length; i++) {
346: if (!inFieldName.equalsIgnoreCase(fields[i].fieldName
347: .trim())) {
348: // if this is the last field and we still haven't found the
349: // named field
350: if (i == j && i == fields.length - 1) {
351: System.err.println("Could not find a field named '"
352: + inFieldName + "' for removal");
353: return retCol;
354: }
355: tempFieldDescriptors[j] = fields[i];
356: tempFieldDescriptors[j].fieldDataAddress = tempLength;
357: tempLength += tempFieldDescriptors[j].fieldLength;
358: // only increment j on non-matching fields
359: j++;
360: } else {
361: retCol = i;
362: }
363: }
364:
365: // set the new fields.
366: fields = tempFieldDescriptors;
367: headerLength = 33 + 32 * fields.length;
368: recordLength = tempLength;
369:
370: return retCol;
371: }
372:
373: // Retrieve the length of the field at the given index
374: /** Returns the field length in bytes.
375: * @param inIndex The field index.
376: * @return The length in bytes.
377: */
378: public int getFieldLength(int inIndex) {
379: return fields[inIndex].fieldLength;
380: }
381:
382: // Retrieve the location of the decimal point within the field.
383: /** Get the decimal count of this field.
384: * @param inIndex The field index.
385: * @return The decimal count.
386: */
387: public int getFieldDecimalCount(int inIndex) {
388: return fields[inIndex].decimalCount;
389: }
390:
391: // Retrieve the Name of the field at the given index
392: /** Get the field name.
393: * @param inIndex The field index.
394: * @return The name of the field.
395: */
396: public String getFieldName(int inIndex) {
397: return fields[inIndex].fieldName;
398: }
399:
400: // Retrieve the type of field at the given index
401: /** Get the character class of the field.
402: * @param inIndex The field index.
403: * @return The dbase character representing this field.
404: */
405: public char getFieldType(int inIndex) {
406: return fields[inIndex].fieldType;
407: }
408:
409: /** Get the date this file was last updated.
410: * @return The Date last modified.
411: */
412: public Date getLastUpdateDate() {
413: return date;
414: }
415:
416: /** Return the number of fields in the records.
417: * @return The number of fields in this table.
418: */
419: public int getNumFields() {
420: return fields.length;
421: }
422:
423: /** Return the number of records in the file
424: * @return The number of records in this table.
425: */
426: public int getNumRecords() {
427: return recordCnt;
428: }
429:
430: /** Get the length of the records in bytes.
431: * @return The number of bytes per record.
432: */
433: public int getRecordLength() {
434: return recordLength;
435: }
436:
437: /** Get the length of the header
438: * @return The length of the header in bytes.
439: */
440: public int getHeaderLength() {
441: return headerLength;
442: }
443:
444: /** Read the header data from the DBF file.
445: * @param channel A readable byte channel. If you have an InputStream you need to use, you can
446: * call java.nio.Channels.getChannel(InputStream in).
447: * @throws IOException If errors occur while reading.
448: */
449: public void readHeader(ReadableByteChannel channel)
450: throws IOException {
451: // we'll read in chunks of 1K
452: ByteBuffer in = ByteBuffer.allocateDirect(1024);
453: // do this or GO CRAZY
454: // ByteBuffers come preset to BIG_ENDIAN !
455: in.order(ByteOrder.LITTLE_ENDIAN);
456:
457: // only want to read first 10 bytes...
458: in.limit(10);
459:
460: read(in, channel);
461: in.position(0);
462:
463: // type of file.
464: byte magic = in.get();
465: if (magic != MAGIC) {
466: throw new IOException("Unsupported DBF file Type "
467: + Integer.toHexString(magic));
468: }
469:
470: // parse the update date information.
471: int tempUpdateYear = in.get();
472: int tempUpdateMonth = in.get();
473: int tempUpdateDay = in.get();
474: // ouch Y2K uncompliant
475: if (tempUpdateYear > 90) {
476: tempUpdateYear = tempUpdateYear + 1900;
477: } else {
478: tempUpdateYear = tempUpdateYear + 2000;
479: }
480: Calendar c = Calendar.getInstance();
481: c.set(Calendar.YEAR, tempUpdateYear);
482: c.set(Calendar.MONTH, tempUpdateMonth - 1);
483: c.set(Calendar.DATE, tempUpdateDay);
484: date = c.getTime();
485:
486: // read the number of records.
487: recordCnt = in.getInt();
488:
489: // read the length of the header structure.
490: // ahhh.. unsigned little-endian shorts
491: // mask out the byte and or it with shifted 2nd byte
492: headerLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);
493:
494: // if the header is bigger than our 1K, reallocate
495: if (headerLength > in.capacity()) {
496: NIOUtilities.clean(in);
497: in = ByteBuffer.allocateDirect(headerLength - 10);
498: }
499: in.limit(headerLength - 10);
500: in.position(0);
501: read(in, channel);
502: in.position(0);
503:
504: // read the length of a record
505: // ahhh.. unsigned little-endian shorts
506: recordLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);
507:
508: // skip / skip thesreserved bytes in the header.
509: in.position(in.position() + 20);
510:
511: // calculate the number of Fields in the header
512: fieldCnt = (headerLength - FILE_DESCRIPTOR_SIZE - 1)
513: / FILE_DESCRIPTOR_SIZE;
514:
515: // read all of the header records
516: List lfields = new ArrayList();
517: for (int i = 0; i < fieldCnt; i++) {
518: DbaseField field = new DbaseField();
519:
520: // read the field name
521: byte[] buffer = new byte[11];
522: in.get(buffer);
523: String name = new String(buffer);
524: int nullPoint = name.indexOf(0);
525: if (nullPoint != -1) {
526: name = name.substring(0, nullPoint);
527: }
528: field.fieldName = name.trim();
529:
530: // read the field type
531: field.fieldType = (char) in.get();
532:
533: // read the field data address, offset from the start of the record.
534: field.fieldDataAddress = in.getInt();
535:
536: // read the field length in bytes
537: int length = (int) in.get();
538: if (length < 0) {
539: length = length + 256;
540: }
541: field.fieldLength = length;
542:
543: if (length > largestFieldSize) {
544: largestFieldSize = length;
545: }
546:
547: // read the field decimal count in bytes
548: field.decimalCount = (int) in.get();
549:
550: // rreservedvededved bytes.
551: //in.skipBytes(14);
552: in.position(in.position() + 14);
553:
554: // some broken shapefiles have 0-length attributes. The reference implementation
555: // (ArcExplorer 2.0, built with MapObjects) just ignores them.
556: if (field.fieldLength > 0) {
557: lfields.add(field);
558: }
559: }
560:
561: // Last byte is a marker for the end of the field definitions.
562: //in.skipBytes(1);
563: in.position(in.position() + 1);
564:
565: NIOUtilities.clean(in);
566:
567: fields = new DbaseField[lfields.size()];
568: fields = (DbaseField[]) lfields.toArray(fields);
569: }
570:
571: /** Get the largest field size of this table.
572: * @return The largt field size iiin bytes.
573: */
574: public int getLargestFieldSize() {
575: return largestFieldSize;
576: }
577:
578: /** Set the number of records in the file
579: * @param inNumRecords The number of records.
580: */
581: public void setNumRecords(int inNumRecords) {
582: recordCnt = inNumRecords;
583: }
584:
585: /** Write the header data to the DBF file.
586: * @param out A channel to write to. If you have an OutputStream you can obtain the correct
587: * channel by using java.nio.Channels.newChannel(OutputStream out).
588: * @throws IOException If errors occur.
589: */
590: public void writeHeader(WritableByteChannel out) throws IOException {
591: // take care of the annoying case where no records have been added...
592: if (headerLength == -1) {
593: headerLength = MINIMUM_HEADER;
594: }
595: ByteBuffer buffer = ByteBuffer.allocateDirect(headerLength);
596: buffer.order(ByteOrder.LITTLE_ENDIAN);
597:
598: // write the output file type.
599: buffer.put((byte) MAGIC);
600:
601: // write the date stuff
602: Calendar c = Calendar.getInstance();
603: c.setTime(new Date());
604: buffer.put((byte) (c.get(Calendar.YEAR) % 100));
605: buffer.put((byte) (c.get(Calendar.MONTH) + 1));
606: buffer.put((byte) (c.get(Calendar.DAY_OF_MONTH)));
607:
608: // write the number of records in the datafile.
609: buffer.putInt(recordCnt);
610:
611: // write the length of the header structure.
612: buffer.putShort((short) headerLength);
613:
614: // write the length of a record
615: buffer.putShort((short) recordLength);
616:
617: // // write the reserved bytes in the header
618: // for (int i=0; i<20; i++) out.writeByteLE(0);
619: buffer.position(buffer.position() + 20);
620:
621: // write all of the header records
622: int tempOffset = 0;
623: for (int i = 0; i < fields.length; i++) {
624:
625: // write the field name
626: for (int j = 0; j < 11; j++) {
627: if (fields[i].fieldName.length() > j) {
628: buffer.put((byte) fields[i].fieldName.charAt(j));
629: } else {
630: buffer.put((byte) 0);
631: }
632: }
633:
634: // write the field type
635: buffer.put((byte) fields[i].fieldType);
636: // // write the field data address, offset from the start of the record.
637: buffer.putInt(tempOffset);
638: tempOffset += fields[i].fieldLength;
639:
640: // write the length of the field.
641: buffer.put((byte) fields[i].fieldLength);
642:
643: // write the decimal count.
644: buffer.put((byte) fields[i].decimalCount);
645:
646: // write the reserved bytes.
647: //for (in j=0; jj<14; j++) out.writeByteLE(0);
648: buffer.position(buffer.position() + 14);
649: }
650:
651: // write the end of the field definitions marker
652: buffer.put((byte) 0x0D);
653:
654: buffer.position(0);
655:
656: int r = buffer.remaining();
657: while ((r -= out.write(buffer)) > 0) {
658: ; // do nothing
659: }
660:
661: NIOUtilities.clean(buffer);
662: }
663:
664: /** Get a simple representation of this header.
665: * @return A String representing the state of the header.
666: */
667: public String toString() {
668: StringBuffer fs = new StringBuffer();
669: for (int i = 0, ii = fields.length; i < ii; i++) {
670: DbaseField f = fields[i];
671: fs.append(f.fieldName + " " + f.fieldType + " "
672: + f.fieldLength + " " + f.decimalCount + " "
673: + f.fieldDataAddress + "\n");
674: }
675:
676: return "DB3 Header\n" + "Date : " + date + "\n" + "Records : "
677: + recordCnt + "\n" + "Fields : " + fieldCnt + "\n" + fs;
678:
679: }
680:
681: }
|