001: //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/io/dbaseapi/DBaseFile.java $
002: /*---------------- FILE HEADER ------------------------------------------
003:
004: This file is part of deegree.
005: Copyright (C) 2001-2008 by:
006: EXSE, Department of Geography, University of Bonn
007: http://www.giub.uni-bonn.de/deegree/
008: lat/lon GmbH
009: http://www.lat-lon.de
010:
011: This library is free software; you can redistribute it and/or
012: modify it under the terms of the GNU Lesser General Public
013: License as published by the Free Software Foundation; either
014: version 2.1 of the License, or (at your option) any later version.
015:
016: This library is distributed in the hope that it will be useful,
017: but WITHOUT ANY WARRANTY; without even the implied warranty of
018: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019: Lesser General Public License for more details.
020:
021: You should have received a copy of the GNU Lesser General Public
022: License along with this library; if not, write to the Free Software
023: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024:
025: Contact:
026:
027: Andreas Poth
028: lat/lon GmbH
029: Aennchenstr. 19
030: 53177 Bonn
031: Germany
032: E-Mail: poth@lat-lon.de
033:
034: Prof. Dr. Klaus Greve
035: Department of Geography
036: University of Bonn
037: Meckenheimer Allee 166
038: 53115 Bonn
039: Germany
040: E-Mail: greve@giub.uni-bonn.de
041:
042:
043: ---------------------------------------------------------------------------*/
044: package org.deegree.io.dbaseapi;
045:
046: import java.io.ByteArrayOutputStream;
047: import java.io.File;
048: import java.io.IOException;
049: import java.io.RandomAccessFile;
050: import java.net.URI;
051: import java.util.ArrayList;
052: import java.util.HashMap;
053: import java.util.List;
054: import java.util.Map;
055:
056: import org.deegree.datatypes.QualifiedName;
057: import org.deegree.datatypes.Types;
058: import org.deegree.framework.util.TimeTools;
059: import org.deegree.model.feature.Feature;
060: import org.deegree.model.feature.FeatureFactory;
061: import org.deegree.model.feature.FeatureProperty;
062: import org.deegree.model.feature.schema.FeatureType;
063: import org.deegree.model.feature.schema.GeometryPropertyType;
064: import org.deegree.model.feature.schema.PropertyType;
065: import org.deegree.model.spatialschema.ByteUtils;
066: import org.deegree.ogcbase.CommonNamespaces;
067:
068: /**
069: * the datatypes of the dBase file and their representation as java types:
070: *
071: * dBase-type dBase-type-ID java-type
072: *
073: * character "C" String float "F" Float number "N" Double logical "L" String memo "M" String date
074: * "D" Date binary "B" ByteArrayOutputStream
075: *
076: * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
077: * @author last edited by: $Author: apoth $
078: *
079: * @version $Revision: 9342 $, $Date: 2007-12-27 04:32:57 -0800 (Thu, 27 Dec 2007) $
080: */
081: public class DBaseFile {
082:
083: private static final URI DEEGREEAPP = CommonNamespaces
084: .buildNSURI("http://www.deegree.org/app");
085:
086: private static final String APP_PREFIX = "app";
087:
088: private ArrayList<String> colHeader = new ArrayList<String>();
089:
090: // representing the datasection of the dBase file
091: // only needed for writing a dBase file
092: private DBFDataSection dataSection = null;
093:
094: // feature type of generated features
095: private FeatureType ft;
096:
097: // keys: property types, values: column (in dbase file)
098: private Map<PropertyType, String> ftMapping = new HashMap<PropertyType, String>(
099: 100);
100:
101: // Hashtable to contain info abouts in the table
102: private Map<String, dbfCol> column_info = new HashMap<String, dbfCol>();
103:
104: // references to the dbase file
105: private RandomAccessFile rafDbf;
106:
107: // file suffixes for dbf
108: private final String _dbf = ".dbf";
109:
110: // represents the dBase file header
111: // only needed for writing the dBase file
112: private DBFHeader header = null;
113:
114: // representing the name of the dBase file
115: // only needed for writing the dBase file
116: private String fname = null;
117:
118: private String ftName = null;
119:
120: // number of records in the table
121: private double file_numrecs;
122:
123: // data start position, and length of the data
124: private int file_datalength;
125:
126: // data start position, and length of the data
127: private int file_datap;
128:
129: // flag which indicates if a dBase file should be
130: // read or writed.
131: // filemode = 0 : read only
132: // filemode = 1 : write only
133: private int filemode = 0;
134:
135: // number of columns
136: private int num_fields;
137:
138: // current record
139: private long record_number = 0;
140:
141: // size of the cache used for reading data from the dbase table
142: private long cacheSize = 1000000;
143:
144: // array containing the data of the cache
145: private byte[] dataArray = null;
146:
147: // file position the caches starts
148: private long startIndex = 0;
149:
150: /**
151: * constructor<BR>
152: * only for reading a dBase file<BR>
153: *
154: * @param url
155: */
156: public DBaseFile(String url) throws IOException {
157: fname = url;
158:
159: // creates rafDbf
160: rafDbf = new RandomAccessFile(url + _dbf, "r");
161:
162: // dataArray = new byte[(int)rafDbf.length()];
163: if (cacheSize > rafDbf.length()) {
164: cacheSize = rafDbf.length();
165: }
166:
167: dataArray = new byte[(int) cacheSize];
168: rafDbf.read(dataArray);
169: rafDbf.seek(0);
170:
171: // initialize dbase file
172: initDBaseFile();
173:
174: filemode = 0;
175: }
176:
177: /**
178: * constructor<BR>
179: * only for writing a dBase file<BR>
180: *
181: * @param url
182: * @param fieldDesc
183: *
184: */
185: public DBaseFile(String url, FieldDescriptor[] fieldDesc)
186: throws DBaseException {
187: fname = url;
188:
189: // create header
190: header = new DBFHeader(fieldDesc);
191:
192: // create data section
193: dataSection = new DBFDataSection(fieldDesc);
194:
195: filemode = 1;
196: }
197:
198: /**
199: *
200: */
201: public void close() {
202: try {
203: if (rafDbf != null) {
204: // just true for reading access
205: rafDbf.close();
206: }
207: } catch (Exception ex) {
208: // should never happen
209: ex.printStackTrace();
210: }
211: }
212:
213: /**
214: * method: initDBaseFile(); inits a DBF file. This is based on Pratap Pereira's Xbase.pm perl
215: * module
216: *
217: */
218: private void initDBaseFile() throws IOException {
219: // position the record pointer at 0
220: rafDbf.seek(0);
221:
222: /*
223: * // read the file type file_type = fixByte( rafDbf.readByte() ); // get the last update
224: * date file_update_year = fixByte( rafDbf.readByte() ); file_update_month = fixByte(
225: * rafDbf.readByte() ); file_update_day = fixByte( rafDbf.readByte() );
226: */
227:
228: fixByte(rafDbf.readByte());
229: fixByte(rafDbf.readByte());
230: fixByte(rafDbf.readByte());
231: fixByte(rafDbf.readByte());
232:
233: // a byte array to hold little-endian long data
234: byte[] b = new byte[4];
235:
236: // read that baby in...
237: rafDbf.readFully(b);
238:
239: // convert the byte array into a long (really a double)
240: file_numrecs = ByteUtils.readLEInt(b, 0);
241:
242: b = null;
243:
244: // a byte array to hold little-endian short data
245: b = new byte[2];
246:
247: // get the data position (where it starts in the file)
248: rafDbf.readFully(b);
249: file_datap = ByteUtils.readLEShort(b, 0);
250:
251: // find out the length of the data portion
252: rafDbf.readFully(b);
253: file_datalength = ByteUtils.readLEShort(b, 0);
254:
255: // calculate the number of fields
256: num_fields = (file_datap - 33) / 32;
257:
258: // read in the column data
259: int locn = 0; // offset of the current column
260:
261: // process each field
262: for (int i = 1; i <= num_fields; i++) {
263: // seek the position of the field definition data.
264: // This information appears after the first 32 byte
265: // table information, and lives in 32 byte chunks.
266: rafDbf.seek(((i - 1) * 32) + 32);
267:
268: b = null;
269:
270: // get the column name into a byte array
271: b = new byte[11];
272: rafDbf.readFully(b);
273:
274: // convert the byte array to a String
275: String col_name = new String(b).trim().toUpperCase();
276:
277: // read in the column type
278: char[] c = new char[1];
279: c[0] = (char) rafDbf.readByte();
280:
281: // String ftyp = new String( c );
282:
283: // skip four bytes
284: rafDbf.skipBytes(4);
285:
286: // get field length and precision
287: short flen = fixByte(rafDbf.readByte());
288: short fdec = fixByte(rafDbf.readByte());
289:
290: // set the field position to the current
291: // value of locn
292: int fpos = locn;
293:
294: // increment locn by the length of this field.
295: locn += flen;
296:
297: // create a new dbfCol object and assign it the
298: // attributes of the current field
299: dbfCol column = new dbfCol(col_name);
300: column.type = new String(c);
301: column.size = flen;
302: column.position = fpos + 1;
303: column.prec = fdec;
304:
305: // to be done: get the name of dbf-table via method in ShapeFile
306: column.table = "NOT";
307:
308: column_info.put(col_name, column);
309: colHeader.add(col_name);
310: } // end for
311:
312: ft = createCanonicalFeatureType();
313:
314: } // end of initDBaseFile
315:
316: /**
317: * Overrides the default feature type (which is generated from all columns in the dbase file) to
318: * allow customized naming and ordering of properties.
319: *
320: * @param ft
321: * @param ftMapping
322: */
323: public void setFeatureType(FeatureType ft,
324: Map<PropertyType, String> ftMapping) {
325: this .ft = ft;
326: this .ftMapping = ftMapping;
327: }
328:
329: /**
330: * Creates a canonical {@link FeatureType} from all fields of the <code>DBaseFile</code>.
331: *
332: * @return feature type that contains all fields as property types
333: */
334: private FeatureType createCanonicalFeatureType() {
335: dbfCol column = null;
336:
337: PropertyType[] ftp = new PropertyType[colHeader.size() + 1];
338:
339: for (int i = 0; i < colHeader.size(); i++) {
340: // retrieve the dbfCol object which corresponds // to this column.
341: column = column_info.get(colHeader.get(i));
342:
343: QualifiedName name = new QualifiedName(APP_PREFIX,
344: column.name, DEEGREEAPP);
345:
346: if (column.type.equalsIgnoreCase("C")) {
347: ftp[i] = FeatureFactory.createSimplePropertyType(name,
348: Types.VARCHAR, true);
349: } else if (column.type.equalsIgnoreCase("F")
350: || column.type.equalsIgnoreCase("N")) {
351: if (column.prec == 0) {
352: if (column.size < 10) {
353: ftp[i] = FeatureFactory
354: .createSimplePropertyType(name,
355: Types.INTEGER, true);
356: } else {
357: ftp[i] = FeatureFactory
358: .createSimplePropertyType(name,
359: Types.BIGINT, true);
360: }
361: } else {
362: if (column.size < 8) {
363: ftp[i] = FeatureFactory
364: .createSimplePropertyType(name,
365: Types.FLOAT, true);
366: } else {
367: ftp[i] = FeatureFactory
368: .createSimplePropertyType(name,
369: Types.DOUBLE, true);
370: }
371: }
372: } else if (column.type.equalsIgnoreCase("M")) {
373: ftp[i] = FeatureFactory.createSimplePropertyType(name,
374: Types.VARCHAR, true);
375: } else if (column.type.equalsIgnoreCase("L")) {
376: ftp[i] = FeatureFactory.createSimplePropertyType(name,
377: Types.VARCHAR, true);
378: } else if (column.type.equalsIgnoreCase("D")) {
379: ftp[i] = FeatureFactory.createSimplePropertyType(name,
380: Types.VARCHAR, true);
381: } else if (column.type.equalsIgnoreCase("B")) {
382: ftp[i] = FeatureFactory.createSimplePropertyType(name,
383: Types.BLOB, true);
384: }
385:
386: this .ftMapping.put(ftp[i], column.name);
387: }
388:
389: int index = fname.lastIndexOf("/");
390: ftName = fname;
391: if (index >= 0) {
392: ftName = fname.substring(index + 1);
393: } else {
394: index = fname.lastIndexOf("\\");
395: if (index >= 0) {
396: ftName = fname.substring(index + 1);
397: }
398: }
399:
400: QualifiedName featureTypeName = new QualifiedName(APP_PREFIX,
401: ftName, DEEGREEAPP);
402:
403: QualifiedName name = new QualifiedName(APP_PREFIX, "GEOM",
404: DEEGREEAPP);
405: ftp[ftp.length - 1] = FeatureFactory
406: .createGeometryPropertyType(name,
407: Types.GEOMETRY_PROPERTY_NAME, 1, 1);
408:
409: return FeatureFactory.createFeatureType(featureTypeName, false,
410: ftp);
411: }
412:
413: /**
414: *
415: * @return number of records in the table
416: */
417: public int getRecordNum() throws DBaseException {
418: if (filemode == 1) {
419: throw new DBaseException(
420: "class is initialized in write-only mode");
421: }
422:
423: return (int) file_numrecs;
424: }
425:
426: /**
427: *
428: * Positions the record pointer at the top of the table.
429: */
430: public void goTop() throws DBaseException {
431: if (filemode == 1) {
432: throw new DBaseException(
433: "class is initialized in write-only mode");
434: }
435:
436: record_number = 0;
437: }
438:
439: /**
440: * Advance the record pointer to the next record.
441: *
442: * @return true if pointer has been increased
443: */
444: public boolean nextRecord() throws DBaseException {
445: if (filemode == 1) {
446: throw new DBaseException(
447: "class is initialized in write-only mode");
448: }
449:
450: if (record_number < file_numrecs) {
451: record_number++;
452: return true;
453: }
454: return false;
455:
456: }
457:
458: /**
459: *
460: * @return column's string value from the current row.
461: */
462: public String getColumn(String col_name) throws DBaseException {
463: if (filemode == 1) {
464: throw new DBaseException(
465: "class is initialized in write-only mode");
466: }
467:
468: try {
469: // retrieve the dbfCol object which corresponds
470: // to this column.
471: dbfCol column = column_info.get(col_name);
472:
473: // seek the starting offset of the current record,
474: // as indicated by record_number
475: long pos = file_datap
476: + ((record_number - 1) * file_datalength);
477:
478: // read data from cache if the requested part of the dbase file is
479: // within it
480: if ((pos >= startIndex)
481: && ((pos + column.position + column.size) < (startIndex + cacheSize))) {
482: pos = pos - startIndex;
483: } else {
484: // actualize cache starting at the current cursor position
485: // if neccesary correct cursor position
486: rafDbf.seek(pos);
487: rafDbf.read(dataArray);
488: startIndex = pos;
489: pos = 0;
490: }
491: int ff = (int) (pos + column.position);
492: return new String(dataArray, ff, column.size).trim();
493: } catch (Exception e) {
494: e.printStackTrace();
495: return e.toString();
496: }
497: }
498:
499: /**
500: * @return properties (column headers) of the dBase-file<BR>
501: */
502: public String[] getProperties() throws DBaseException {
503: if (filemode == 1) {
504: throw new DBaseException(
505: "class is initialized in write-only mode");
506: }
507:
508: return colHeader.toArray(new String[colHeader.size()]);
509: }
510:
511: /**
512: * @return datatype of each column of the database<BR>
513: */
514: public String[] getDataTypes() throws DBaseException {
515: if (filemode == 1) {
516: throw new DBaseException(
517: "class is initialized in write-only mode");
518: }
519:
520: String[] datatypes = new String[colHeader.size()];
521: dbfCol column;
522:
523: for (int i = 0; i < colHeader.size(); i++) {
524: // retrieve the dbfCol object which corresponds
525: // to this column.
526: column = column_info.get(colHeader.get(i));
527:
528: datatypes[i] = column.type.trim();
529: }
530:
531: return datatypes;
532: }
533:
534: /**
535: * @param container
536: * @param element
537: * @return true if the container sting array contains element<BR>
538: */
539: private boolean contains(String[] container, String element) {
540: for (int i = 0; i < container.length; i++)
541:
542: if (container[i].equals(element)) {
543: return true;
544: }
545:
546: return false;
547: }
548:
549: /**
550: * @param field
551: * @return the size of a column
552: */
553: public int getDataLength(String field) throws DBaseException {
554: dbfCol col = column_info.get(field);
555: if (col == null)
556: throw new DBaseException("Field " + field + " not found");
557:
558: return col.size;
559: }
560:
561: /**
562: * @param fields
563: * @return the datatype of each column of the database specified by fields<BR>
564: */
565: public String[] getDataTypes(String[] fields) throws DBaseException {
566: if (filemode == 1) {
567: throw new DBaseException(
568: "class is initialized in write-only mode");
569: }
570:
571: ArrayList<String> vec = new ArrayList<String>();
572: dbfCol column;
573:
574: for (int i = 0; i < colHeader.size(); i++) {
575: // check if the current (i'th) column (string) is
576: // within the array of specified columns
577: if (contains(fields, colHeader.get(i))) {
578: // retrieve the dbfCol object which corresponds
579: // to this column.
580: column = column_info.get(colHeader.get(i));
581:
582: vec.add(column.type.trim());
583: }
584: }
585:
586: return vec.toArray(new String[vec.size()]);
587: }
588:
589: /**
590: * Returns a row of the dBase file as a {@link Feature} instance.
591: *
592: * @param rowNo
593: * @return a row of the dBase file as a Feature instance
594: * @throws DBaseException
595: */
596: public Feature getFRow(int rowNo) throws DBaseException {
597:
598: Map<String, Object> columnValues = getRow(rowNo);
599:
600: PropertyType[] propTypes = this .ft.getProperties();
601: List<FeatureProperty> props = new ArrayList<FeatureProperty>();
602:
603: for (int i = 0; i < propTypes.length; i++) {
604: PropertyType pt = propTypes[i];
605: if (pt instanceof GeometryPropertyType) {
606: // insert dummy property for geometry
607: FeatureProperty prop = FeatureFactory
608: .createFeatureProperty(pt.getName(), null);
609: props.add(prop);
610: } else {
611: String columnName = this .ftMapping.get(pt);
612: Object columnValue = columnValues.get(columnName);
613: if (columnValue != null) {
614: FeatureProperty prop = FeatureFactory
615: .createFeatureProperty(pt.getName(),
616: columnValue);
617: props.add(prop);
618: }
619: }
620: }
621: FeatureProperty[] fp = props.toArray(new FeatureProperty[props
622: .size()]);
623: return FeatureFactory.createFeature(ftName + rowNo, ft, fp);
624: }
625:
626: /**
627: *
628: * @param rowNo
629: * @return a row of the dbase file
630: * @throws DBaseException
631: */
632: private Map<String, Object> getRow(int rowNo) throws DBaseException {
633:
634: Map<String, Object> columnValues = new HashMap<String, Object>();
635:
636: goTop();
637: record_number += rowNo;
638:
639: for (int i = 0; i < colHeader.size(); i++) {
640:
641: // retrieve the dbfCol object which corresponds to this column.
642: dbfCol column = column_info.get(colHeader.get(i));
643:
644: String value = getColumn(column.name);
645: Object columnValue = value;
646:
647: if (value != null) {
648: // cast the value of the i'th column to corresponding datatype
649: if (column.type.equalsIgnoreCase("C")) {
650: // nothing to do
651: } else if (column.type.equalsIgnoreCase("F")
652: || column.type.equalsIgnoreCase("N")) {
653: try {
654: if (column.prec == 0) {
655: if (column.size < 10) {
656: columnValue = new Integer(value);
657: } else {
658: columnValue = new Long(value);
659: }
660: } else {
661: if (column.size < 8) {
662: columnValue = new Float(value);
663: } else {
664: columnValue = new Double(value);
665: }
666: }
667: } catch (Exception ex) {
668: columnValue = new Double("0");
669: }
670: } else if (column.type.equalsIgnoreCase("M")) {
671: // nothing to do
672: } else if (column.type.equalsIgnoreCase("L")) {
673: // nothing to do
674: } else if (column.type.equalsIgnoreCase("D")) {
675: if (value.equals("")) {
676: columnValue = null;
677: } else {
678: String s = value.substring(0, 4) + '-'
679: + value.substring(4, 6) + '-'
680: + value.substring(6, 8);
681: columnValue = TimeTools.createCalendar(s)
682: .getTime();
683: }
684: } else if (column.type.equalsIgnoreCase("B")) {
685: ByteArrayOutputStream os = new ByteArrayOutputStream(
686: 10000);
687: try {
688: os.write(value.getBytes());
689: } catch (IOException e) {
690: e.printStackTrace();
691: }
692: columnValue = os;
693: }
694: } else {
695: columnValue = "";
696: }
697: columnValues.put(column.name, columnValue);
698: }
699:
700: return columnValues;
701: }
702:
703: /**
704: * bytes are signed; let's fix them...
705: * @param b
706: * @return unsigned byte as short
707: */
708: private static short fixByte(byte b) {
709: if (b < 0) {
710: return (short) (b + 256);
711: }
712:
713: return b;
714: }
715:
716: /**
717: * creates the dbase file and writes all data to it if the
718: * file specified by fname (s.o.) exists it will be deleted!
719: *
720: * @throws IOException
721: * @throws DBaseException
722: */
723: public void writeAllToFile() throws IOException, DBaseException {
724: if (filemode == 0) {
725: throw new DBaseException(
726: "class is initialized in read-only mode");
727: }
728:
729: // if a file with the retrieved filename exists, delete it!
730: File file = new File(fname + ".dbf");
731:
732: if (file.exists()) {
733: file.delete();
734: }
735:
736: // create a new file
737: RandomAccessFile rdbf = new RandomAccessFile(fname + ".dbf",
738: "rw");
739:
740: try {
741: byte[] b = header.getHeader();
742: int nRecords = dataSection.getNoOfRecords();
743: // write number of records
744: ByteUtils.writeLEInt(b, 4, nRecords);
745: // write header to the file
746: rdbf.write(b);
747: b = dataSection.getDataSection();
748: // write datasection to the file
749: rdbf.write(b);
750: } catch (IOException e) {
751: throw e;
752: } finally {
753: rdbf.close();
754: }
755: }
756:
757: /**
758: * writes a data record to byte array representing
759: * the data section of the dBase file. The method gets the data type of each field in recData
760: * from fieldDesc wich has been set at the constructor.
761: * @param recData
762: * @throws DBaseException
763: */
764: public void setRecord(ArrayList recData) throws DBaseException {
765: if (filemode == 0) {
766: throw new DBaseException(
767: "class is initialized in read-only mode");
768: }
769:
770: dataSection.setRecord(recData);
771: }
772:
773: /**
774: * writes a data record to byte array
775: * representing the data section of the dBase file. The method gets the data type of each field
776: * in recData from fieldDesc wich has been set at the constructor. index specifies the location
777: * of the retrieved record in the datasection. if an invalid index is used an exception will be
778: * thrown
779: * @param index
780: * @param recData
781: * @throws DBaseException
782: */
783: public void setRecord(int index, ArrayList recData)
784: throws DBaseException {
785: if (filemode == 0) {
786: throw new DBaseException(
787: "class is initialized in read-only mode");
788: }
789:
790: dataSection.setRecord(index, recData);
791: }
792:
793: /**
794: * @return the feature type of the generated features
795: */
796: public FeatureType getFeatureType() {
797: return ft;
798: }
799:
800: } // end of class DBaseFile
801:
802: /**
803: *
804: *
805: * @version $Revision: 9342 $
806: * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
807: */
808: class tsColumn {
809: public String name = null; // the column's name
810:
811: public String table = null; // the table which "owns" the column
812:
813: public String type = null; // the column's type
814:
815: public int prec = 0; // the column's precision
816:
817: public int size = 0; // the column's size
818:
819: /**
820: *
821: * Constructs a tsColumn object.
822: *
823: * @param s
824: * the column name
825: */
826: tsColumn(String s) {
827: name = s;
828: }
829: } // end of class tsColumn
830:
831: /**
832: *
833: *
834: * @version $Revision: 9342 $
835: * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
836: */
837: class dbfCol extends tsColumn {
838: int position = 0;
839:
840: /**
841: * Creates a new dbfCol object.
842: *
843: * @param c
844: */
845: public dbfCol(String c) {
846: super(c);
847: }
848: }
|