blob: a7b9add93167e22bcae31d0db154f4934c8a57f6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.storage.shapefile;
import java.io.File;
import java.io.InputStream;
import java.sql.SQLFeatureNotSupportedException;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.util.List;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.internal.shapefile.*;
import org.apache.sis.internal.shapefile.jdbc.*;
import org.apache.sis.internal.shapefile.jdbc.connection.DBFConnection;
import org.apache.sis.internal.shapefile.jdbc.metadata.DBFDatabaseMetaData;
import org.apache.sis.internal.shapefile.jdbc.resultset.*;
import org.apache.sis.internal.shapefile.jdbc.sql.SQLIllegalParameterException;
import org.apache.sis.internal.shapefile.jdbc.sql.SQLInvalidStatementException;
import org.apache.sis.internal.shapefile.jdbc.sql.SQLUnsupportedParsingFeatureException;
import org.apache.sis.internal.shapefile.jdbc.statement.DBFStatement;
import org.apache.sis.storage.DataStoreClosedException;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.feature.AbstractFeature;
/**
* Input Stream of features.
*
* <div class="warning">This is an experimental class,
* not yet target for any Apache SIS release at this time.</div>
*
* @author Marc Le Bihan
* @version 0.5
* @since 0.5
* @module
*/
public class InputFeatureStream extends InputStream {
/** Logger. */
private static Logger LOGGER = Logging.getLogger(InputFeatureStream.class);
/** Resource bundle. */
private ResourceBundle rsc = ResourceBundle.getBundle(InputFeatureStream.class.getName());
/** Dedicated connection to DBF. */
private DBFConnection connection;
/** Statement. */
private DBFStatement stmt;
/** ResultSet. */
private DBFRecordBasedResultSet rs;
/** SQL Statement executed. */
private String sql;
/** Marks the end of file. */
private boolean endOfFile;
/** Shapefile. */
private File shapefile;
/** Database file. */
private File databaseFile;
/** Shapefile index. */
private File shapefileIndex;
/** Indicates that the shape file has a valid index provided with it. */
private boolean hasShapefileIndex;
/** Type of the features contained in this shapefile. */
private DefaultFeatureType featuresType;
/** Shapefile reader. */
private ShapefileByteReader shapefileReader;
/**
* Create an input stream of features over a connection.
* @param shpfile Shapefile.
* @param dbaseFile Database file.
* @param shpfileIndex Shapefile index, null if none provided, will be checked for existence.
* @param sqlStatement SQL Statement to run, if null, a SELECT * FROM DBF will occurs.
* @throws InvalidShapefileFormatException if the shapefile format is invalid.
* @throws InvalidDbaseFileFormatException if the Dbase file format is invalid.
* @throws ShapefileNotFoundException if the shapefile has not been found.
* @throws DbaseFileNotFoundException if the database file has not been found.
*/
public InputFeatureStream(File shpfile, File dbaseFile, File shpfileIndex, String sqlStatement) throws InvalidDbaseFileFormatException, InvalidShapefileFormatException, ShapefileNotFoundException, DbaseFileNotFoundException {
try {
this.connection = (DBFConnection)new DBFDriver().connect(dbaseFile.getAbsolutePath(), null);
if (sqlStatement == null) {
this.sql = MessageFormat.format("SELECT * FROM {0}", dbaseFile.getName());
}
else {
this.sql = sqlStatement;
}
this.shapefile = shpfile;
this.databaseFile = dbaseFile;
if (shpfileIndex != null && (shpfileIndex.exists() && shpfileIndex.isFile())) {
this.shapefileIndex = shpfileIndex;
this.hasShapefileIndex = true;
}
else {
this.hasShapefileIndex = false;
}
this.shapefileReader = new ShapefileByteReader(this.shapefile, this.databaseFile, this.shapefileIndex);
this.featuresType = this.shapefileReader.getFeaturesType();
try {
executeQuery();
}
catch(SQLConnectionClosedException e) {
// This would be an internal trouble because in this function (at least) it should be open.
throw new RuntimeException(e.getMessage(), e);
}
catch(SQLInvalidStatementException e) {
// This would be an internal trouble because if any SQL statement is executed for the dbase file initialization, it should has a correct syntax or grammar.
throw new RuntimeException(e.getMessage(), e);
}
}
catch(SQLInvalidDbaseFileFormatException ex) {
// Promote this exception to an DataStoreException compatible exception.
throw new InvalidDbaseFileFormatException(ex.getMessage(), ex);
}
catch(SQLDbaseFileNotFoundException ex) {
// Promote this exception to an DataStoreException compatible exception.
throw new DbaseFileNotFoundException(ex.getMessage(), ex);
}
catch(SQLShapefileNotFoundException ex) {
// Promote this exception to an DataStoreException compatible exception.
throw new ShapefileNotFoundException(ex.getMessage(), ex);
}
}
/**
* Create an input stream of features over a connection, responding to a SELECT * FROM DBF statement.
* @param shpfile Shapefile.
* @param dbaseFile Database file.
* @param shpfileIndex Shapefile index, null if none provided, will be checked for existence.
* @throws InvalidShapefileFormatException if the shapefile format is invalid.
* @throws InvalidDbaseFileFormatException if the Dbase file format is invalid.
* @throws ShapefileNotFoundException if the shapefile has not been found.
* @throws DbaseFileNotFoundException if the database file has not been found.
*/
public InputFeatureStream(File shpfile, File dbaseFile, File shpfileIndex) throws InvalidDbaseFileFormatException, InvalidShapefileFormatException, ShapefileNotFoundException, DbaseFileNotFoundException {
this(shpfile, dbaseFile, shpfileIndex, null);
}
/**
* Create an input stream of features over a connection, responding to a SELECT * FROM DBF statement.
* @param shpfile Shapefile.
* @param dbaseFile Database file.
* @throws InvalidShapefileFormatException if the shapefile format is invalid.
* @throws InvalidDbaseFileFormatException if the Dbase file format is invalid.
* @throws ShapefileNotFoundException if the shapefile has not been found.
* @throws DbaseFileNotFoundException if the database file has not been found.
*/
public InputFeatureStream(File shpfile, File dbaseFile) throws InvalidDbaseFileFormatException, InvalidShapefileFormatException, ShapefileNotFoundException, DbaseFileNotFoundException {
this(shpfile, dbaseFile, null);
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() {
throw new UnsupportedOperationException("InputFeatureStream doesn't allow the use of read(). Use readFeature() instead.");
}
/**
* @see java.io.InputStream#available()
*/
@Override
public int available() {
throw new UnsupportedOperationException("InputFeatureStream doesn't allow the use of available(). Use readFeature() will return null when feature are no more available.");
}
/**
* @see java.io.InputStream#close()
*/
@Override
public void close() {
this.rs.close();
this.stmt.close();
this.connection.close();
}
/**
* Read next feature responding to the SQL query.
* @return Feature, null if no more feature is available.
* @throws DataStoreClosedException if the current connection used to query the shapefile has been closed.
* @throws DataStoreQueryException if the statement used to query the shapefile content is incorrect, or requires a shapefile index to be executed and none is available.
* @throws DataStoreQueryResultException if the shapefile results cause a trouble (wrong format, for example).
* @throws InvalidShapefileFormatException if the shapefile structure shows a problem.
*/
public AbstractFeature readFeature() throws DataStoreClosedException, DataStoreQueryException, DataStoreQueryResultException, InvalidShapefileFormatException {
try {
return internalReadFeature();
}
catch(SQLConnectionClosedException e) {
throw new DataStoreClosedException(e.getMessage(), e);
}
catch(SQLInvalidStatementException | SQLIllegalParameterException | SQLNoSuchFieldException | SQLUnsupportedParsingFeatureException | SQLFeatureNotSupportedException e) {
throw new DataStoreQueryException(e.getMessage(), e);
}
catch(SQLNotNumericException | SQLNotDateException e) {
throw new DataStoreQueryResultException(e.getMessage(), e);
}
catch(SQLNoDirectAccessAvailableException e) {
throw new DataStoreQueryException(e.getMessage(), e);
}
}
/**
* Return the features type.
* @return Features type.
*/
public DefaultFeatureType getFeaturesType() {
return this.featuresType;
}
/**
* Returns the shapefile descriptor.
* @return Shapefile descriptor.
*/
public ShapefileDescriptor getShapefileDescriptor() {
return this.shapefileReader.getShapefileDescriptor();
}
/**
* Returns the database fields descriptors.
* @return List of fields descriptors.
*/
public List<DBase3FieldDescriptor> getDatabaseFieldsDescriptors() {
return this.shapefileReader.getFieldsDescriptors();
}
/**
* Checks if the shapefile has an index provided with it.
* @return true if an index file (.shx) has been given with the shapefile.
*/
public boolean hasShapefileIndex() {
return this.hasShapefileIndex;
}
/**
* Read next feature responding to the SQL query.
* @return Feature, null if no more feature is available.
* @throws SQLNotNumericException if a field expected numeric isn't.
* @throws SQLNotDateException if a field expected of date kind, isn't.
* @throws SQLNoSuchFieldException if a field doesn't exist.
* @throws SQLIllegalParameterException if a parameter is illegal in the query.
* @throws SQLInvalidStatementException if the SQL statement is invalid.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLUnsupportedParsingFeatureException if a SQL ability is not currently available through this driver.
* @throws SQLFeatureNotSupportedException if a SQL ability is not currently available through this driver.
* @throws InvalidShapefileFormatException if the shapefile format is invalid.
* @throws SQLNoDirectAccessAvailableException if the underlying SQL statement requires a direct access in the shapefile, but the shapefile cannot allow it.
*/
private AbstractFeature internalReadFeature() throws SQLConnectionClosedException, SQLInvalidStatementException, SQLIllegalParameterException, SQLNoSuchFieldException, SQLUnsupportedParsingFeatureException, SQLNotNumericException, SQLNotDateException, SQLFeatureNotSupportedException, InvalidShapefileFormatException, SQLNoDirectAccessAvailableException {
try {
if (this.endOfFile) {
return null;
}
int previousRecordNumber = this.rs.getRowNum();
if (this.rs.next() == false) {
this.endOfFile = true;
return null;
}
int currentRecordNumber = this.rs.getRowNum();
// On the shapefile, only jump in another place if a direct access is needed.
boolean directAccesRequired = currentRecordNumber != (previousRecordNumber + 1);
if (directAccesRequired) {
try {
if (LOGGER.isLoggable(Level.FINER)) {
MessageFormat format = new MessageFormat(this.rsc.getString("log.shapefile_reading_with_direct_access"));
LOGGER.finer(format.format(new Object[] {previousRecordNumber, currentRecordNumber}));
}
this.shapefileReader.setRowNum(currentRecordNumber);
}
catch(SQLInvalidRecordNumberForDirectAccessException e) {
// This would be an internal API problem, because as soon as we handle a shapefile index, we shall go through its relative shape feature file correctly.
throw new RuntimeException(e.getMessage(), e);
}
}
else {
if (LOGGER.isLoggable(Level.FINER)) {
MessageFormat format = new MessageFormat(this.rsc.getString("log.shapefile_reading_with_sequential_access"));
LOGGER.finer(format.format(new Object[] {previousRecordNumber, currentRecordNumber}));
}
}
AbstractFeature feature = this.featuresType.newInstance();
this.shapefileReader.completeFeature(feature);
DBFDatabaseMetaData metadata = (DBFDatabaseMetaData)this.connection.getMetaData();
try(DBFBuiltInMemoryResultSetForColumnsListing rsDatabase = (DBFBuiltInMemoryResultSetForColumnsListing)metadata.getColumns(null, null, null, null)) {
while(rsDatabase.next()) {
String fieldName = rsDatabase.getString("COLUMN_NAME");
Object fieldValue = this.rs.getObject(fieldName);
// FIXME To allow features to be filled again, the values are converted to String again : feature should allow any kind of data.
String stringValue;
if (fieldValue == null) {
stringValue = null;
}
else {
if (fieldValue instanceof Integer || fieldValue instanceof Long) {
stringValue = MessageFormat.format("{0,number,#0}", fieldValue); // Avoid thousand separator.
}
else {
if (fieldValue instanceof Double || fieldValue instanceof Float) {
// Avoid thousand separator.
DecimalFormat df = new DecimalFormat();
df.setGroupingUsed(false);
stringValue = df.format(fieldValue);
}
else
stringValue = fieldValue.toString();
}
}
feature.setPropertyValue(fieldName, stringValue);
}
return feature;
}
catch(SQLNoResultException e) {
// This an internal trouble, if it occurs.
throw new RuntimeException(e.getMessage(), e);
}
}
catch(SQLNoResultException e) {
// We are trying to prevent this. If it occurs, we have an internal problem.
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Execute the wished SQL query.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLInvalidStatementException if the given SQL Statement is invalid.
*/
private void executeQuery() throws SQLConnectionClosedException, SQLInvalidStatementException {
this.stmt = (DBFStatement)this.connection.createStatement();
this.rs = (DBFRecordBasedResultSet)this.stmt.executeQuery(this.sql);
}
}