/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2015, 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.sort;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.List;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKBReader;
import com.vividsolutions.jts.io.WKBWriter;
/**
* Allows writing and reading features to/from the given file
*
* @author Andrea Aime - GeoSolutions
*/
public class SimpleFeatureIO {
RandomAccessFile raf;
SimpleFeatureType schema;
SimpleFeatureBuilder builder;
File file;
public SimpleFeatureIO(File file, SimpleFeatureType schema) throws FileNotFoundException {
this.file = file;
this.raf = new RandomAccessFile(file, "rw");
this.schema = schema;
this.builder = new SimpleFeatureBuilder(schema);
}
/**
* Writes the feature to the file
*
* @param sf
* @throws IOException
*/
public void write(SimpleFeature sf) throws IOException {
// write each attribute in the random access file
List<AttributeDescriptor> attributes = schema.getAttributeDescriptors();
// write feature id
raf.writeUTF(sf.getID());
// write the attributes
for (AttributeDescriptor ad : attributes) {
Object value = sf.getAttribute(ad.getLocalName());
writeAttribute(ad, value);
}
}
void writeAttribute(AttributeDescriptor ad, Object value) throws IOException {
if (value == null) {
// null marker
raf.writeBoolean(true);
} else {
// not null, write the contents. This one requires some explanation. We are not
// writing any type metadata in the stream for the types we can optimize (primitives,
// numbers,
// strings and the like). This means we have to be 100% sure the class we're writing is
// actually the one we can optimize for, and not some subclass. Thus, we are authorized
// to use identity comparison instead of isAssignableFrom or equality, when we read back
// it must be as if we did not serialize stuff at all
raf.writeBoolean(false);
Class<?> binding = ad.getType().getBinding();
if (binding == Boolean.class) {
raf.writeBoolean((Boolean) value);
} else if (binding == Byte.class || binding == byte.class) {
raf.writeByte((Byte) value);
} else if (binding == Short.class || binding == short.class) {
raf.writeShort((Short) value);
} else if (binding == Integer.class || binding == int.class) {
raf.writeInt((Integer) value);
} else if (binding == Long.class || binding == long.class) {
raf.writeLong((Long) value);
} else if (binding == Float.class || binding == float.class) {
raf.writeFloat((Float) value);
} else if (binding == Double.class || binding == double.class) {
raf.writeDouble((Double) value);
} else if (binding == String.class) {
raf.writeUTF((String) value);
} else if (binding == java.sql.Date.class || binding == java.sql.Time.class
|| binding == java.sql.Timestamp.class || binding == java.util.Date.class) {
raf.writeLong(((Date) value).getTime());
} else if (Geometry.class.isAssignableFrom(binding)) {
WKBWriter writer = new WKBWriter();
byte[] buffer = writer.write((Geometry) value);
int length = buffer.length;
raf.writeInt(length);
raf.write(buffer);
} else {
// can't optimize, in this case we use an ObjectOutputStream to write out
// full metadata
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(value);
oos.flush();
byte[] bytes = bos.toByteArray();
raf.writeInt(bytes.length);
raf.write(bytes);
}
}
}
/**
* Reads the next feature form the file
*
* @return
* @throws IOException
*/
public SimpleFeature read() throws IOException {
// read the fid, check for file end
String fid = raf.readUTF();
// read the other attributes, build the feature
for (AttributeDescriptor ad : schema.getAttributeDescriptors()) {
Object att = readAttribute(ad);
builder.add(att);
}
// return the feature
return builder.buildFeature(fid);
}
/**
* Reads the attributes.
*
* @param ad
* @return
* @throws IOException
*/
Object readAttribute(AttributeDescriptor ad) throws IOException {
// See the comments in {@link MergeSortDumper#writeAttribute(RandomAccessFile,
// AttributeDescriptor, Object)} to get an insight on why the method is built like this
boolean isNull = raf.readBoolean();
if (isNull) {
return null;
} else {
Class<?> binding = ad.getType().getBinding();
if (binding == Boolean.class) {
return raf.readBoolean();
} else if (binding == Byte.class || binding == byte.class) {
return raf.readByte();
} else if (binding == Short.class || binding == short.class) {
return raf.readShort();
} else if (binding == Integer.class || binding == int.class) {
return raf.readInt();
} else if (binding == Long.class || binding == long.class) {
return raf.readLong();
} else if (binding == Float.class || binding == float.class) {
return raf.readFloat();
} else if (binding == Double.class || binding == double.class) {
return raf.readDouble();
} else if (binding == String.class) {
return raf.readUTF();
} else if (binding == java.sql.Date.class) {
return new java.sql.Date(raf.readLong());
} else if (binding == java.sql.Time.class) {
return new java.sql.Time(raf.readLong());
} else if (binding == java.sql.Timestamp.class) {
return new java.sql.Timestamp(raf.readLong());
} else if (binding == java.util.Date.class) {
return new java.util.Date(raf.readLong());
} else if (Geometry.class.isAssignableFrom(binding)) {
WKBReader reader = new WKBReader();
int length = raf.readInt();
byte[] buffer = new byte[length];
raf.read(buffer);
try {
return reader.read(buffer);
} catch (ParseException e) {
throw new IOException("Failed to parse the geometry WKB", e);
}
} else {
int length = raf.readInt();
byte[] buffer = new byte[length];
raf.read(buffer);
ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
ObjectInputStream ois = new ObjectInputStream(bis);
try {
return ois.readObject();
} catch (ClassNotFoundException e) {
throw new IOException("Could not read back object", e);
}
}
}
}
/**
* Moves the IO to the specified offset in the file
*
* @param offset
* @throws IOException
*/
public void seek(long offset) throws IOException {
raf.seek(offset);
}
/**
* Returns the current reading position in the file
*
* @return
* @throws IOException
*/
public long getOffset() throws IOException {
return raf.getFilePointer();
}
/**
* Returns true if the end of file has been reached
*
* @return
* @throws IOException
*/
public boolean endOfFile() throws IOException {
return getOffset() >= raf.length();
}
/**
* Closes the IO, eventually deleting the file in the process
*
* @param deleteFile
* @throws IOException
*/
public void close(boolean deleteFile) throws IOException {
try {
raf.close();
} finally {
if (deleteFile) {
file.delete();
}
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "SimpleFeatureIO [schema=" + schema + ", file=" + file + "]";
}
}