| /* |
| * 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.connection; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.sql.*; |
| import java.util.*; |
| import java.util.logging.Level; |
| import java.util.stream.Collectors; |
| |
| import org.apache.sis.internal.shapefile.jdbc.*; |
| import org.apache.sis.internal.shapefile.jdbc.metadata.DBFDatabaseMetaData; |
| import org.apache.sis.internal.shapefile.jdbc.resultset.*; |
| import org.apache.sis.internal.shapefile.jdbc.statement.DBFStatement; |
| |
| |
| /** |
| * Connection to a DBF database. |
| * @author Marc Le Bihan |
| * @version 0.5 |
| * @since 0.5 |
| * @module |
| */ |
| public class DBFConnection extends AbstractConnection { |
| /** The object to use for reading the database content. */ |
| final File databaseFile; |
| |
| /** Opened statement. */ |
| private HashSet<DBFStatement> openedStatements = new HashSet<>(); |
| |
| /** ByteReader. */ |
| private Dbase3ByteReader byteReader; |
| |
| /** |
| * Constructs a connection to the given database. |
| * @param datafile Data file ({@code .dbf} extension). |
| * @param br Byte reader to use for reading binary content. |
| * @throws SQLDbaseFileNotFoundException if the Database file cannot be found or is not a file. |
| */ |
| public DBFConnection(final File datafile, Dbase3ByteReader br) throws SQLDbaseFileNotFoundException { |
| // Check that file exists. |
| if (!datafile.exists()) { |
| throw new SQLDbaseFileNotFoundException(format(Level.WARNING, "excp.file_not_found", datafile.getAbsolutePath())); |
| } |
| |
| // Check that its not a directory. |
| if (datafile.isDirectory()) { |
| throw new SQLDbaseFileNotFoundException(format(Level.WARNING, "excp.directory_not_expected", datafile.getAbsolutePath())); |
| } |
| |
| this.databaseFile = datafile; |
| this.byteReader = br; |
| log(Level.FINE, "log.database_connection_opened", this.databaseFile.getAbsolutePath(), "FIXME : column desc."); |
| } |
| |
| /** |
| * Closes the connection to the database. |
| */ |
| @Override |
| public void close() { |
| if (isClosed()) |
| return; |
| |
| try { |
| // Check if all the underlying connections that has been opened with this connection has been closed. |
| // If not, we log a warning to help the developer. |
| if (this.openedStatements.size() > 0) { |
| log(Level.WARNING, "log.statements_left_opened", this.openedStatements.size(), this.openedStatements.stream().map(DBFStatement::toString).collect(Collectors.joining(", "))); |
| } |
| |
| this.byteReader.close(); |
| } catch (IOException e) { |
| log(Level.FINE, e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Creates an object for sending SQL statements to the database. |
| * @throws SQLConnectionClosedException if the connection is closed. |
| */ |
| @Override |
| public Statement createStatement() throws SQLConnectionClosedException { |
| assertNotClosed(); |
| |
| DBFStatement stmt = new DBFStatement(this); |
| this.openedStatements.add(stmt); |
| return stmt; |
| } |
| |
| /** |
| * @see java.sql.Connection#getCatalog() |
| */ |
| @Override |
| public String getCatalog() { |
| return null; // DBase 3 offers no catalog. |
| } |
| |
| /** |
| * Returns the charset. |
| * @return Charset. |
| */ |
| public Charset getCharset() { |
| return this.byteReader.getCharset(); |
| } |
| |
| /** |
| * Returns the database File. |
| * @return File. |
| */ |
| @Override |
| public File getFile() { |
| return this.databaseFile; |
| } |
| |
| /** |
| * Returns the JDBC interface implemented by this class. |
| * This is used for formatting error messages. |
| */ |
| @Override |
| final protected Class<?> getInterface() { |
| return Connection.class; |
| } |
| |
| /** |
| * @see java.sql.Connection#getMetaData() |
| */ |
| @Override |
| public DatabaseMetaData getMetaData() { |
| return new DBFDatabaseMetaData(this); |
| } |
| |
| /** |
| * Returns {@code true} if this connection has been closed. |
| */ |
| @Override |
| public boolean isClosed() { |
| return this.byteReader.isClosed(); |
| } |
| |
| /** |
| * Returns {@code true} if the connection has not been closed and is still valid. |
| * The timeout parameter is ignored and this method bases itself only on {@link #isClosed()} state. |
| */ |
| @Override |
| public boolean isValid(@SuppressWarnings("unused") int timeout) { |
| return !isClosed(); |
| } |
| |
| /** |
| * @see java.sql.Wrapper#isWrapperFor(java.lang.Class) |
| */ |
| @Override |
| public boolean isWrapperFor(Class<?> iface) { |
| return iface.isAssignableFrom(getInterface()); |
| } |
| |
| /** |
| * Asserts that the connection is opened. |
| * @throws SQLConnectionClosedException if the connection is closed. |
| */ |
| public void assertNotClosed() throws SQLConnectionClosedException { |
| // If closed throw an exception specifying the name if the DBF that is closed. |
| if (isClosed()) { |
| throw new SQLConnectionClosedException(format(Level.WARNING, "excp.closed_connection", getFile().getName()), null, getFile()); |
| } |
| } |
| |
| /** |
| * Method called by Statement class to notity this connection that a statement has been closed. |
| * @param stmt Statement that has been closed. |
| */ |
| public void notifyCloseStatement(DBFStatement stmt) { |
| Objects.requireNonNull(stmt, "The statement notified being closed cannot be null."); |
| |
| if (this.openedStatements.remove(stmt) == false) { |
| throw new RuntimeException(format(Level.SEVERE, "assert.statement_not_opened_by_me", stmt, toString())); |
| } |
| } |
| |
| /** |
| * Returns the column index for the given column name. |
| * The default implementation of all methods expecting a column label will invoke this method. |
| * @param columnLabel The name of the column. |
| * @param sql For information, the SQL statement that is attempted. |
| * @return The index of the given column name : first column is 1. |
| * @throws SQLNoSuchFieldException if there is no field with this name in the query. |
| */ |
| public int findColumn(String columnLabel, String sql) throws SQLNoSuchFieldException { |
| return this.byteReader.findColumn(columnLabel, sql); |
| } |
| |
| /** |
| * Returns the column count of the table of the database. |
| * @return Column count. |
| */ |
| public int getColumnCount() { |
| return this.byteReader.getColumnCount(); |
| } |
| |
| /** |
| * Get a field description. |
| * @param columnLabel Column label. |
| * @param sql SQL Statement. |
| * @return ResultSet with current row set on the wished field. |
| * @throws SQLConnectionClosedException if the connection is closed. |
| * @throws SQLNoSuchFieldException if no column with that name exists. |
| */ |
| public ResultSet getFieldDesc(String columnLabel, String sql) throws SQLConnectionClosedException, SQLNoSuchFieldException { |
| Objects.requireNonNull(columnLabel, "The column name cannot be null."); |
| |
| DBFBuiltInMemoryResultSetForColumnsListing rs = (DBFBuiltInMemoryResultSetForColumnsListing)((DBFDatabaseMetaData)getMetaData()).getColumns(null, null, null, null); |
| |
| try { |
| while(rs.next()) { |
| try { |
| if (rs.getString("COLUMN_NAME").equalsIgnoreCase(columnLabel)) { |
| return rs; |
| } |
| } |
| catch(SQLNoSuchFieldException e) { |
| // if it is the COLUMN_NAME column that has not been found in the desc ResultSet, we have an internal error. |
| rs.close(); |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| } |
| catch(SQLNoResultException e) { |
| // if we run out of bound of the ResultSet, the boolean returned by next() has not been checked well, and it's an internal error. |
| rs.close(); |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| |
| // But if we are here, we have not found the column with this name, and we have to throw an SQLNoSuchFieldException exception ourselves. |
| String message = format("excp.no_such_column_in_resultset", columnLabel, sql, getFile().getName()); |
| throw new SQLNoSuchFieldException(message, sql, getFile(), columnLabel); |
| } |
| |
| /** |
| * Get a field description. |
| * @param column Column index. |
| * @param sql SQL Statement. |
| * @return ResultSet with current row set on the wished field. |
| * @throws SQLConnectionClosedException if the connection is closed. |
| * @throws SQLIllegalColumnIndexException if the column index is out of bounds. |
| */ |
| public ResultSet getFieldDesc(int column, String sql) throws SQLConnectionClosedException, SQLIllegalColumnIndexException { |
| DBFBuiltInMemoryResultSetForColumnsListing rs = (DBFBuiltInMemoryResultSetForColumnsListing)((DBFDatabaseMetaData)getMetaData()).getColumns(null, null, null, null); |
| |
| if (column <= 0 || column > getColumnCount()) { |
| rs.close(); |
| String message = format("excp.illegal_column_index_metadata", column, getColumnCount()); |
| throw new SQLIllegalColumnIndexException(message, sql, getFile(), column); |
| } |
| |
| // TODO Implements ResultSet:absolute(int) instead. |
| for(int index=1; index <= column; index ++) { |
| try { |
| rs.next(); |
| } |
| catch(SQLNoResultException e) { |
| // We encounter an internal API error in this case. |
| rs.close(); |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| |
| return rs; |
| } |
| |
| /** |
| * Returns the fields descriptors in their binary format. |
| * @return Fields descriptors. |
| */ |
| public List<DBase3FieldDescriptor> getFieldsDescriptors() { |
| return this.byteReader.getFieldsDescriptors(); |
| } |
| |
| /** |
| * Return a field name. |
| * @param columnIndex Column index. |
| * @param sql For information, the SQL statement that is attempted. |
| * @return Field Name. |
| * @throws SQLIllegalColumnIndexException if the index is out of bounds. |
| */ |
| public String getFieldName(int columnIndex, String sql) throws SQLIllegalColumnIndexException { |
| return this.byteReader.getFieldName(columnIndex, sql); |
| } |
| |
| /** |
| * Checks if a next row is available. Warning : it may be a deleted one. |
| * @return true if a next row is available. |
| */ |
| public boolean nextRowAvailable() { |
| return this.byteReader.nextRowAvailable(); |
| } |
| |
| /** |
| * Read the next row as a set of objects. |
| * @return Map of field name / object value, or null if EoF has been encountered. |
| */ |
| public Map<String, byte[]> readNextRowAsObjects() { |
| return this.byteReader.readNextRowAsObjects(); |
| } |
| |
| /** |
| * Returns the record number of the last record red. |
| * @return The record number. |
| */ |
| public int getRowNum() { |
| return this.byteReader.getRowNum(); |
| } |
| |
| /** |
| * @see java.lang.Object#toString() |
| */ |
| @Override |
| public String toString() { |
| return format("toString", this.databaseFile.getAbsolutePath(), isClosed() == false); |
| } |
| } |