de.hpi.bp2013n1.anonymizer.Anonymizer.java Source code

Java tutorial

Introduction

Here is the source code for de.hpi.bp2013n1.anonymizer.Anonymizer.java

Source

package de.hpi.bp2013n1.anonymizer;

/*
 * #%L
 * Anonymizer
 * %%
 * Copyright (C) 2013 - 2014 HPI-BP2013N1
 * %%
 * 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.
 * #L%
 */

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;

import de.hpi.bp2013n1.anonymizer.TransformationStrategy.ColumnTypeNotSupportedException;
import de.hpi.bp2013n1.anonymizer.TransformationStrategy.FetchPseudonymsFailedException;
import de.hpi.bp2013n1.anonymizer.TransformationStrategy.PreparationFailedException;
import de.hpi.bp2013n1.anonymizer.TransformationStrategy.TransformationFailedException;
import de.hpi.bp2013n1.anonymizer.db.BatchOperation;
import de.hpi.bp2013n1.anonymizer.db.TableField;
import de.hpi.bp2013n1.anonymizer.shared.Config;
import de.hpi.bp2013n1.anonymizer.shared.Config.DependantWithoutRuleException;
import de.hpi.bp2013n1.anonymizer.shared.Config.InvalidConfigurationException;
import de.hpi.bp2013n1.anonymizer.shared.Config.MalformedException;
import de.hpi.bp2013n1.anonymizer.shared.DatabaseConnector;
import de.hpi.bp2013n1.anonymizer.shared.Rule;
import de.hpi.bp2013n1.anonymizer.shared.Scope;
import de.hpi.bp2013n1.anonymizer.shared.TableRuleMap;
import de.hpi.bp2013n1.anonymizer.shared.TransformationKeyCreationException;
import de.hpi.bp2013n1.anonymizer.shared.TransformationKeyNotFoundException;
import de.hpi.bp2013n1.anonymizer.shared.TransformationTableCreationException;
import de.hpi.bp2013n1.anonymizer.tools.ConstraintToggler;
import de.hpi.bp2013n1.anonymizer.tools.TableTruncater;
import de.hpi.bp2013n1.anonymizer.util.RuleConnector;
import de.hpi.bp2013n1.anonymizer.util.SQLHelper;

/**
 * Application class that conducts an anonymization run on databases.
 */
public class Anonymizer {

    public Config config;
    public Scope scope;
    private Connection originalDatabase, anonymizedDatabase, transformationDB;
    private ArrayList<TransformationStrategy> transformationStrategies = new ArrayList<>();
    private TreeMap<String, TransformationStrategy> strategyByClassName = new TreeMap<>();
    private Map<String, TransformationStrategy> strategyByName = new TreeMap<>();
    private final int LOG_INTERVAL = 1000;

    public static final Logger anonymizerLogger = Logger.getLogger(Anonymizer.class.getName());
    private static FileHandler logFileHandler;
    private static SimpleFormatter logFormatter;
    ConstraintToggler toggler = new ConstraintToggler();
    private RowRetainService retainService;
    private ForeignKeyDeletionsHandler foreignKeyDeletions = new ForeignKeyDeletionsHandler();
    private boolean skipRuleValidation;
    Multimap<TableField, Rule> comprehensiveRulesBySite;

    public static class TableNotInScopeException extends Exception {
        private static final long serialVersionUID = -4527921975005958468L;
    }

    public static class FatalError extends Exception {
        private static final long serialVersionUID = -6431519862013506163L;

        public FatalError() {
            super();
        }

        public FatalError(Throwable cause) {
            super(cause);
        }
    }

    public static class TableNotFoundException extends Exception {
        private static final long serialVersionUID = -852972263392782109L;

        public TableNotFoundException(String message) {
            super(message);
        }
    }

    /**
     * Initializes Anonymizer
     * Connects databases specified in config
     * Creates strategies with database connections
     * 
     * @param config Config object generated from config file.
     * @param scope Scope object generated from scope file.
     * @throws SQLException
     */
    public Anonymizer(Config config, Scope scope) {
        this.config = config;
        this.scope = scope;
    }

    public RowRetainService getRetainService() {
        return retainService;
    }

    public void connectAndRun() throws FatalError {
        if (!connectDatabases()) {
            throw new FatalError();
        }
        run();
    }

    /**
     * Initializes transformation strategies, valides the configuration and
     * then performs the actual anonymization. Catches all declared exceptions
     * and wraps them as FatalError.
     */
    public void run() throws FatalError {
        try {
            loadAndInstantiateStrategies();
        } catch (InvalidConfigurationException e) {
            throw new FatalError(e);
        } catch (ClassNotFoundException e) {
            anonymizerLogger.severe("Could not load strategy: " + e.getMessage());
            throw new FatalError(e);
        }
        // getConstructor
        catch (NoSuchMethodException e) {
            anonymizerLogger.severe("Strategy is missing the required constructor: " + e.getMessage());
            throw new FatalError(e);
        } catch (SecurityException e) {
            anonymizerLogger.severe("Could not access strategy constructor: " + e.getMessage());
            throw new FatalError(e);
        }
        // newInstance
        catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            anonymizerLogger.severe("Could not create strategy: " + e.getMessage());
            throw new FatalError(e);
        }
        // not a TransformationStrategy
        catch (ClassCastException e) {
            // error message has already been emitted in loadAndInstanciateStrategy
            throw new FatalError(e);
        }

        try {
            if (skipRuleValidation || validateRules() == 0)
                try {
                    anonymize();
                } catch (TransformationTableCreationException e) {
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                } catch (TransformationKeyCreationException e) {
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                } catch (FetchPseudonymsFailedException e) {
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                } catch (ColumnTypeNotSupportedException e) {
                    anonymizerLogger.severe("An anonymization strategy does not "
                            + "support the type of column to which the strategy " + "should be applied: "
                            + e.getMessage());
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                } catch (PreparationFailedException e) {
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                } catch (TableNotFoundException e) {
                    logSevereErrorAndCausesWithInnerStackTrace(e);
                    throw new FatalError(e);
                }
        } catch (SQLException e) {
            anonymizerLogger.severe("SQL error while checking whether all " + "values will fit into their column: "
                    + e.getMessage());
            throw new FatalError(e);
        }
    }

    private void logSevereErrorAndCausesWithInnerStackTrace(Throwable t) {
        Throwable last = t;
        anonymizerLogger.severe(t.getMessage());
        while (t != null) {
            anonymizerLogger.severe("caused by: " + t.getMessage());
            last = t;
            t = t.getCause();
        }
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        last.printStackTrace(pw);
        anonymizerLogger.severe("stack trace of inner exception: " + sw.toString());
        if (last instanceof SQLException) {
            SQLException e = (SQLException) last;
            e = e.getNextException();
            anonymizerLogger.severe("SQLException detected, exception chain follows");
            while (e != null) {
                anonymizerLogger.severe(e.getMessage());
                e = e.getNextException();
            }
        }
    }

    public void loadAndInstantiateStrategies()
            throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException,
            InvocationTargetException, ClassCastException, InvalidConfigurationException {
        anonymizerLogger.info("Loading transformation strategies.");
        for (Map.Entry<String, String> strategiesEntry : config.getStrategyMapping().entrySet()) {
            String strategy = strategiesEntry.getKey();
            String strategyClassName = strategiesEntry.getValue();
            checkNotNull(strategyClassName, "No strategy class defined for " + strategy);
            if (!strategyByClassName.containsKey(strategyClassName)) {
                loadAndInstanciateStrategy(strategyClassName);
            }
            TransformationStrategy transformationStrategy = strategyByClassName.get(strategyClassName);
            strategyByName.put(strategy, transformationStrategy);
        }
        for (Rule rule : config.rules) {
            String strategy = rule.getStrategy();
            if (!strategyByName.containsKey(strategy))
                throw new Config.InvalidConfigurationException(
                        "Strategy " + strategy + " is not defined but used in rule " + rule);
            rule.setTransformation(strategyByName.get(strategy));
        }
    }

    protected void loadAndInstanciateStrategy(String strategyClassName)
            throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException,
            InvocationTargetException, ClassCastException {
        System.out.println("Loading " + strategyClassName);
        try {
            TransformationStrategy strategy = TransformationStrategy.loadAndCreate(strategyClassName, this,
                    originalDatabase, transformationDB);
            transformationStrategies.add(strategy);
            strategyByClassName.put(strategyClassName, strategy);
        } catch (ClassCastException e) {
            anonymizerLogger.severe(String.format("%s is not a valid transformation class", strategyClassName));
            throw e;
        }
    }

    private boolean connectDatabases() {

        try {
            originalDatabase = DatabaseConnector.connect(this.config.originalDB);
            anonymizedDatabase = DatabaseConnector.connect(this.config.destinationDB);
            transformationDB = DatabaseConnector.connect(this.config.transformationDB);

        } catch (SQLException e) {
            anonymizerLogger.severe("Could not connect to Databases. ");
            e.printStackTrace();
            return false;
        }
        retainService = new RowRetainService(originalDatabase, transformationDB);
        return true;
    }

    protected void useDatabases(Connection originalDbConnection, Connection destinationDbConnection,
            Connection transformationDbConnection) {
        originalDatabase = originalDbConnection;
        anonymizedDatabase = destinationDbConnection;
        transformationDB = transformationDbConnection;
        retainService = new RowRetainService(originalDatabase, transformationDB);
    }

    /**
     * Loads Config, and scopes. Then starts the anonymization
     * 
     * @param args
     *            [0] = path to intermediary config file, args[1] = path to scope file
     * @throws Exception
     */
    public static void main(String[] args) {
        if (args.length < 3) {
            System.err.println("Expected 3 Arguments\n" + "1. : path to intermediary config file, \n"
                    + "2. : path to scope file,\n" + "3. : desired name of logfile");
            System.exit(64);
            return;
        }

        List<String> arguments = Lists.newArrayList(args);
        boolean skipRuleValidation = arguments.remove("--skip-rule-validation");

        try {
            setUpLogging(arguments.get(2));
        } catch (IOException e) {
            anonymizerLogger.severe("Could not set up logging to the specified " + "file: " + e.getMessage());
            System.exit(74);
        }

        Config config = new Config();
        Scope scope = new Scope();
        try {
            anonymizerLogger.info("Reading config file.");
            config.readFromFile(arguments.get(0));
        } catch (IOException e) {
            anonymizerLogger.severe("Could not read from config file: " + e.getMessage());
            System.exit(74);
            return;
        } catch (DependantWithoutRuleException | MalformedException e) {
            anonymizerLogger.severe("Invalid config file: " + e.getMessage());
            System.exit(78);
            return;
        }
        try {
            anonymizerLogger.info("Reading scope file");
            scope.readFromFile(arguments.get(1));
        } catch (IOException e) {
            anonymizerLogger.severe("Reading scope file failed: " + e.getMessage());
            System.exit(74);
            return;
        }
        Anonymizer anon = new Anonymizer(config, scope);
        if (skipRuleValidation)
            anon.skipRuleValidation = true;
        try {
            anon.connectAndRun();
        } catch (FatalError e) {
            anonymizerLogger.severe("Cannot recover from previous errors, exiting.");
            System.exit(1);
        }
    }

    /**
     * Overrides the logging format and also routes logging output to the
     * specified log file.
     * 
     * @param logFilename path to the log file to which logging output should be written
     * @throws IOException cannot initialize FileHandler for the logfile
     */
    public static void setUpLogging(String logFilename) throws IOException {
        System.setProperty("java.util.logging.SimpleFormatter.format", "[%1$tF %1$tT] - %4$s: %5$s (%2$s)%n");
        Logger logger = Logger.getLogger("de.hpi.bp2013n1");
        logFileHandler = new FileHandler(logFilename);

        logFormatter = new SimpleFormatter();

        logger.addHandler(logFileHandler);
        logFileHandler.setFormatter(logFormatter);
    }

    public int validateRules() throws SQLException {
        anonymizerLogger.info("Checking whether the transformation rules are valid.");
        int numberOfErrors = 0;
        RuleValidator ruleValidator = new RuleValidator(anonymizedDatabase.getMetaData());
        for (Rule rule : config.rules) {
            if (!ruleValidator.isValid(rule)) {
                numberOfErrors++;
            }
        }
        return numberOfErrors;
    }

    /**
     * Main anonymization method. Creates transformation tables in
     * transformation database, then copies and/or anonymizes the data.
     * During the transformation and copying, every relevant constraint in the
     * destination database will be disabled.
     * 
     * @throws ColumnTypeNotSupportedException
     * @throws TransformationTableCreationException
     * @throws TransformationKeyCreationException
     * @throws FetchPseudonymsFailedException
     * @throws PreparationFailedException
     */
    public void anonymize() throws FetchPseudonymsFailedException, TransformationKeyCreationException,
            TransformationTableCreationException, ColumnTypeNotSupportedException, PreparationFailedException,
            TableNotFoundException {
        anonymizerLogger.info("Started anonymizing.");
        checkIfTablesExistInDestinationDatabase();
        try {
            foreignKeyDeletions.determineForeignKeysAmongTables(originalDatabase, config.schemaName, scope.tables);
        } catch (SQLException e) {
            anonymizerLogger
                    .severe("Could not determine relationships in the " + "source database: " + e.getMessage());
        }
        foreignKeyDeletions.addForeignKeysForRuleDependents(config.rules);
        Collection<Constraint> constraints = disableAnonymizedDbConstraints();

        prepareTransformations();
        copyAndAnonymizeData();

        ConstraintToggler.enableConstraints(constraints, anonymizedDatabase);
        anonymizerLogger.info("Finished: Anonymizing");
        for (TransformationStrategy strategy : transformationStrategies)
            strategy.printSummary();
    }

    private void checkIfTablesExistInDestinationDatabase() throws TableNotFoundException {
        try {
            DatabaseMetaData metaData = anonymizedDatabase.getMetaData();
            String[] tableTypes = new String[] { "TABLE" };
            List<String> missingTables = new ArrayList<>();
            for (String tableName : scope.tables) {
                try (ResultSet tables = metaData.getTables(null, config.schemaName, tableName, tableTypes)) {
                    if (!tables.next())
                        missingTables.add(SQLHelper.qualifiedTableName(config.schemaName, tableName));
                }
            }
            if (!missingTables.isEmpty())
                throw new TableNotFoundException(
                        "The following tables could not be found:\n" + Joiner.on('\n').join(missingTables));
        } catch (SQLException e) {
            anonymizerLogger.severe("Could not determine if all tables exist "
                    + "in the destination database. This may lead to a lot of "
                    + "error messages if some tables are really missing. " + "The error was: " + e.getMessage());
        }
    }

    private Collection<Constraint> disableAnonymizedDbConstraints() {
        return ConstraintToggler.disableConstraints(anonymizedDatabase, config, scope);
    }

    private void copyAndAnonymizeData() {
        anonymizerLogger.info("Started copying data.");
        try {
            anonymizedDatabase.setAutoCommit(false);
        } catch (SQLException | AbstractMethodError e) {
            // no performance gain but not severe
        }

        collectRulesBySite();
        int currentTableNumber = 0;
        for (String table : scope.tables) {
            TableRuleMap ruleMap = buildTableRuleMapFor(table);
            anonymizerLogger.info("Copying data from: " + table + " (table " + (++currentTableNumber) + "/"
                    + scope.tables.size() + ").");
            copyAndAnonymizeTable(ruleMap);
        }

        try {
            anonymizedDatabase.setAutoCommit(true);
        } catch (SQLException | AbstractMethodError e) {
            // probably disabling it earlier failed as well
        }
        anonymizerLogger.info("Finished: Copying Data.");
    }

    private TableRuleMap buildTableRuleMapFor(String table) {
        TableField tableSite = new TableField(table, null, config.schemaName);
        TableRuleMap ruleMap = new TableRuleMap(table);
        for (Map.Entry<TableField, Rule> fieldAndRule : comprehensiveRulesBySite.entries()) {
            TableField tableField = fieldAndRule.getKey();
            if (tableField.asTableSite().equals(tableSite)) {
                Rule rule = fieldAndRule.getValue();
                ruleMap.put(tableField.getColumn(), rule);
            }
        }
        return ruleMap;
    }

    /**
     * Creates a multimap which maps applied strategies to a TableField
     * (table or attribtue) in the correct order of application.
     */
    void collectRulesBySite() {
        RuleConnector ruleConnector = new RuleConnector();
        ruleConnector.addRules(config.getRules());
        Multimap<TableField, Rule> rulesBySite = ruleConnector.getRulesBySite();
        comprehensiveRulesBySite = LinkedHashMultimap.create();
        for (Map.Entry<TableField, Rule> siteAndRule : rulesBySite.entries()) {
            TableField thisSite = siteAndRule.getKey();
            ArrayList<Rule> rulesToApplyToThisSite = new ArrayList<>();
            Rule rule = siteAndRule.getValue();
            rulesToApplyToThisSite.addAll(rule.transitiveParents());
            rulesToApplyToThisSite.add(rule);
            comprehensiveRulesBySite.putAll(thisSite, rulesToApplyToThisSite);
        }

        Iterator<Rule> rulesIterator = comprehensiveRulesBySite.values().iterator();
        while (rulesIterator.hasNext()) {
            Rule eachRule = rulesIterator.next();
            if (eachRule.getTransformation() == null) {
                eachRule.setTransformation(strategyByName.get(eachRule.getStrategy()));
            }
            if (eachRule.getTransformation() instanceof NoOperationStrategy) {
                rulesIterator.remove();
            }
        }
    }

    private void copyAndAnonymizeTable(TableRuleMap tableRuleMap) {
        // make sure target newDB is empty
        String qualifiedTableName = config.schemaName + "." + tableRuleMap.tableName;
        truncateTable(qualifiedTableName);
        ResultSetMetaData rsMeta;
        int rowCount = countRowsInTable(qualifiedTableName);
        if (rowCount > 0)
            anonymizerLogger.info("Found " + rowCount + " rows.");
        try (PreparedStatement selectStarStatement = originalDatabase
                .prepareStatement("SELECT * FROM " + qualifiedTableName);
                ResultSet rs = selectStarStatement.executeQuery()) {
            try {
                rsMeta = rs.getMetaData();

                for (TransformationStrategy strategy : transformationStrategies) {
                    TableRuleMap tableRuleMapForStrategy = tableRuleMap.filteredByStrategy(strategy);
                    if (tableRuleMapForStrategy.isEmpty())
                        continue;
                    strategy.prepareTableTransformation(tableRuleMapForStrategy);
                }

            } catch (SQLException | FetchPseudonymsFailedException e) {
                anonymizerLogger.warning("Fetching rows failed: " + e.getMessage());
                e.printStackTrace();
                return;
            }

            copyAndAnonymizeRows(tableRuleMap, qualifiedTableName, rsMeta, rowCount, rs);

            try {
                anonymizedDatabase.commit();
            } catch (SQLException e) {
                anonymizerLogger.warning("Commit operation concluding table " + qualifiedTableName + " failed.");
            }
        } catch (SQLException e) {
            anonymizerLogger.severe("Could not query table " + qualifiedTableName + ": " + e.getMessage());
        }
    }

    private void copyAndAnonymizeRows(TableRuleMap tableRuleMap, String qualifiedTableName,
            ResultSetMetaData rsMeta, int rowCount, ResultSet rs) throws SQLException {
        // prepared Statement for batch loading
        int columnCount = rsMeta.getColumnCount();
        List<String> columnNames = new ArrayList<>();
        for (int column = 1; column <= columnCount; column++) {
            columnNames.add(rsMeta.getColumnName(column));
        }
        StringBuilder insertQueryBuilder = new StringBuilder();
        insertQueryBuilder.append("INSERT INTO ").append(qualifiedTableName).append(" (")
                .append(Joiner.on(',').join(columnNames)).append(") VALUES (");
        for (int j = 0; j < columnCount - 1; j++)
            insertQueryBuilder.append("?,");
        insertQueryBuilder.append("?)");

        ResultSetRowReader rowReader = new ResultSetRowReader(rs);
        rowReader.setCurrentTable(tableRuleMap.tableName);
        rowReader.setCurrentSchema(config.schemaName);
        try (PreparedStatement insertStatement = anonymizedDatabase
                .prepareStatement(insertQueryBuilder.toString())) {
            int processedRowsCount = 0;
            while (!rs.isClosed() && rs.next()) { // for all rows
                try {
                    copyAndAnonymizeRow(tableRuleMap, qualifiedTableName, rsMeta, columnCount, rowReader,
                            insertStatement);
                } catch (SQLException e) {
                    anonymizerLogger.severe(
                            "SQL error when transforming row #" + (processedRowsCount + 1) + ": " + e.getMessage());
                }
                processedRowsCount++;
                if ((processedRowsCount % config.batchSize) == 0) {
                    try {
                        BatchOperation.executeAndCommit(insertStatement);
                    } catch (SQLException e) {
                        logBatchInsertError(e);
                    }
                }

                if ((processedRowsCount % LOG_INTERVAL) == 0)
                    System.out.format("Progress: %d/%d (%d %%)\r", processedRowsCount, rowCount,
                            100 * processedRowsCount / rowCount);

            }
            if ((processedRowsCount % LOG_INTERVAL) != 0)
                System.out.format("Progress: %d/%d (%d %%)\n", processedRowsCount, rowCount,
                        100 * processedRowsCount / rowCount);
            else
                System.out.format("\n");

            try {
                BatchOperation.executeAndCommit(insertStatement);
            } catch (SQLException e) {
                logBatchInsertError(e);
            }
        } catch (SQLException e) {
            anonymizerLogger
                    .severe("SQL error while traversing table " + qualifiedTableName + ": " + e.getMessage());
        }
    }

    private void logBatchInsertError(SQLException e) {
        anonymizerLogger.severe("Error(s) during batch insert: " + e.getMessage());
        for (Throwable chainedException : Iterables.skip(e, 1)) {
            anonymizerLogger.severe("Insert error: " + chainedException.getMessage());
        }
    }

    private void copyAndAnonymizeRow(TableRuleMap tableRuleMap, String qualifiedTableName, ResultSetMetaData rsMeta,
            int columnCount, ResultSetRowReader rowReader, PreparedStatement insertStatement) throws SQLException {
        boolean retainRow = false;
        if (foreignKeyDeletions.hasParentRowBeenDeleted(rowReader)) {
            retainRow = retainService.currentRowShouldBeRetained(config.schemaName, tableRuleMap.tableName,
                    rowReader);
            if (retainRow) {
                anonymizerLogger.warning("Retaining a row in " + qualifiedTableName + " even though its parent row "
                        + "in another table has been deleted!");
            } else {
                // the parent row has been deleted, so delete this row as well
                // since the foreign key cannot be reestablished and should
                // possible be kept secret
                foreignKeyDeletions.rowHasBeenDeleted(rowReader);
                return;
            }
        }
        // apply rules which have no column name specified first
        // because these are likely to be retain or delete instructions
        for (Rule configRule : tableRuleMap.getRules(null)) {
            if (Iterables.isEmpty(anonymizeValue(null, configRule, rowReader, null, tableRuleMap))) {
                if (retainRow || retainService.currentRowShouldBeRetained(configRule.getTableField().schema,
                        configRule.getTableField().table, rowReader)) {
                    // skip this transformation which deleted the tuple
                    anonymizerLogger.info("Not deleting a row in " + qualifiedTableName + " because it "
                            + "was previously marked to be " + "retained.");
                    retainRow = true;
                    continue;
                }
                foreignKeyDeletions.rowHasBeenDeleted(rowReader);
                return;
            }
        }
        // apply rules to specific columns
        List<Iterable<?>> columnValues = new ArrayList<>(columnCount);
        for (int j = 1; j <= columnCount; j++) { // for all columns
            String columnName = rsMeta.getColumnName(j);
            ImmutableList<Rule> appliedRules = tableRuleMap.getRules(columnName); // check if column needs translation
            if (!appliedRules.isEmpty()) {
                // fetch translations
                Iterable<?> currentValues = Lists.newArrayList(rowReader.getObject(j));
                for (Rule configRule : appliedRules) {
                    Iterable<Object> newValues = Collections.emptyList();
                    for (Object intermediateValue : currentValues) {
                        Iterable<?> transformationResults = anonymizeValue(intermediateValue, configRule, rowReader,
                                columnName, tableRuleMap);
                        newValues = Iterables.concat(newValues, transformationResults);
                    }
                    if (!newValues.iterator().hasNext()) {
                        if (retainRow || retainService.currentRowShouldBeRetained(configRule.getTableField().schema,
                                configRule.getTableField().table, rowReader)) {
                            // skip this transformation which deleted the tuple
                            anonymizerLogger.info("Not deleting a row in " + qualifiedTableName + " because it "
                                    + "was previously marked to be " + "retained.");
                            retainRow = true;
                            continue;
                        }
                        // if a Strategy returned an empty transformation, the
                        // cross product of all column values will be empty,
                        // the original tuple is lost
                        foreignKeyDeletions.rowHasBeenDeleted(rowReader);
                        return;
                    }
                    currentValues = newValues;
                }
                columnValues.add(currentValues);
            } else {
                // if column doesn't have to be anonymized, take old value
                columnValues.add(Lists.newArrayList(rowReader.getObject(j)));
            }
        }
        try {
            addBatchInserts(insertStatement, columnValues);
        } catch (SQLException e) {
            anonymizerLogger.severe("Adding insert statement failed: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Compute the cross product of columnValues and add them to
     * anonymizedDatabaseStatement with {@link PreparedStatement.addBatch}.
     * @param anonymizedDatabaseStatement
     * @param columnValues
     * @throws SQLException if calls to anonymizedDatabaseStatement fail
     */
    private void addBatchInserts(PreparedStatement anonymizedDatabaseStatement, List<Iterable<?>> columnValues)
            throws SQLException {
        addBatchInserts(anonymizedDatabaseStatement, columnValues, 1);
    }

    private void addBatchInserts(PreparedStatement anonymizedDatabaseStatement, List<Iterable<?>> columnValues,
            int columnIndex) throws SQLException {
        if (columnIndex > columnValues.size()) {
            anonymizedDatabaseStatement.addBatch();
        } else {
            for (Object columnValue : columnValues.get(columnIndex - 1)) {
                anonymizedDatabaseStatement.setObject(columnIndex, columnValue);
                addBatchInserts(anonymizedDatabaseStatement, columnValues, columnIndex + 1);
            }
        }
    }

    private Iterable<?> anonymizeValue(Object currentValue, Rule configRule, ResultSetRowReader rowReader,
            String columnName, TableRuleMap tableRules) {
        TransformationStrategy strategy;
        strategy = configRule.getTransformation();
        try {
            return strategy.transform(currentValue, configRule, rowReader);
        } catch (TransformationKeyNotFoundException e) {
            anonymizerLogger.log(Level.SEVERE,
                    "Transformation value for \"" + currentValue + "\" (from table " + tableRules.tableName + "."
                            + columnName + ") was not found in keys for " + configRule.getTableField()
                            + ". Used empty String instead.",
                    e);
        } catch (SQLException e) {
            anonymizerLogger.severe("SQL error while transforming value \"" + currentValue + "\" (from table "
                    + tableRules.tableName + "." + columnName + ") : " + e.getMessage()
                    + ". Using empty String instead.");
        } catch (TransformationFailedException e) {
            anonymizerLogger.severe(e.getMessage());
            Throwable t = e;
            while ((t = t.getCause()) != null) {
                anonymizerLogger.severe("caused by: " + t.getMessage());
            }
        }
        return Lists.newArrayList("");
    }

    private int countRowsInTable(String qualifiedTableName) {
        int rowCount = -1;
        try (Statement countStatement = originalDatabase.createStatement();
                ResultSet countResult = countStatement.executeQuery("SELECT COUNT(*) FROM " + qualifiedTableName)) {
            countResult.next();
            rowCount = countResult.getInt(1);
        } catch (SQLException e) {
            anonymizerLogger.info("Could not retrieve row count for table " + qualifiedTableName
                    + " (error message follows), " + "progress report will be awkward.");
            anonymizerLogger.warning(e.getMessage());
        }
        return rowCount;
    }

    private void truncateTable(String qualifiedTableName) {
        TableTruncater.truncateTable(qualifiedTableName, anonymizedDatabase);
    }

    private void prepareTransformations() throws FetchPseudonymsFailedException, TransformationKeyCreationException,
            TransformationTableCreationException, ColumnTypeNotSupportedException, PreparationFailedException {
        anonymizerLogger.info("Setting up transformations.");
        try {
            createSchemaInTransformataionDatabase();
        } catch (SQLException e) {
            anonymizerLogger.warning(
                    "Could not create schema in the " + "transformation database, this might cause the creation "
                            + "of pseudonym tables or others to fail.");
        }
        Multimap<TransformationStrategy, Rule> rulesByStrategy = ArrayListMultimap.create();
        for (Rule rule : config.rules) {
            if (!scope.tables.contains(rule.getTableField().table)) {
                anonymizerLogger.warning("Table " + rule.getTableField().table
                        + " not in scope. Skipping dependants and continuing.");
                continue;
            }
            rulesByStrategy.put(rule.getTransformation(), rule);
            for (TableField dependant : rule.getDependants()) {
                if (!scope.tables.contains(dependant.table)) {
                    anonymizerLogger.warning("Dependend table " + dependant.table
                            + " not in scope. Skipping dependant and continuing.");
                    continue;
                }
            }
        }
        for (TransformationStrategy strategy : rulesByStrategy.keySet()) {
            anonymizerLogger.info("Setting up transformations for " + strategy.getClass().getSimpleName());
            strategy.setUpTransformation(rulesByStrategy.get(strategy));
        }
        anonymizerLogger.info("Finished: setting up transformations.");
    }

    private void createSchemaInTransformataionDatabase() throws SQLException {
        try (ResultSet schemasResult = transformationDB.getMetaData().getSchemas(null, config.schemaName)) {
            if (schemasResult.next())
                return; // schema is already present
        }
        SQLHelper.createSchema(config.schemaName, transformationDB);
    }

    /**
     * Retrieves all rules which are applied to the specified attribute in the
     * order of application.
     * 
     * @param tableName table which contains the attribute
     * @param columnName column name of the attribute
     */
    public Collection<Rule> getRulesFor(String tableName, String columnName) {
        return comprehensiveRulesBySite.get(new TableField(tableName, columnName, config.getSchemaName()));
    }

    public boolean isTableInScope(TableField tableSite) {
        return scope.tables.contains(tableSite.getTable());
    }

    public boolean isTableInScope(String tableName) {
        return scope.tables.contains(tableName);
    }

}