/*
* ARX: Powerful Data Anonymization
* Copyright 2014 - 2015 Karol Babioch, Fabian Prasser, Florian Kohlmayer
*
* Licensed 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.deidentifier.arx.io;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.deidentifier.arx.DataType;
/**
* Import adapter for JDBC
*
* This adapter can import data from JDBC sources. The source itself is
* described by an appropriate {@link ImportConfigurationJDBC} object.
*
* @author Karol Babioch
* @author Fabian Prasser
*/
public class ImportAdapterJDBC extends ImportAdapter {
/** The configuration describing the CSV file being used. */
private ImportConfigurationJDBC config;
/**
* ResultSet containing rows to return.
*
* @see {@link #next()}
*/
private ResultSet resultSet;
/** JDBC statement. */
private Statement statement;
/** Indicates whether there is another row to return. */
private boolean hasNext;
/**
* Indicates whether the first row has already been returned
*
* The first row contains the name of the columns. Depending upon whether
* the name of the column has been assigned explicitly, this is either the
* value of the table itself, the value defined by the user.
*/
private boolean headerReturned;
/**
* Number of rows that need to be processed in total.
*
* @see {@link #getProgress()}
*/
private int totalRows;
/**
* Creates a new instance of this object with given configuration.
*
* @param config {@link #config}
* @throws IOException In case of communication errors with JDBC
* @todo Fix IOException
*/
protected ImportAdapterJDBC(ImportConfigurationJDBC config) throws IOException {
super(config);
this.config = config;
/* Preparation work */
indexes = getIndexesToImport();
dataTypes = getColumnDatatypes();
try {
/* Used to keep track of progress */
statement = config.getConnection().createStatement();
statement.execute("SELECT COUNT(*) FROM " + config.getTable());
resultSet = statement.getResultSet();
if (resultSet.next()) {
totalRows = resultSet.getInt(1);
if (totalRows == 0) {
closeResources();
throw new IOException("Table doesn't contain any rows");
}
} else {
closeResources();
throw new IOException("Couldn't determine number of rows");
}
/* Query for actual data */
statement = config.getConnection().createStatement();
statement.execute("SELECT * FROM " + config.getTable());
resultSet = statement.getResultSet();
hasNext = resultSet.next();
} catch (SQLException e) {
closeResources();
throw new IOException(e.getMessage());
}
// Create header
header = createHeader();
}
/**
* Returns the percentage of data that has already been returned
*
* This divides the number of rows that have already been returned by the
* number of total rows and casts the result into a percentage. In case of
* an {@link SQLException} 0 will be returned.
*
* @return
*/
@Override
public int getProgress() {
try {
return (int) (((double) resultSet.getRow() / (double) totalRows) * 100d);
} catch (SQLException e) {
return 0;
}
}
/**
* Indicates whether there is another element to return
*
* This returns true when there is another element in the result set {@link #resultSet}.
*
* @return
*/
@Override
public boolean hasNext() {
return hasNext;
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#next()
*/
@Override
public String[] next() {
/* Return header in first iteration */
if (!headerReturned) {
headerReturned = true;
return header;
}
try {
/* Create regular row */
String[] result = new String[indexes.length];
for (int i = 0; i < indexes.length; i++) {
result[i] = IOUtil.trim(resultSet.getString(indexes[i]));
if (!dataTypes[i].isValid(result[i])) {
if (config.columns.get(i).isCleansing()) {
result[i] = DataType.NULL_VALUE;
} else {
closeResources();
throw new IllegalArgumentException("Data value does not match data type");
}
}
}
/* Move cursor forward and assign result to {@link #hasNext} */
hasNext = resultSet.next();
if (!hasNext) {
closeResources();
}
return result;
} catch (SQLException e) {
closeResources();
throw new RuntimeException("Couldn't retrieve data from database");
}
}
/**
* Dummy.
*/
@Override
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Closes the JDBC resources.
*/
private void closeResources() {
try {
if (resultSet != null) {
resultSet.close();
}
} catch (Exception e) {
/* Ignore silently */
}
try {
if (statement != null) {
statement.close();
}
} catch (Exception e) {
/* Ignore silently */
}
try {
if (config.isManageConnection()) {
config.getConnection().close();
}
} catch (Exception e) {
/* Die silently */
}
}
/**
* Creates the header row
*
* This returns a string array with the names of the columns that will be
* returned later on by iterating over this object. Depending upon whether
* or not names have been assigned explicitly either the appropriate values
* will be returned, or names from the JDBC metadata will be used.
*
* @return
*/
private String[] createHeader() {
/* Initialization */
String[] header = new String[config.getColumns().size()];
List<ImportColumn> columns = config.getColumns();
/* Create header */
for (int i = 0, len = columns.size(); i < len; i++) {
ImportColumn column = columns.get(i);
/* Check whether name has been assigned explicitly or is nonempty */
if ((column.getAliasName() != null) &&
!column.getAliasName().equals("")) {
header[i] = column.getAliasName();
} else {
/* Assign name from JDBC metadata */
try {
/* +1 offset, because counting in JDBC starts at 1 */
header[i] = IOUtil.trim(resultSet.getMetaData().getColumnName(((ImportColumnJDBC) column).getIndex() + 1));
} catch (SQLException e) {
throw new IllegalArgumentException("Index for column '" + ((ImportColumnJDBC) column).getIndex() + "' couldn't be found");
}
}
column.setAliasName(header[i]);
}
/* Return header */
return header;
}
/**
* Returns an array with indexes of columns that should be imported
*
* Only columns listed within {@link #column} will be imported. This
* iterates over the list of columns and returns an array with indexes of
* columns that should be imported.
*
* @return Array containing indexes of columns that should be imported
*/
protected int[] getIndexesToImport() {
/* Get indexes to import from */
ArrayList<Integer> indexes = new ArrayList<Integer>();
for (ImportColumn column : config.getColumns()) {
indexes.add(((ImportColumnJDBC) column).getIndex());
}
int[] result = new int[indexes.size()];
for (int i = 0; i < result.length; i++) {
result[i] = indexes.get(i) + 1;
}
return result;
}
}