/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-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. */ package org.geotools.data.vpf.file; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.URI; import java.util.AbstractList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import org.geotools.data.vpf.VPFColumn; import org.geotools.data.vpf.exc.VPFHeaderFormatException; import org.geotools.data.vpf.ifc.DataTypesDefinition; import org.geotools.data.vpf.ifc.FileConstants; import org.geotools.data.vpf.io.TripletId; import org.geotools.data.vpf.io.VPFInputStream; import org.geotools.data.vpf.io.VariableIndexInputStream; import org.geotools.data.vpf.io.VariableIndexRow; import org.geotools.data.vpf.util.DataUtils; import org.geotools.feature.FeatureTypes; import org.geotools.feature.IllegalAttributeException; import org.geotools.feature.SchemaException; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.feature.type.AnnotationFeatureType; import org.geotools.filter.CompareFilter; import org.geotools.filter.Filter; import org.geotools.filter.LengthFunction; import org.geotools.filter.LiteralExpression; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.GeometryType; import org.opengis.feature.type.Name; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.InternationalString; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateList; import com.vividsolutions.jts.geom.DefaultCoordinateSequenceFactory; import com.vividsolutions.jts.geom.GeometryFactory; /** * This class encapsulates VPF files. By implementing the <code>FeatureType</code> interface, * it serves as a factory for VPFColumns. Instances of this class should * be created by VPFFileFactory. * * @author <a href="mailto:jeff@ionicenterprise.com">Jeff Yutzler</a> * * * @source $URL$ */ public class VPFFile implements SimpleFeatureType, FileConstants, DataTypesDefinition { // private final TableInputStream stream; private static String ACCESS_MODE = "r"; /** * Variable <code>byteOrder</code> keeps value of byte order in which * table is written: * * <ul> * <li> * <b>L</b> - least-significant-first * </li> * <li> * <b>M</b> - most-significant-first * </li> * </ul> */ private char byteOrder = LEAST_SIGNIF_FIRST; /** * The columns of the file. This list shall contain objects of type <code>VPFColumn</code> */ private final List columns = new Vector(); /** * Variable <code>description</code> keeps value of text description of the * table's contents. */ private String description = null; /** * The contained Feature Type */ private final SimpleFeatureType featureType; /** * Keeps value of length of ASCII header * string (i.e., the remaining information after this field) */ private int headerLength = -0; /** The associated stream */ private RandomAccessFile inputStream = null; /** * Variable <code>narrativeTable</code> keeps value of an optional * narrative file which contains miscellaneous information about the * table. */ private String narrativeTable = null; /** The path name */ private final String pathName; /** Describe variable <code>variableIndex</code> here. */ private VPFInputStream variableIndex = null; /** * Constructor. * * @param cPathName The path to this file * * @throws IOException if the path or the file are invalid * @throws SchemaException if the contained feature type can not be * constructed */ public VPFFile(String cPathName) throws IOException, SchemaException { pathName = cPathName; inputStream = new RandomAccessFile(cPathName, ACCESS_MODE); readHeader(); GeometryDescriptor gat = null; VPFColumn geometryColumn = null; Iterator iter = columns.iterator(); while (iter.hasNext()) { geometryColumn = (VPFColumn) iter.next(); if (geometryColumn.isGeometry()) { gat = geometryColumn.getGeometryAttributeType(); break; } } SimpleFeatureType superType = null; // if it's a text geometry feature type add annotation as a super type if (pathName.endsWith(TEXT_PRIMITIVE)) { superType = AnnotationFeatureType.ANNOTATION; } SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder(); b.setName( cPathName ); b.setNamespaceURI("VPF"); b.setSuperType(superType); b.addAll( columns ); b.setDefaultGeometry(gat.getLocalName()); featureType = b.buildFeatureType(); } /** * Gets the value of full length of ASCII header string including * <code>headerLength</code> field. * * @return the value of headerLength */ private int getAdjustedHeaderLength() { return this.headerLength + 4; } /* * (non-Javadoc) * @see org.geotools.feature.FeatureType#getAttributeCount() */ public int getAttributeCount() { return featureType.getAttributeCount(); } /** * Gets the value of byteOrder variable. Byte order in which table is * written: * * <ul> * <li> * <b>L</b> - least-significant-first * </li> * <li> * <b>M</b> - most-significant-first * </li> * </ul> * * * @return the value of byteOrder */ public char getByteOrder() { return byteOrder; } /** * Gets the value of the description of table content. This is nice to * have, but I don't know how to make use of it. * * @return the value of description */ // public String getDescription() { // return description; // } /** * Returns the directory name for this file by chopping off the file name * and the separator. * * @return the directory name for this file */ public String getDirectoryName() { String result = new String(); int index = pathName.lastIndexOf(File.separator); if (index >= 0) { result = pathName.substring(0, index); } return result; } /** * Returns the file name (without path) for the file * * @return the file name for this file */ public String getFileName() { String result = pathName.substring(pathName.lastIndexOf(File.separator) + 1); return result; } /** * Gets the value of narrativeTable variable file name. * * @return the value of narrativeTable */ public String getNarrativeTable() { return narrativeTable; } /** * Gets the full path name for this file * * @return the path name for this file */ public String getPathName() { return pathName; } /** * Method <code><code>getRecordSize</code></code> is used to return size in * bytes of records stored in this table. If table keeps variable length * records <code>-1</code> should be returned. * * @return an <code><code>int</code></code> value */ protected int getRecordSize() { int size = 0; Iterator iter = columns.iterator(); while (iter.hasNext()) { VPFColumn column = (VPFColumn) iter.next(); int length = FeatureTypes.getFieldLength(column); if ( length > -1 ) { size += length; } } return size; } /** * Returns a row with a matching value for the provided column * * @param idName The name of the column to look for, such as "id" * @param id An identifier for the requested row * * @return The first row which matches the ID * * @throws IllegalAttributeException The feature can not be created due to * illegal attributes in the source file */ public SimpleFeature getRowFromId(String idName, int id) throws IllegalAttributeException { SimpleFeature result = null; try { // This speeds things up mightily String firstColumnName = ((VPFColumn) columns.get(0)).getLocalName(); if (idName.equals(firstColumnName)) { setPosition(id); result = readFeature(); Number value = (Number) result.getAttribute(idName); // Check to make sure we got a primary key match if ((value == null) || (value.intValue() != id)) { result = null; } } if (result == null) { Iterator joinedIter = readAllRows().iterator(); result = getRowFromIterator(joinedIter, idName, id); } } catch (IOException exc) { exc.printStackTrace(); } return result; } /** * Returns a single matching row from the Iterator, ignoring rows that do * not match a particular id * * @param iter the iterator to examine * @param idName The name of the column to check * @param id The value of the column to check * * @return a TableRow that matches the other parameters */ private SimpleFeature getRowFromIterator(Iterator iter, String idName, int id) { SimpleFeature result = null; SimpleFeature currentFeature; int value = -1; while (iter.hasNext()) { currentFeature = (SimpleFeature) iter.next(); try { value = Integer.parseInt(currentFeature.getAttribute(idName).toString()); if (id == value) { result = currentFeature; break; } } catch (NumberFormatException exc) { // If this happens, the data is invalid so dumping a stack trace seems reasonable exc.printStackTrace(); } } return result; } /* * (non-Javadoc) * @see org.geotools.feature.FeatureType#getTypeName() */ public String getTypeName() { return featureType.getTypeName(); } /** * Determines if the stream contains storage for another object. Who knows * how well this will work on variable length objects? * * @return a <code>boolean</code> */ public boolean hasNext() { boolean result = false; try { int recordSize = getRecordSize(); if (recordSize > 0) { result = inputStream.length() >= (inputStream.getFilePointer() + recordSize); } else { result = inputStream.length() >= (inputStream.getFilePointer() + 1); } } catch (IOException exc) { // No idea what to do if this happens exc.printStackTrace(); } return result; } /* * (non-Javadoc) * @see org.geotools.feature.FeatureType#isAbstract() */ public boolean isAbstract() { return featureType.isAbstract(); } /** * Generates a list containing all of the features in the file * * @return a <code>List</code> value containing Feature objects * * @exception IOException if an error occurs */ public AbstractList readAllRows() throws IOException { AbstractList list = new LinkedList(); try { setPosition(1); } catch (IOException exc) { // This indicates that there are no rows return list; } try { SimpleFeature row = readFeature(); while (row != null) { list.add(row); if(hasNext()){ row = readFeature(); }else{ row = null; } } } catch (IllegalAttributeException exc1) { throw new IOException(exc1.getMessage()); } return list; } /** * Reads a single byte as a character value * * @return a <code>char</code> value * * @exception IOException if an error occurs */ protected char readChar() throws IOException { return (char) inputStream.read(); } /** * Reads a column definition from the file header * * @return a <code>VPFColumn</code> value * * @exception VPFHeaderFormatException if an error occurs * @exception IOException if an error occurs * @exception NumberFormatException if an error occurs */ private VPFColumn readColumn() throws VPFHeaderFormatException, IOException, NumberFormatException { char ctrl = readChar(); if (ctrl == VPF_RECORD_SEPARATOR) { return null; } String name = ctrl + readString("="); char type = readChar(); ctrl = readChar(); if (ctrl != VPF_ELEMENT_SEPARATOR) { throw new VPFHeaderFormatException( "Header format does not fit VPF file definition."); } String elemStr = readString(new String() + VPF_ELEMENT_SEPARATOR).trim(); if (elemStr.equals("*")) { elemStr = "-1"; } int elements = Integer.parseInt(elemStr); char key = readChar(); ctrl = readChar(); if (ctrl != VPF_ELEMENT_SEPARATOR) { throw new VPFHeaderFormatException( "Header format does not fit VPF file definition."); } String colDesc = readString(new String() + VPF_ELEMENT_SEPARATOR + VPF_FIELD_SEPARATOR); String descTableName = readString(new String() + VPF_ELEMENT_SEPARATOR + VPF_FIELD_SEPARATOR); String indexFile = readString(new String() + VPF_ELEMENT_SEPARATOR + VPF_FIELD_SEPARATOR); String narrTable = readString(new String() + VPF_ELEMENT_SEPARATOR + VPF_FIELD_SEPARATOR); return new VPFColumn(name, type, elements, key, colDesc, descTableName, indexFile, narrTable); } /** * Constructs an object which is an instance of Geometry * by reading values from the file. * @param instancesCount number of coordinates to read * @param dimensionality either 2 or 3 * @param readDoubles true: read a double value; false: read a float value * @return the constructed object * @throws IOException on any file IO errors */ protected Object readGeometry(int instancesCount, int dimensionality, boolean readDoubles) throws IOException { Object result = null; Coordinate coordinate = null; CoordinateList coordinates = new CoordinateList(); GeometryFactory factory = new GeometryFactory(); for (int inx = 0; inx < instancesCount; inx++) { switch (dimensionality) { case 2: coordinate = new Coordinate(readDoubles ? readDouble() : readFloat(), readDoubles ? readDouble() : readFloat()); break; case 3: coordinate = new Coordinate(readDoubles ? readDouble() : readFloat(), readDoubles ? readDouble() : readFloat(), readDoubles ? readDouble() : readFloat()); break; default: //WTF??? } coordinates.add(coordinate); } // Special handling for text primitives per the VPF spec. // The first 2 points are the endpoints of the line, the following // points fill in between the first 2 points. if (pathName.endsWith(TEXT_PRIMITIVE) && coordinates.size() > 2) { Object o = coordinates.remove(1); coordinates.add(o); } if (instancesCount == 1) { result = factory.createPoint(coordinate); } else { result = factory.createLineString(DefaultCoordinateSequenceFactory.instance() .create(coordinates .toCoordinateArray())); } return result; } /** * Retrieves a double from the file * * @return a <code>double</code> value * * @exception IOException if an error occurs */ protected double readDouble() throws IOException { return DataUtils.decodeDouble(readNumber(DATA_LONG_FLOAT_LEN)); } /** * Retrieves a feature from the file * * @return the retieved feature * * @throws IOException on any file IO errors * @throws IllegalAttributeException if any of the attributes retrieved are * illegal */ public SimpleFeature readFeature() throws IOException, IllegalAttributeException { SimpleFeature result = null; Iterator iter = columns.iterator(); VPFColumn column; boolean textPrimitive = pathName.endsWith(TEXT_PRIMITIVE); int size = columns.size(); if (textPrimitive) size++; Object[] values = new Object[size]; try { for (int inx = 0; inx < columns.size(); inx++) { column = (VPFColumn) columns.get(inx); if ( column.getType().getRestrictions().isEmpty() || column.getType().getRestrictions().contains( org.opengis.filter.Filter.INCLUDE )) { values[inx] = readVariableSizeData(column.getTypeChar()); } else { values[inx] = readFixedSizeData(column.getTypeChar(), column.getElementsNumber()); } } if (textPrimitive) { values[size-1] = "nam"; } result = SimpleFeatureBuilder.build( featureType, values, null); } catch (EOFException exp) { // Should we be throwing an exception instead of eating it? exp.printStackTrace(); } return result; } /** * Retrieves a fixed amount of data from the file * * @param dataType a <code>char</code> value indicating the data type * @param instancesCount an <code>int</code> value indicating the number * of instances to retrieve. * * @return an <code>Object</code> value * * @exception IOException if an error occurs */ protected Object readFixedSizeData(char dataType, int instancesCount) throws IOException { Object result = null; switch (dataType) { case DATA_TEXT: case DATA_LEVEL1_TEXT: case DATA_LEVEL2_TEXT: case DATA_LEVEL3_TEXT: byte[] dataBytes = new byte[instancesCount * DataUtils .getDataTypeSize(dataType)]; inputStream.readFully(dataBytes); result = DataUtils.decodeData(dataBytes, dataType); break; case DATA_SHORT_FLOAT: result = new Float(readFloat()); break; case DATA_LONG_FLOAT: result = new Double(readDouble()); break; case DATA_SHORT_INTEGER: result = new Short(readShort()); break; case DATA_LONG_INTEGER: result = new Integer(readInteger()); break; case DATA_NULL_FIELD: result = "NULL"; break; case DATA_TRIPLET_ID: result = readTripletId(); break; case DATA_2_COORD_F: result = readGeometry(instancesCount, 2, false); break; case DATA_2_COORD_R: result = readGeometry(instancesCount, 2, true); break; case DATA_3_COORD_F: result = readGeometry(instancesCount, 3, false); break; case DATA_3_COORD_R: result = readGeometry(instancesCount, 3, true); break; default: break; } // end of switch (dataType) return result; } /** * Retrieves a floating point number from the file. * * @return a <code>float</code> value * * @exception IOException if an error occurs */ protected float readFloat() throws IOException { return DataUtils.decodeFloat(readNumber(DATA_SHORT_FLOAT_LEN)); } /** * Retrieves a number of attributes from the file header * * @exception VPFHeaderFormatException if an error occurs * @exception IOException if an error occurs */ protected void readHeader() throws VPFHeaderFormatException, IOException { byte[] fourBytes = new byte[4]; inputStream.readFully(fourBytes); byteOrder = readChar(); char ctrl = byteOrder; if (byteOrder == VPF_RECORD_SEPARATOR) { byteOrder = LITTLE_ENDIAN_ORDER; } else { ctrl = readChar(); } if (byteOrder == LITTLE_ENDIAN_ORDER) { fourBytes = DataUtils.toBigEndian(fourBytes); } headerLength = DataUtils.decodeInt(fourBytes); if (ctrl != VPF_RECORD_SEPARATOR) { throw new VPFHeaderFormatException( "Header format does not fit VPF file definition."); } description = readString(new String() + VPF_RECORD_SEPARATOR); narrativeTable = readString(new String() + VPF_RECORD_SEPARATOR); VPFColumn column = readColumn(); while (column != null) { columns.add(column); ctrl = readChar(); if (ctrl != VPF_FIELD_SEPARATOR) { throw new VPFHeaderFormatException( "Header format does not fit VPF file definition."); } column = readColumn(); } if (getRecordSize() < 0) { variableIndex = new VariableIndexInputStream(getVariableIndexFileName(), getByteOrder()); } } /** * Retrieves an integer value from the file * * @return an <code>int</code> value * * @exception IOException if an error occurs */ protected int readInteger() throws IOException { return DataUtils.decodeInt(readNumber(DATA_LONG_INTEGER_LEN)); } /** * Reads some byte data from the file * * @param cnt an <code>int</code> value indicating the number of bytes to retrieve * * @return a <code>byte[]</code> value * * @throws IOException if an error occurs */ protected byte[] readNumber(int cnt) throws IOException { byte[] dataBytes = new byte[cnt]; inputStream.readFully(dataBytes); if (byteOrder == LITTLE_ENDIAN_ORDER) { dataBytes = DataUtils.toBigEndian(dataBytes); } return dataBytes; } /** * Retrieves a short value from the file * * @return a <code>short</code> value * * @exception IOException if an error occurs */ protected short readShort() throws IOException { return DataUtils.decodeShort(readNumber(DATA_SHORT_INTEGER_LEN)); } /** * Reads a string value from the file * * @param terminators a <code>String</code> value indicating the terminators to look for * * @return a <code>String</code> value * * @exception IOException if an error occurs */ protected String readString(String terminators) throws IOException { StringBuffer text = new StringBuffer(); char ctrl = readChar(); if (terminators.indexOf(ctrl) != -1) { if (ctrl == VPF_FIELD_SEPARATOR) { unread(1); } return null; } while (terminators.indexOf(ctrl) == -1) { text.append(ctrl); ctrl = readChar(); } if (text.toString().equals(STRING_NULL_VALUE)) { return null; } else { return text.toString(); } } /** * Retrieves a triplet object from the file * * @return a <code>TripletId</code> value * * @throws IOException on any IO errors */ protected TripletId readTripletId() throws IOException { // TODO: does this take into account byte order properly? byte tripletDef = (byte) inputStream.read(); int dataSize = TripletId.calculateDataSize(tripletDef); byte[] tripletData = new byte[dataSize + 1]; tripletData[0] = tripletDef; if (dataSize > 0) { inputStream.readFully(tripletData, 1, dataSize); } return new TripletId(tripletData); } /** * Retrieves variable sized data from the file by first reading an integer * which indicates how many instances of the data type to retrieve * * @param dataType a <code>char</code> value indicating the data type * * @return an <code>Object</code> value * * @exception IOException if an error occurs */ protected Object readVariableSizeData(char dataType) throws IOException { int instances = readInteger(); return readFixedSizeData(dataType, instances); } /** * Resets the file stream by setting its pointer * to the first position after the header. * */ public void reset() { try { setPosition(1); } catch (IOException exc) { // This just means there is nothing in the table } } /** * Close the input stream pointed to by the object * @throws IOException in some unlikely situation */ public void close() throws IOException{ inputStream.close(); if (variableIndex != null) { variableIndex.close(); } } /** * Sets the position in the stream * * @param pos A 1-indexed position * * @throws IOException on any IO failures */ protected void setPosition(long pos) throws IOException { if (getRecordSize() < 0) { VariableIndexRow varRow = (VariableIndexRow) variableIndex.readRow((int) pos); inputStream.seek(varRow.getOffset()); } else { inputStream.seek(getAdjustedHeaderLength() + ((pos - 1) * getRecordSize())); } } /* (non-Javadoc) * @see java.lang.Object#toString() */ public String toString() { return featureType.toString(); } /** * Back up a specified number of bytes in the file stream * * @param bytes a <code>long</code> value * * @exception IOException if an error occurs */ protected void unread(long bytes) throws IOException { inputStream.seek(inputStream.getFilePointer() - bytes); } /** * Returns the full path of the variable index associated with the current file * * @return a <code>String</code> value */ private String getVariableIndexFileName() { String result = null; String fileName = getFileName(); if (fileName.toLowerCase().equals(FEATURE_CLASS_SCHEMA_TABLE)) { result = getDirectoryName().concat(File.separator).concat("fcz"); } else { result = getDirectoryName().concat(File.separator).concat(fileName .substring(0, fileName.length() - 1) + "x"); } return result; } public List<AttributeDescriptor> getAttributeDescriptors() { return featureType.getAttributeDescriptors(); } public AttributeDescriptor getDescriptor(String name) { return featureType.getDescriptor(name); } public AttributeDescriptor getDescriptor(Name name) { return featureType.getDescriptor(name); } public AttributeDescriptor getDescriptor(int index) { return featureType.getDescriptor(index); } public org.opengis.feature.type.AttributeType getType(Name name) { return featureType.getType( name ); } public org.opengis.feature.type.AttributeType getType(String name) { return featureType.getType( name ); } public org.opengis.feature.type.AttributeType getType(int index) { return featureType.getType( index ); } public List getTypes() { return featureType.getTypes(); } public CoordinateReferenceSystem getCoordinateReferenceSystem() { return featureType.getCoordinateReferenceSystem(); } public GeometryDescriptor getGeometryDescriptor() { return featureType.getGeometryDescriptor(); } public Class getBinding() { return featureType.getBinding(); } public Collection getDescriptors() { return featureType.getDescriptors(); } public boolean isInline() { return featureType.isInline(); } public List getRestrictions() { return featureType.getRestrictions(); } public org.opengis.feature.type.AttributeType getSuper() { return featureType.getSuper(); } public boolean isIdentified() { return featureType.isIdentified(); } public InternationalString getDescription() { return featureType.getDescription(); } public Name getName() { return featureType.getName(); } public int indexOf(String name) { return featureType.indexOf(name); } public int indexOf(Name name) { return featureType.indexOf(name); } public Map<Object, Object> getUserData() { return featureType.getUserData(); } }