blob: 86d799b205bcc98dc4ae0792e38e2c7f3b08b9b4 [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.internal.shapefile.jdbc.resultset;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.sql.Date;
import java.sql.ResultSetMetaData;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import org.apache.sis.internal.shapefile.jdbc.SQLConnectionClosedException;
import org.apache.sis.internal.shapefile.jdbc.connection.DBFConnection;
import org.apache.sis.internal.shapefile.jdbc.metadata.DBFResultSetMataData;
import org.apache.sis.internal.shapefile.jdbc.sql.*;
import org.apache.sis.internal.shapefile.jdbc.statement.DBFStatement;
/**
* A ResultSet based on a record.
* @author Marc LE BIHAN
*/
public class DBFRecordBasedResultSet extends DBFResultSet {
/** The current record. */
private Map<String, byte[]> record;
/** Condition of where clause (currently, only one is handled). */
private ConditionalClauseResolver singleConditionOfWhereClause;
/** Indicates that the last result set record matching conditions has already been returned, and a further call of next() shall throw a "no more record" exception. */
private boolean lastResultSetRecordAlreadyReturned;
/** The record number of this record. */
private int recordNumber;
/**
* Constructs a result set.
* @param stmt Parent statement.
* @param sqlQuery SQL Statment that produced this ResultSet.
* @throws SQLInvalidStatementException if the SQL Statement is invalid.
*/
public DBFRecordBasedResultSet(final DBFStatement stmt, String sqlQuery) throws SQLInvalidStatementException {
super(stmt, sqlQuery);
this.singleConditionOfWhereClause = new CrudeSQLParser(this).parse();
}
/**
* @see org.apache.sis.internal.shapefile.jdbc.resultset.AbstractResultSet#getBigDecimal(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Override
public BigDecimal getBigDecimal(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getBigDecimal", columnLabel);
assertNotClosed();
// Act as if we were a double, but store the result in a pre-created BigDecimal at the end.
try(DBFBuiltInMemoryResultSetForColumnsListing field = (DBFBuiltInMemoryResultSetForColumnsListing)getFieldDesc(columnLabel, sql)) {
MathContext mc = new MathContext(field.getInt("DECIMAL_DIGITS"), RoundingMode.HALF_EVEN);
Double doubleValue = getDouble(columnLabel);
if (doubleValue != null) {
BigDecimal number = new BigDecimal(doubleValue, mc);
this.wasNull = false;
return number;
}
else {
this.wasNull = true;
return null;
}
}
}
/**
* @see java.sql.ResultSet#getBigDecimal(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public BigDecimal getBigDecimal(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getBigDecimal", columnIndex);
return getBigDecimal(getFieldName(columnIndex, this.sql));
}
/**
* @see java.sql.ResultSet#getBigDecimal(java.lang.String, int)
* @deprecated Deprecated API (from ResultSet Interface)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Deprecated @Override
public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getBigDecimal", columnLabel, scale);
assertNotClosed();
// Act as if we were a double, but store the result in a pre-created BigDecimal at the end.
MathContext mc = new MathContext(scale, RoundingMode.HALF_EVEN);
Double doubleValue = getDouble(columnLabel);
if (doubleValue != null) {
BigDecimal number = new BigDecimal(getDouble(columnLabel), mc);
this.wasNull = false;
return number;
}
else {
this.wasNull = true;
return null;
}
}
/**
* @see java.sql.ResultSet#getDate(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotDateException if the field is not a date.
*/
@Override
public Date getDate(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotDateException {
logStep("getDate", columnLabel);
assertNotClosed();
String value = getString(columnLabel);
if (value == null || value.equals("00000000")) { // "00000000" is stored in Database to represent a null value too.
this.wasNull = true;
return null; // The ResultSet:getDate() contract is to return null when a null date is encountered.
}
else {
this.wasNull = false;
}
// The DBase 3 date format is "YYYYMMDD".
// if the length of the string isn't eight characters, the field format is incorrect.
if (value.length() != 8) {
String message = format(Level.WARNING, "excp.field_is_not_a_date", columnLabel, this.sql, value);
throw new SQLNotDateException(message, this.sql, getFile(), columnLabel, value);
}
// Extract the date parts.
int year, month, dayOfMonth;
try {
year = Integer.parseInt(value.substring(0, 4));
month = Integer.parseInt(value.substring(5, 7));
dayOfMonth = Integer.parseInt(value.substring(7));
}
catch(NumberFormatException e) {
String message = format(Level.WARNING, "excp.field_is_not_a_date", columnLabel, this.sql, value);
throw new SQLNotDateException(message, this.sql, getFile(), columnLabel, value);
}
// Create a date.
Calendar calendar = new GregorianCalendar(year, month-1, dayOfMonth, 0, 0, 0);
Date sqlDate = new Date(calendar.getTimeInMillis());
return sqlDate;
}
/**
* @see java.sql.ResultSet#getDate(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotDateException if the field is not a date.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public Date getDate(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotDateException, SQLIllegalColumnIndexException {
logStep("getDate", columnIndex);
return getDate(getFieldName(columnIndex, this.sql));
}
/**
* @see java.sql.ResultSet#getDouble(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Override
public double getDouble(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getDouble", columnLabel);
Double value = getNumeric(columnLabel, Double::parseDouble);
this.wasNull = (value == null);
return value != null ? value : 0.0; // The ResultSet contract for numbers is to return 0 when a null value is encountered.
}
/**
* @see java.sql.ResultSet#getDouble(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public double getDouble(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getDouble", columnIndex);
return getDouble(getFieldName(columnIndex, this.sql));
}
/**
* @see java.sql.ResultSet#getFloat(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Override
public float getFloat(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getFloat", columnLabel);
Float value = getNumeric(columnLabel, Float::parseFloat);
this.wasNull = (value == null);
return value != null ? value : 0; // The ResultSet contract for numbers is to return 0 when a null value is encountered.
}
/**
* @see java.sql.ResultSet#getFloat(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public float getFloat(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getFloat", columnIndex);
return getFloat(getFieldName(columnIndex, this.sql));
}
/**
* @see org.apache.sis.internal.shapefile.jdbc.resultset.AbstractResultSet#getInt(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Override
public int getInt(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getInt", columnLabel);
Integer value = getNumeric(columnLabel, Integer::parseInt);
this.wasNull = (value == null);
return value != null ? value : 0; // The ResultSet contract for numbers is to return 0 when a null value is encountered.
}
/**
* @see java.sql.ResultSet#getInt(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public int getInt(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getInt", columnIndex);
return getInt(getFieldName(columnIndex, this.sql));
}
/**
* @see java.sql.ResultSet#getLong(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
*/
@Override
public long getLong(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getLong", columnLabel);
Long value = getNumeric(columnLabel, Long::parseLong);
this.wasNull = (value == null);
return value != null ? value : 0; // The ResultSet contract for numbers is to return 0 when a null value is encountered.
}
/**
* @see java.sql.ResultSet#getLong(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override public long getLong(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getLong", columnIndex);
return getLong(getFieldName(columnIndex, this.sql));
}
/**
* @see java.sql.ResultSet#getMetaData()
*/
@Override
public ResultSetMetaData getMetaData() {
logStep("getMetaData");
DBFResultSetMataData meta = new DBFResultSetMataData(this);
return meta;
}
/**
* @see org.apache.sis.internal.shapefile.jdbc.resultset.AbstractResultSet#getObject(int)
*/
@Override
public Object getObject(int column) throws SQLConnectionClosedException, SQLIllegalColumnIndexException, SQLFeatureNotSupportedException, SQLNoSuchFieldException, SQLNotNumericException, SQLNotDateException {
try(DBFBuiltInMemoryResultSetForColumnsListing field = (DBFBuiltInMemoryResultSetForColumnsListing)getFieldDesc(column, this.sql)) {
String fieldType;
try {
fieldType = field.getString("TYPE_NAME");
}
catch(SQLNoSuchFieldException e) {
// This is an internal trouble because the field type must be found.
throw new RuntimeException(e.getMessage(), e);
}
switch(fieldType) {
case "AUTO_INCREMENT":
case "INTEGER":
return getInt(column);
case "CHAR":
return getString(column);
case "DATE":
return getDate(column);
case "DECIMAL": {
// Choose Integer or Long type, if no decimal and that the field is not to big.
if (field.getInt("DECIMAL_DIGITS") == 0 && field.getInt("COLUMN_SIZE") <= 18) {
if (field.getInt("COLUMN_SIZE") <= 9)
return getInt(column);
else
return getLong(column);
}
return getDouble(column);
}
case "DOUBLE":
case "CURRENCY":
return getDouble(column);
case "FLOAT":
return getFloat(column);
case "BOOLEAN":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on Boolean");
case "DATETIME":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on DateTime");
case "TIMESTAMP":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on TimeStamp");
case "MEMO":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on Memo");
case "PICTURE":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on Picture");
case "VARIFIELD":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on VariField");
case "VARIANT":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on Variant");
case "UNKNOWN":
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on " + fieldType);
default:
throw unsupportedOperation("ResultSetMetaData.getColumnClassName(..) on " + fieldType);
}
}
}
/**
* @see org.apache.sis.internal.shapefile.jdbc.resultset.DBFResultSet#getObject(java.lang.String)
*/
@Override
public Object getObject(String columnLabel) throws SQLConnectionClosedException, SQLFeatureNotSupportedException, SQLNoSuchFieldException, SQLNotNumericException, SQLNotDateException {
int index = -1;
try {
index = findColumn(columnLabel);
return getObject(index);
}
catch(SQLIllegalColumnIndexException e) {
String message = format(Level.SEVERE, "assert.wrong_index_for_column_name", index, columnLabel);
throw new RuntimeException(message, e);
}
}
/**
* Return the record number of this record.
* @return Record number of this record.
*/
public int getRowNum() {
return this.recordNumber;
}
/**
* @see java.sql.ResultSet#getShort(java.lang.String)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric or has a NULL value.
*/
@Override
public short getShort(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
logStep("getShort", columnLabel);
Short value = getNumeric(columnLabel, Short::parseShort);
this.wasNull = (value == null);
return value != null ? value : 0; // The ResultSet contract for numbers is to return 0 when a null value is encountered.
}
/**
* @see java.sql.ResultSet#getShort(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric or has a NULL value.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public short getShort(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException, SQLIllegalColumnIndexException {
logStep("getShort", columnIndex);
return getShort(getFieldName(columnIndex, this.sql));
}
/**
* Returns the value in the current row for the given column.
* @param columnLabel Column name.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field does not exist.
*/
@Override
@SuppressWarnings("resource") // Only read the current connection to get the Charset.
public String getString(String columnLabel) throws SQLConnectionClosedException, SQLNoSuchFieldException {
logStep("getString", columnLabel);
assertNotClosed();
getFieldDesc(columnLabel, this.sql); // Ensure that the field queried exists, else a null value here can be interpreted as "not existing" or "has a null value".
byte[] bytes = this.record.get(columnLabel);
if (bytes == null) {
this.wasNull = true;
return null;
}
else {
this.wasNull = false;
}
// If a non null value has been readed, convert it to the wished Charset (provided one has been given).
DBFConnection cnt = (DBFConnection)((DBFStatement)getStatement()).getConnection();
Charset charset = cnt.getCharset();
if (charset == null) {
return new String(bytes);
}
else {
String withDatabaseCharset = new String(bytes, charset);
log(Level.FINER, "log.string_field_charset", columnLabel, withDatabaseCharset, charset);
return withDatabaseCharset;
}
}
/**
* @see java.sql.ResultSet#getString(int)
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLIllegalColumnIndexException if the column index has an illegal value.
*/
@Override
public String getString(int columnIndex) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLIllegalColumnIndexException {
logStep("getString", columnIndex);
return(getString(getFieldName(columnIndex, this.sql)));
}
/**
* Moves the cursor forward one row from its current position.
* @throws SQLInvalidStatementException if the SQL statement is invalid.
* @throws SQLIllegalParameterException if the value of one parameter of a condition is invalid.
* @throws SQLNoSuchFieldException if a field mentionned in the condition doesn't exist.
* @throws SQLUnsupportedParsingFeatureException if the caller asked for a not yet supported feature of the driver.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNotNumericException if a value or data expected to be numeric isn't.
* @throws SQLNotDateException if a value or data expected to be a date isn't.
*/
@Override
@SuppressWarnings("resource") // Only read the current connection to find if a next row is available and read it.
public boolean next() throws SQLNoResultException, SQLConnectionClosedException, SQLInvalidStatementException, SQLIllegalParameterException, SQLNoSuchFieldException, SQLUnsupportedParsingFeatureException, SQLNotNumericException, SQLNotDateException {
logStep("next");
assertNotClosed();
DBFConnection cnt = (DBFConnection)((DBFStatement)getStatement()).getConnection();
// Check that we aren't at the end of the Database file.
if (cnt.nextRowAvailable() == false) {
if (this.lastResultSetRecordAlreadyReturned) {
throw new SQLNoResultException(format(Level.WARNING, "excp.no_more_results", this.sql, getFile().getName()), this.sql, getFile());
}
else {
this.lastResultSetRecordAlreadyReturned = true;
return false;
}
}
return nextRecordMatchingConditions();
}
/**
* Find the next record that match the where condition.
* @return true if a record has been found.
* @throws SQLInvalidStatementException if the SQL statement is invalid.
* @throws SQLIllegalParameterException if the value of one parameter of a condition is invalid.
* @throws SQLNoSuchFieldException if a field mentionned in the condition doesn't exist.
* @throws SQLUnsupportedParsingFeatureException if the caller asked for a not yet supported feature of the driver.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNotNumericException if a value or data expected to be numeric isn't.
* @throws SQLNotDateException if a value or data expected to be a date isn't.
*/
@SuppressWarnings("resource") // Only read the current connection to find if a next row is available and read it.
private boolean nextRecordMatchingConditions() throws SQLInvalidStatementException, SQLIllegalParameterException, SQLNoSuchFieldException, SQLUnsupportedParsingFeatureException, SQLConnectionClosedException, SQLNotNumericException, SQLNotDateException {
boolean recordMatchesConditions = false;
DBFConnection cnt = (DBFConnection)((DBFStatement)getStatement()).getConnection();
while(cnt.nextRowAvailable() && recordMatchesConditions == false) {
this.record = cnt.readNextRowAsObjects();
this.recordNumber = cnt.getRowNum();
recordMatchesConditions = this.singleConditionOfWhereClause == null || this.singleConditionOfWhereClause.isVerified(this);
}
return recordMatchesConditions;
}
/**
* @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
*/
@Override
public boolean isWrapperFor(Class<?> iface) {
logStep("isWrapperFor", iface);
return iface.isAssignableFrom(getInterface());
}
/**
* @see java.sql.ResultSet#wasNull()
*/
@Override
public boolean wasNull() {
logStep("wasNull");
return this.wasNull;
}
/**
* Get a numeric value.
* @param <T> Type of the number.
* @param columnLabel Column Label.
* @param parse Parsing function : Integer.parseInt, Float.parseFloat, Long.parseLong, ...
* @return The expected value or null if null was encountered.
* @throws SQLConnectionClosedException if the connection is closed.
* @throws SQLNoSuchFieldException if the field looked for doesn't exist.
* @throws SQLNotNumericException if the field value is not numeric or has a NULL value.
*/
private <T extends Number> T getNumeric(String columnLabel, Function<String, T> parse) throws SQLConnectionClosedException, SQLNoSuchFieldException, SQLNotNumericException {
assertNotClosed();
try(DBFBuiltInMemoryResultSetForColumnsListing rs = (DBFBuiltInMemoryResultSetForColumnsListing)getFieldDesc(columnLabel, this.sql)) {
String textValue = getString(columnLabel);
if (textValue == null) {
return null;
}
try {
textValue = textValue.trim(); // Field must be trimed before being converted.
T value = parse.apply(textValue);
return(value);
}
catch(NumberFormatException e) {
String message = format(Level.WARNING, "excp.field_is_not_numeric", columnLabel, rs.getString("TYPE_NAME"), this.sql, textValue);
throw new SQLNotNumericException(message, this.sql, getFile(), columnLabel, textValue);
}
}
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return format("toString", this.statement != null ? this.statement.toString() : null, this.sql, isClosed() == false);
}
}