/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2011, 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.shapefile.ng;
import static org.geotools.data.shapefile.ng.files.ShpFileType.*;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Level;
import org.geotools.data.DataSourceException;
import org.geotools.data.FeatureReader;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ng.dbf.DbaseFileException;
import org.geotools.data.shapefile.ng.dbf.DbaseFileHeader;
import org.geotools.data.shapefile.ng.files.ShpFiles;
import org.geotools.data.shapefile.ng.files.StorageFile;
import org.geotools.data.shapefile.ng.shp.ShapeType;
import org.geotools.data.shapefile.ng.shp.ShapefileWriter;
import org.geotools.data.store.ContentDataStore;
import org.geotools.data.store.ContentEntry;
import org.geotools.data.store.ContentFeatureSource;
import org.geotools.feature.FeatureTypes;
import org.geotools.feature.NameImpl;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
public class ShapefileDataStore extends ContentDataStore {
// User-data keyword names for storing the original field name and its
// instance number when the field name is replaced with a new name built
// using the original+count convention.
public static final String ORIGINAL_FIELD_NAME = "original";
public static final String ORIGINAL_FIELD_DUPLICITY_COUNT = "count";
public static final Charset DEFAULT_STRING_CHARSET = (Charset) ShapefileDataStoreFactory.DBFCHARSET
.getDefaultValue();
public static final TimeZone DEFAULT_TIMEZONE = (TimeZone) ShapefileDataStoreFactory.DBFTIMEZONE
.getDefaultValue();
/**
* When true, the stack trace that got a lock that wasn't released is recorded and then printed
* out when warning the user about this.
*/
protected static final Boolean TRACE_ENABLED = "true".equalsIgnoreCase(System
.getProperty("gt2.shapefile.trace"));
/**
* The stack trace used to track code that grabs the data store without disposing it
*/
Exception trace;
ShpFiles shpFiles;
Charset charset = DEFAULT_STRING_CHARSET;
TimeZone timeZone = DEFAULT_TIMEZONE;
boolean memoryMapped = false;
boolean bufferCachingEnabled = true;
boolean indexed = true;
boolean fidIndexed = true;
IndexManager indexManager;
ShapefileSetManager shpManager;
public ShapefileDataStore(URL url) {
shpFiles = new ShpFiles(url);
if (TRACE_ENABLED) {
trace = new Exception();
trace.fillInStackTrace();
}
shpManager = new ShapefileSetManager(shpFiles, this);
indexManager = new IndexManager(shpFiles, this);
}
@Override
protected List<Name> createTypeNames() throws IOException {
return Collections.singletonList(getTypeName());
}
Name getTypeName() {
return new NameImpl(namespaceURI, shpFiles.getTypeName());
}
@Override
protected ContentFeatureSource createFeatureSource(ContentEntry entry) throws IOException {
return getFeatureSource();
}
public ContentFeatureSource getFeatureSource() throws IOException {
ContentEntry entry = ensureEntry(getTypeName());
if (shpFiles.isWritable()) {
return new ShapefileFeatureStore(entry, shpFiles);
} else {
return new ShapefileFeatureSource(entry, shpFiles);
}
}
public Charset getCharset() {
return charset;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
public TimeZone getTimeZone() {
return timeZone;
}
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
public boolean isMemoryMapped() {
return memoryMapped;
}
public void setMemoryMapped(boolean memoryMapped) {
this.memoryMapped = memoryMapped;
}
public boolean isBufferCachingEnabled() {
return bufferCachingEnabled;
}
public void setBufferCachingEnabled(boolean bufferCachingEnabled) {
this.bufferCachingEnabled = bufferCachingEnabled;
}
public boolean isIndexed() {
return indexed;
}
public void setIndexed(boolean indexed) {
this.indexed = indexed;
}
public SimpleFeatureType getSchema() throws IOException {
return getSchema(getTypeName());
}
public FeatureReader<SimpleFeatureType, SimpleFeature> getFeatureReader() throws IOException {
return super.getFeatureReader(new Query(getTypeName().getLocalPart()),
Transaction.AUTO_COMMIT);
}
public long getCount(Query query) throws IOException {
return getFeatureSource().getCount(query);
}
/**
* Set the FeatureType of this DataStore. This method will delete any existing local resources
* or throw an IOException if the DataStore is remote.
*
* @param featureType The desired FeatureType.
*
* @throws IOException If the DataStore is remote.
*/
public void createSchema(SimpleFeatureType featureType) throws IOException {
if (!shpFiles.isLocal()) {
throw new IOException("Cannot create FeatureType on remote or in-classpath shapefile");
}
shpFiles.delete();
CoordinateReferenceSystem crs = featureType.getGeometryDescriptor()
.getCoordinateReferenceSystem();
final Class<?> geomType = featureType.getGeometryDescriptor().getType().getBinding();
final ShapeType shapeType;
if (Point.class.isAssignableFrom(geomType)) {
shapeType = ShapeType.POINT;
} else if (MultiPoint.class.isAssignableFrom(geomType)) {
shapeType = ShapeType.MULTIPOINT;
} else if (LineString.class.isAssignableFrom(geomType)
|| MultiLineString.class.isAssignableFrom(geomType)) {
shapeType = ShapeType.ARC;
} else if (Polygon.class.isAssignableFrom(geomType)
|| MultiPolygon.class.isAssignableFrom(geomType)) {
shapeType = ShapeType.POLYGON;
} else {
throw new DataSourceException("Cannot create a shapefile whose geometry type is "
+ geomType);
}
StorageFile shpStoragefile = shpFiles.getStorageFile(SHP);
StorageFile shxStoragefile = shpFiles.getStorageFile(SHX);
StorageFile dbfStoragefile = shpFiles.getStorageFile(DBF);
StorageFile prjStoragefile = shpFiles.getStorageFile(PRJ);
FileChannel shpChannel = shpStoragefile.getWriteChannel();
FileChannel shxChannel = shxStoragefile.getWriteChannel();
ShapefileWriter writer = new ShapefileWriter(shpChannel, shxChannel);
try {
// by spec, if the file is empty, the shape envelope should be ignored
writer.writeHeaders(new Envelope(), shapeType, 0, 100);
} finally {
writer.close();
assert !shpChannel.isOpen();
assert !shxChannel.isOpen();
}
DbaseFileHeader dbfheader = createDbaseHeader(featureType);
dbfheader.setNumRecords(0);
WritableByteChannel dbfChannel = dbfStoragefile.getWriteChannel();
try {
dbfheader.writeHeader(dbfChannel);
} finally {
dbfChannel.close();
}
if (crs != null) {
String s = crs.toWKT();
// .prj files should have no carriage returns in them, this
// messes up
// ESRI's ArcXXX software, so we'll be compatible
s = s.replaceAll("\n", "").replaceAll(" ", "");
FileWriter prjWriter = new FileWriter(prjStoragefile.getFile());
try {
prjWriter.write(s);
} finally {
prjWriter.close();
}
} else {
LOGGER.warning("PRJ file not generated for null CoordinateReferenceSystem");
}
StorageFile
.replaceOriginals(shpStoragefile, shxStoragefile, dbfStoragefile, prjStoragefile);
}
/**
* Attempt to create a DbaseFileHeader for the FeatureType. Note, we cannot set the number of
* records until the write has completed.
*
* @param featureType DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws IOException DOCUMENT ME!
* @throws DbaseFileException DOCUMENT ME!
*/
protected static DbaseFileHeader createDbaseHeader(SimpleFeatureType featureType)
throws IOException, DbaseFileException {
DbaseFileHeader header = new DbaseFileHeader();
for (int i = 0, ii = featureType.getAttributeCount(); i < ii; i++) {
AttributeDescriptor type = featureType.getDescriptor(i);
Class<?> colType = type.getType().getBinding();
String colName = type.getLocalName();
int fieldLen = FeatureTypes.getFieldLength(type);
if (fieldLen == FeatureTypes.ANY_LENGTH)
fieldLen = 255;
if ((colType == Integer.class) || (colType == Short.class) || (colType == Byte.class)) {
header.addColumn(colName, 'N', Math.min(fieldLen, 9), 0);
} else if (colType == Long.class) {
header.addColumn(colName, 'N', Math.min(fieldLen, 19), 0);
} else if (colType == BigInteger.class) {
header.addColumn(colName, 'N', Math.min(fieldLen, 33), 0);
} else if (Number.class.isAssignableFrom(colType)) {
int l = Math.min(fieldLen, 33);
int d = Math.max(l - 2, 0);
header.addColumn(colName, 'N', l, d);
// This check has to come before the Date one or it is never reached
// also, this field is only activated with the following system property:
// org.geotools.shapefile.datetime=true
} else if (java.util.Date.class.isAssignableFrom(colType)
&& Boolean.getBoolean("org.geotools.shapefile.datetime")) {
header.addColumn(colName, '@', fieldLen, 0);
} else if (java.util.Date.class.isAssignableFrom(colType)
|| Calendar.class.isAssignableFrom(colType)) {
header.addColumn(colName, 'D', fieldLen, 0);
} else if (colType == Boolean.class) {
header.addColumn(colName, 'L', 1, 0);
} else if (CharSequence.class.isAssignableFrom(colType)) {
// Possible fix for GEOT-42 : ArcExplorer doesn't like 0 length
// ensure that maxLength is at least 1
header.addColumn(colName, 'C', Math.min(254, fieldLen), 0);
} else if (Geometry.class.isAssignableFrom(colType)) {
continue;
} else {
throw new IOException("Unable to write : " + colType.getName());
}
}
return header;
}
/**
* This method is used to force the creation of a .prj file.
* <p>
* The internally cached FeatureType will be removed, so the next call to getSchema() will read
* in the created file. This method is not thread safe and will have dire consequences for any
* other thread making use of the shapefile.
* <p>
*
* @param crs
*/
public void forceSchemaCRS(CoordinateReferenceSystem crs) throws IOException {
if (crs == null)
throw new NullPointerException("CRS required for .prj file");
String s = crs.toWKT();
s = s.replaceAll("\n", "").replaceAll(" ", "");
StorageFile storageFile = shpFiles.getStorageFile(PRJ);
FileWriter out = new FileWriter(storageFile.getFile());
try {
out.write(s);
} finally {
out.close();
}
storageFile.replaceOriginal();
entries.clear();
}
@Override
public void dispose() {
super.dispose();
if (shpFiles != null) {
shpFiles.dispose();
shpFiles = null;
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (shpFiles != null && trace != null) {
LOGGER.log(Level.SEVERE,
"Undisposed of shapefile, you should call dispose() on all shapefile stores",
trace);
}
dispose();
}
/**
* Returns true if the store uses the .fix index file for feature ids. The .fix file speeds up
* filters by feature id and allows for stable ids in face of feature removals, without it the
* feature id is simply the position of the feature in the shapefile, something which changes
* when data is removed
*
* @return
*/
public boolean isFidIndexed() {
return fidIndexed;
}
/**
* Enables/disables the feature id index. The index is enabled by default
* @param fidIndexed
*/
public void setFidIndexed(boolean fidIndexed) {
this.fidIndexed = fidIndexed;
}
@Override
public String toString() {
return "ShapefileDataStore [file=" + shpFiles.get(SHP) + ", charset=" + charset + ", timeZone=" + timeZone
+ ", memoryMapped=" + memoryMapped + ", bufferCachingEnabled="
+ bufferCachingEnabled + ", indexed=" + indexed + ", fidIndexed=" + fidIndexed
+ "]";
}
}