/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.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.
*/
package org.geotoolkit.data.shapefile;
import org.geotoolkit.data.shapefile.lock.StorageFile;
import org.geotoolkit.data.shapefile.lock.ShpFiles;
import static org.geotoolkit.data.shapefile.lock.ShpFileType.DBF;
import static org.geotoolkit.data.shapefile.lock.ShpFileType.SHP;
import static org.geotoolkit.data.shapefile.lock.ShpFileType.SHX;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import org.geotoolkit.data.FeatureReader;
import org.geotoolkit.data.FeatureWriter;
import org.geotoolkit.data.dbf.DbaseFileException;
import org.geotoolkit.data.dbf.DbaseFileHeader;
import org.geotoolkit.data.dbf.DbaseFileWriter;
import org.geotoolkit.data.shapefile.shp.JTSUtilities;
import org.geotoolkit.data.shapefile.shp.ShapeHandler;
import org.geotoolkit.data.shapefile.shp.ShapeType;
import org.geotoolkit.data.shapefile.shp.ShapefileWriter;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.sis.feature.FeatureExt;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.data.FeatureStoreContentEvent;
import org.geotoolkit.data.FeatureStoreRuntimeException;
import org.geotoolkit.data.shapefile.lock.AccessManager;
import org.geotoolkit.data.shapefile.lock.ShpFileType;
import org.geotoolkit.factory.FactoryFinder;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyNotFoundException;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.identity.Identifier;
/**
* A FeatureWriter for ShapefileDataStore. Uses a write and annotate technique
* to avoid buffering attributes and geometries. Because the shapefile and dbf
* require header information which can only be obtained by reading the entire
* series of Features, the headers are updated after the initial write
* completes.
*
* @author Jesse Eichar
* @module
*/
public class ShapefileFeatureWriter implements FeatureWriter {
protected final FilterFactory FF = FactoryFinder.getFilterFactory(null);
protected final ShapefileFeatureStore parent;
// the FeatureReader<SimpleFeatureType, SimpleFeature> to obtain the current Feature from
protected FeatureReader featureReader;
// the AttributeReader
protected final ShapefileAttributeReader attReader;
// the current Feature
protected Feature currentFeature;
/** Initial value for current feature */
protected Feature originalFeature;
// the FeatureType we are representing
protected final FeatureType featureType;
// an array for reuse in Feature creation
//protected final Object[] emptyAtts;
// an array for reuse in writing to dbf.
protected final Object[] transferCache;
protected ShapeType shapeType;
protected ShapeHandler handler;
// keep track of shapefile length during write, starts at 100 bytes for
// required header
protected int shapefileLength = 100;
// keep track of the number of records written
protected int records = 0;
// hold 1 if dbf should write the attribute at the index, 0 if not
protected final byte[] writeFlags;
protected ShapefileWriter shpWriter;
protected DbaseFileWriter dbfWriter;
private DbaseFileHeader dbfHeader;
protected final Set<Identifier> deletedIds = new HashSet<>();
protected final Set<Identifier> updatedIds = new HashSet<>();
protected final Set<Identifier> addedIds = new HashSet<>();
protected final Map<ShpFileType, StorageFile> storageFiles = new HashMap<>();
// keep track of bounds during write
protected Envelope bounds = new Envelope();
protected final ShpFiles shpFiles;
private final FileChannel dbfChannel;
private final Charset dbfCharset;
//Runnable used after closing shapefile, to rebuild indexes for example
protected Runnable postClose = null;
public ShapefileFeatureWriter(final ShapefileFeatureStore parent, final String typeName, final ShpFiles shpFiles, final ShapefileAttributeReader attsReader,
final FeatureReader featureReader, final Charset charset) throws IOException,DataStoreException {
this.parent = parent;
this.shpFiles = shpFiles;
this.dbfCharset = charset;
// set up reader
this.attReader = attsReader;
this.featureReader = featureReader;
storageFiles.put(SHP, getLocker().getStorageFile(SHP));
storageFiles.put(SHX, getLocker().getStorageFile(SHX));
storageFiles.put(DBF, getLocker().getStorageFile(DBF));
this.featureType = featureReader.getFeatureType();
try {
this.featureType.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
} catch(PropertyNotFoundException ex) {
throw new DataStoreException("Missing identifier property in feature type");
}
// set up buffers and write flags
List<AttributeType> attributes = parent.getAttributes(featureType,false);
writeFlags = new byte[attributes.size()];
int cnt = 0;
for (int i=0, n=attributes.size(); i<n; i++) {
// if its a geometry, we don't want to write it to the dbf...
if (!( Geometry.class.isAssignableFrom(attributes.get(i).getValueClass()))) {
cnt++;
writeFlags[i] = (byte) 1;
}
}
// dbf transfer buffer
transferCache = new Object[cnt];
// open underlying writers
final FileChannel shpChannel = storageFiles.get(SHP).getWriteChannel();
final FileChannel shxChannel = storageFiles.get(SHX).getWriteChannel();
shpWriter = new ShapefileWriter(shpChannel, shxChannel);
dbfHeader = DbaseFileHeader.createDbaseHeader(featureType);
dbfChannel = storageFiles.get(DBF).getWriteChannel();
dbfWriter = new DbaseFileWriter(dbfHeader, dbfChannel, dbfCharset);
if(attReader != null) {
// don't try to read a shx file we're writing to in parallel
attReader.shp.disableShxUsage();
if(attReader.hasNext()) {
shapeType = attReader.shp.getHeader().getShapeType();
handler = shapeType.getShapeHandler(true);
shpWriter.writeHeaders(bounds, shapeType, records, shapefileLength);
}
}
}
public final AccessManager getLocker(){
return attReader.getLocker();
}
/**
* Go back and update the headers with the required info.
*
* @throws IOException DOCUMENT ME!
*/
protected void flush() throws IOException {
// not sure the check for records <=0 is necessary,
// but if records > 0 and shapeType is null there's probably
// another problem.
if ((records <= 0) && (shapeType == null)) {
final AttributeType geometryAttributeType = FeatureExt.getDefaultGeometryAttribute(featureType);
if(geometryAttributeType != null){
final Class gat = geometryAttributeType.getValueClass();
shapeType = ShapeType.findBestGeometryType(gat);
if (shapeType == ShapeType.UNDEFINED) {
throw new IOException("Cannot handle geometry class : "+ (gat == null ? "null" : gat.getName()));
}
}else{
shapeType = ShapeType.NULL;
}
}
shpWriter.writeHeaders(bounds, shapeType, records, shapefileLength);
dbfHeader.setNumRecords(records);
dbfChannel.position(0);
dbfHeader.writeHeader(dbfChannel);
}
/**
* In case someone doesn't close me.
*
* @throws Throwable DOCUMENT ME!
*/
@Override
protected void finalize() throws Throwable {
if (featureReader != null) {
try {
close();
} catch (Exception e) {
// oh well, we tried
}
}
}
/**
* Clean up our temporary write if there was one
*
* @throws IOException DOCUMENT ME!
*/
protected void clean() throws IOException {
getLocker().disposeReaderAndWriters();
getLocker().replaceStorageFiles(postClose);
}
/**
* Release resources and flush the header information.
*
* @throws IOException DOCUMENT ME!
*/
@Override
public void close() throws FeatureStoreRuntimeException {
if (featureReader == null) {
throw new FeatureStoreRuntimeException("Writer closed");
}
// make sure to write the last feature...
if (currentFeature != null) {
write();
}
// if the attribute reader is here, that means we may have some
// additional tail-end file flushing to do if the Writer was closed
// before the end of the file
try{
if (attReader != null && attReader.hasNext()) {
shapeType = attReader.shp.getHeader().getShapeType();
handler = shapeType.getShapeHandler(true);
// handle the case where zero records have been written, but the
// stream is closed and the headers
if (records == 0) {
shpWriter.writeHeaders(bounds, shapeType, 0, 0);
}
// copy array for bounds
final double[] env = new double[4];
while (attReader.hasNext()) {
// transfer bytes from shapefile
shapefileLength += attReader.shp.transferTo(shpWriter,++records, env);
// bounds update
bounds.expandToInclude(env[0], env[1]);
bounds.expandToInclude(env[2], env[3]);
// transfer dbf bytes
attReader.dbf.transferTo(dbfWriter);
}
}
}catch(IOException ex){
throw new FeatureStoreRuntimeException(ex);
}catch(DataStoreException ex){
throw new FeatureStoreRuntimeException(ex);
}
doClose();
try {
clean();
} catch (IOException ex) {
throw new FeatureStoreRuntimeException(ex);
}
fireDataChangeEvents();
}
private void fireDataChangeEvents(){
if (!addedIds.isEmpty()) {
final FeatureStoreContentEvent event = new FeatureStoreContentEvent(this, FeatureStoreContentEvent.Type.ADD, featureType.getName(), FF.id(addedIds));
parent.forwardContentEvent(event);
}
if (!updatedIds.isEmpty()) {
final FeatureStoreContentEvent event = new FeatureStoreContentEvent(this, FeatureStoreContentEvent.Type.UPDATE, featureType.getName(), FF.id(updatedIds));
parent.forwardContentEvent(event);
}
if (!deletedIds.isEmpty()) {
final FeatureStoreContentEvent event = new FeatureStoreContentEvent(this, FeatureStoreContentEvent.Type.DELETE, featureType.getName(), FF.id(deletedIds));
parent.forwardContentEvent(event);
}
}
protected void doClose() throws FeatureStoreRuntimeException {
// close reader, flush headers, and copy temp files, if any
try {
featureReader.close();
} finally {
try {
flush();
} catch(IOException ex){
throw new FeatureStoreRuntimeException(ex);
}finally {
try {
shpWriter.close();
dbfWriter.close();
} catch (IOException ex) {
throw new FeatureStoreRuntimeException(ex);
}
}
featureReader = null;
shpWriter = null;
dbfWriter = null;
}
}
/**
* {@inheritDoc }
*/
@Override
public FeatureType getFeatureType() {
return featureType;
}
/**
* {@inheritDoc }
*/
@Override
public boolean hasNext() throws FeatureStoreRuntimeException {
if (featureReader == null) {
throw new FeatureStoreRuntimeException("Writer closed");
}
return featureReader.hasNext();
}
/**
* {@inheritDoc }
*/
@Override
public Feature next() throws FeatureStoreRuntimeException {
// closed already, error!
if (featureReader == null) {
throw new FeatureStoreRuntimeException("Writer closed");
}
// we have to write the current feature back into the stream
if (currentFeature != null) {
write();
}
// is there another? If so, return it
if (featureReader.hasNext()) {
try {
currentFeature = featureReader.next();
originalFeature = FeatureExt.copy(currentFeature);
return currentFeature;
} catch (IllegalArgumentException iae) {
throw new FeatureStoreRuntimeException("Error in reading", iae);
}
}
// reader has no more (no were are adding to the file)
// so return an empty feature
try {
final String featureID = getFeatureType().getName().tip().toString()+"."+(records+1);
originalFeature = null;
currentFeature = getFeatureType().newInstance();
currentFeature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), featureID);
return currentFeature;
} catch (IllegalArgumentException iae) {
throw new FeatureStoreRuntimeException("Error creating empty Feature", iae);
}
}
/**
* Called when a new feature is being created and a new fid is required
*
* @return a fid for the new feature
*/
protected String nextFeatureId() {
return getFeatureType().getName().tip().toString()+"."+(records+1);
}
/**
* {@inheritDoc }
*/
@Override
public void remove() throws FeatureStoreRuntimeException {
if (featureReader == null) {
throw new FeatureStoreRuntimeException("Writer closed");
}
if (currentFeature == null) {
throw new FeatureStoreRuntimeException("Current feature is null");
}
deletedIds.add(FeatureExt.getId(currentFeature));
// mark the current feature as null, this will result in it not
// being rewritten to the stream
currentFeature = null;
}
/**
* {@inheritDoc }
*/
@Override
public void write() throws FeatureStoreRuntimeException {
if (currentFeature == null) {
throw new FeatureStoreRuntimeException("Current feature is null");
}
if (featureReader == null) {
throw new FeatureStoreRuntimeException("Writer closed");
}
// writing of Geometry
Geometry g = (Geometry) currentFeature.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
// if this is the first Geometry, find the shapeType and handler
if (shapeType == null) {
int dims = 2;
if(g != null){
dims = JTSUtilities.guessCoorinateDims(g.getCoordinates());
}
try {
shapeType = JTSUtilities.getShapeType(g, dims);
// we must go back and annotate this after writing
shpWriter.writeHeaders(new Envelope(), shapeType, 0, 0);
handler = shapeType.getShapeHandler(true);
} catch (Exception se) {
throw new FeatureStoreRuntimeException("Unexpected Error", se);
}
}
// convert geometry
g = JTSUtilities.convertToCollection(g, shapeType);
// bounds calculations
Envelope b = g.getEnvelopeInternal();
if (!b.isNull()) {
bounds.expandToInclude(b);
}
// file length update
shapefileLength += (handler.getLength(g) + 8);
try {
// write it
shpWriter.writeGeometry(g);
} catch (IOException ex) {
throw new FeatureStoreRuntimeException(ex);
}
// writing of attributes
int idx = 0;
List<AttributeType> attributes = parent.getAttributes(featureType,false);
for (int i = 0, ii = attributes.size(); i < ii; i++) {
// skip geometries
if (writeFlags[i] > 0) {
transferCache[idx++] = currentFeature.getPropertyValue(attributes.get(i).getName().toString());
}
}
try {
dbfWriter.write(transferCache);
} catch (IOException ex) {
throw new FeatureStoreRuntimeException(ex);
} catch (DbaseFileException ex) {
throw new FeatureStoreRuntimeException(ex);
}
// one more down...
records++;
if (originalFeature == null) {
addedIds.add(FeatureExt.getId(currentFeature));
} else if (!originalFeature.equals(currentFeature)) {
updatedIds.add(FeatureExt.getId(currentFeature));
}
// clear the currentFeature
currentFeature = null;
}
}