org.orbisgis.corejdbc.ReadTable.java Source code

Java tutorial

Introduction

Here is the source code for org.orbisgis.corejdbc.ReadTable.java

Source

/**
 * OrbisGIS is a java GIS application dedicated to research in GIScience.
 * OrbisGIS is developed by the GIS group of the DECIDE team of the 
 * Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
 *
 * The GIS group of the DECIDE team is located at :
 *
 * Laboratoire Lab-STICC  CNRS UMR 6285
 * Equipe DECIDE
 * UNIVERSIT DE BRETAGNE-SUD
 * Institut Universitaire de Technologie de Vannes
 * 8, Rue Montaigne - BP 561 56017 Vannes Cedex
 * 
 * OrbisGIS is distributed under GPL 3 license.
 *
 * Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
 * Copyright (C) 2015-2016 CNRS (Lab-STICC UMR CNRS 6285)
 *
 * This file is part of OrbisGIS.
 *
 * OrbisGIS is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * OrbisGIS is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
 *
 * For more information, please consult: <http://www.orbisgis.org/>
 * or contact directly:
 * info_at_ orbisgis.org
 */
package org.orbisgis.corejdbc;

import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import org.h2gis.utilities.JDBCUtilities;
import org.h2gis.utilities.SFSUtilities;
import org.h2gis.utilities.SpatialResultSet;
import org.h2gis.utilities.TableLocation;
import org.orbisgis.commons.progress.ProgressMonitor;
import org.orbisgis.corejdbc.common.LongUnion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;

import java.beans.EventHandler;
import java.beans.PropertyChangeListener;
import java.io.Closeable;
import java.io.IOException;
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.text.NumberFormat;
import java.util.*;
import java.util.concurrent.locks.Lock;

import org.apache.commons.math3.stat.descriptive.SummaryStatistics;

/**
 * JDBC operations that does not affect database.
 * @author Nicolas Fortin
 */
public class ReadTable {
    /** SQL function to evaluate */
    public enum STATS {
        COUNT, SUM, AVG, STDDEV_SAMP, MIN, MAX
    }

    protected final static I18n I18N = I18nFactory.getI18n(ReadTable.class, Locale.getDefault(),
            I18nFactory.FALLBACK);
    private static Logger LOGGER = LoggerFactory.getLogger(ReadTable.class);
    private static final int INSERT_BATCH_SIZE = 30;

    public static Collection<Integer> getSortedColumnRowIndex(Connection connection, ReadRowSet originalOrder,
            String table, String originalColumnName, boolean ascending, ProgressMonitor progressMonitor)
            throws SQLException {
        String quoteIdentifier = TableLocation.quoteIdentifier(originalColumnName);
        TableLocation tableLocation = TableLocation.parse(table);
        Collection<Integer> columnValues;
        try (Statement st = connection.createStatement()) {
            int rowCount = 0;
            try (ResultSet rs = st.executeQuery("SELECT COUNT(*) cpt from " + tableLocation.toString())) {
                if (rs.next()) {
                    rowCount = rs.getInt(1);
                }
            }
            columnValues = new ArrayList<>(rowCount);
            PropertyChangeListener listener = EventHandler.create(PropertyChangeListener.class, st, "cancel");
            progressMonitor.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, listener);
            try {
                int pkIndex = JDBCUtilities.getIntegerPrimaryKey(connection, tableLocation.toString());
                if (pkIndex > 0) {
                    ProgressMonitor jobProgress = progressMonitor.startTask(2);
                    // Do not cache values
                    // Use SQL sort
                    DatabaseMetaData meta = connection.getMetaData();
                    String pkFieldName = TableLocation
                            .quoteIdentifier(JDBCUtilities.getFieldName(meta, table, pkIndex));
                    String desc = "";
                    if (!ascending) {
                        desc = " DESC";
                    }
                    // Create a map of Row Id to Pk Value
                    ProgressMonitor cacheProgress = jobProgress.startTask(I18N.tr("Cache primary key values"),
                            rowCount);
                    Map<Long, Integer> pkValueToRowId = new HashMap<>(rowCount);
                    int rowId = 0;
                    Lock lock = originalOrder.getReadLock();
                    lock.tryLock();
                    try {
                        originalOrder.beforeFirst();
                        while (originalOrder.next()) {
                            rowId++;
                            pkValueToRowId.put(originalOrder.getPk(), rowId);
                            cacheProgress.endTask();
                        }
                    } finally {
                        lock.unlock();
                    }
                    // Read ordered pk values
                    ProgressMonitor sortProgress = jobProgress.startTask(I18N.tr("Read sorted keys"), rowCount);
                    try (ResultSet rs = st.executeQuery(
                            "select " + pkFieldName + " from " + table + " ORDER BY " + quoteIdentifier + desc)) {
                        while (rs.next()) {
                            columnValues.add(pkValueToRowId.get(rs.getLong(1)));
                            sortProgress.endTask();
                        }
                    }
                } else {
                    ProgressMonitor jobProgress = progressMonitor.startTask(2);
                    //Cache values
                    ProgressMonitor cacheProgress = jobProgress.startTask(I18N.tr("Cache table values"), rowCount);
                    Comparable[] cache = new Comparable[rowCount];
                    Lock lock = originalOrder.getReadLock();
                    lock.tryLock();
                    try {
                        originalOrder.beforeFirst();
                        int i = 0;
                        final int fieldIndex = originalOrder.findColumn(originalColumnName);
                        long time1 = 0, time2 = 0, time3 = 0, time4 = 0, time5 = 0;
                        time5 -= System.currentTimeMillis();
                        while (originalOrder.next()) {
                            time5 += System.currentTimeMillis();
                            time1 -= System.currentTimeMillis();
                            Object obj = originalOrder.getObject(fieldIndex);
                            time1 += System.currentTimeMillis();
                            time2 -= System.currentTimeMillis();
                            if (obj != null && !(obj instanceof Comparable)) {
                                throw new SQLException(I18N.tr("Could only sort comparable database object type"));
                            }
                            time2 += System.currentTimeMillis();
                            time3 -= System.currentTimeMillis();
                            cache[i++] = (Comparable) obj;
                            time3 += System.currentTimeMillis();
                            time4 -= System.currentTimeMillis();
                            cacheProgress.endTask();
                            time4 += System.currentTimeMillis();
                            time5 -= System.currentTimeMillis();
                        }
                        time5 += System.currentTimeMillis();
                        System.out.println("time 1:" + time1 + ";" + "time 2:" + time2 + ";" + "time 3:" + time3
                                + ";" + "time 4:" + time4 + ";" + "time 5:" + time5 + ";");
                    } finally {
                        lock.unlock();
                    }
                    ProgressMonitor sortProgress = jobProgress.startTask(I18N.tr("Sort table values"), rowCount);
                    Comparator<Integer> comparator = new SortValueCachedComparator(cache);
                    if (!ascending) {
                        comparator = Collections.reverseOrder(comparator);
                    }
                    columnValues = new TreeSet<>(comparator);
                    for (int i = 1; i <= rowCount; i++) {
                        columnValues.add(i);
                        sortProgress.endTask();
                    }
                }
            } finally {
                progressMonitor.removePropertyChangeListener(listener);
            }
            return columnValues;
        }
    }

    public static SortedSet<Long> getRowPkFromRowNumber(ReadRowSet rowSet, SortedSet<Integer> rowNumber)
            throws SQLException {
        SortedSet<Long> modelRows = new LongUnion();
        for (int rowNum : rowNumber) {
            rowSet.absolute(rowNum);
            modelRows.add(rowSet.getPk());
        }
        return modelRows;
    }

    public static long getRowCount(Connection connection, String tableReference) throws SQLException {
        TableLocation tableLocation = TableLocation.parse(tableReference);
        if (JDBCUtilities.isH2DataBase(connection.getMetaData())) {
            try (PreparedStatement st = SFSUtilities.prepareInformationSchemaStatement(connection,
                    tableLocation.getCatalog(), tableLocation.getSchema(), tableLocation.getTable(),
                    "INFORMATION_SCHEMA.TABLES", "", "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME");
                    ResultSet rs = st.executeQuery()) {
                if (rs.next()) {
                    long estimatedRowCount = rs.getLong("ROW_COUNT_ESTIMATE");
                    // 100 because H2 views est
                    if (estimatedRowCount > 0 && !"VIEW".equalsIgnoreCase(rs.getString("TABLE_TYPE"))) {
                        return estimatedRowCount;
                    }
                }
            } catch (Exception ex) {
                // This method failed, will use standard one
                LOGGER.debug(ex.getLocalizedMessage(), ex);
            }
        }
        // Use  precise row count
        try (Statement st = connection.createStatement();
                ResultSet rs = st.executeQuery("SELECT COUNT(*) cpt FROM " + tableReference)) {
            rs.next();
            return rs.getLong(1);
        }
    }

    /**
     * Return a concatened and human readable format of provided result set
     * @param rs result set to read
     * @param maxFieldLength Maximum field length to print
     * @param maxPrintedRows Maximum printed rows
     * @param addColumns Add column header
     * @param alignColumns Align columns by using padding
     * @return human readable format of provided result set
     * @throws SQLException
     */
    public static String resultSetToString(ResultSet rs, int maxFieldLength, int maxPrintedRows, boolean addColumns,
            boolean alignColumns) throws SQLException {
        return resultSetToString(rs, maxFieldLength, maxPrintedRows, addColumns, alignColumns,
                new AcceptAllFilter());
    }

    /**
     * Return a concatened and human readable format of provided result set
     * @param rs result set to read
     * @param maxFieldLength Maximum field length to print
     * @param maxPrintedRows Maximum printed rows
     * @param addColumns Add column header
     * @param alignColumns Align columns by using padding
     * @param resultSetFilter Accept or refuse rows by implementing this interface
     * @return human readable format of provided result set
     * @throws SQLException
     */
    public static String resultSetToString(ResultSet rs, int maxFieldLength, int maxPrintedRows, boolean addColumns,
            boolean alignColumns, ResultSetFilter resultSetFilter) throws SQLException {
        // Print headers
        ResultSetMetaData metaData = rs.getMetaData();
        int columnCount = metaData.getColumnCount();
        StringBuilder lines = new StringBuilder();
        StringBuilder formatStringBuilder = new StringBuilder();
        String[] header = new String[columnCount];
        for (int idColumn = 1; idColumn <= columnCount; idColumn++) {
            header[idColumn - 1] = metaData.getColumnLabel(idColumn) + "(" + metaData.getColumnTypeName(idColumn)
                    + ")";
            if (alignColumns) {
                formatStringBuilder.append("%-");
                formatStringBuilder.append(maxFieldLength);
                formatStringBuilder.append("s ");
            } else {
                formatStringBuilder.append("%s ");
            }
        }
        if (addColumns) {
            lines.append(String.format(formatStringBuilder.toString(), header));
            lines.append("\n");
        }
        int shownLines = 0;
        NumberFormat decimalFormat = NumberFormat.getInstance(Locale.getDefault());
        decimalFormat.setGroupingUsed(false);
        decimalFormat.setMaximumFractionDigits(16);
        while (rs.next() && shownLines < maxPrintedRows) {
            if (resultSetFilter.printRow(rs)) {
                String[] row = new String[columnCount];
                for (int idColumn = 1; idColumn <= columnCount; idColumn++) {
                    Object valObj = rs.getObject(idColumn);
                    String value;
                    if (valObj instanceof Number) {
                        value = decimalFormat.format(valObj);
                    } else {
                        value = rs.getString(idColumn);
                    }
                    if (value != null) {
                        if (columnCount > 1 && value.length() > maxFieldLength) {
                            value = value.substring(0, maxFieldLength - 2) + "..";
                        }
                    } else {
                        value = "NULL";
                    }
                    row[idColumn - 1] = value;
                }
                shownLines++;
                lines.append(String.format(formatStringBuilder.toString(), row));
                lines.append("\n");
            }
        }
        if (lines.length() != 0) {
            return lines.toString();
        } else {
            return I18N.tr("No attributes to show");
        }
    }

    private static class AcceptAllFilter implements ResultSetFilter {
        @Override
        public boolean printRow(ResultSet rs) {
            return true;
        }
    }

    public static interface ResultSetFilter {
        /**
         * @param rs Result set in valid row
         * @return True if the row should be printed
         */
        boolean printRow(ResultSet rs) throws SQLException;
    }

    /**
     * Compute numeric stats of the specified table column.
     * @param connection Available connection
     * @param tableName Table name
     * @param columnName Column name
     * @param pm Progress monitor
     * @return An array of attributes {@link STATS}
     * @throws SQLException
     */
    public static String[] computeStatsSQL(Connection connection, String tableName, String columnName,
            ProgressMonitor pm) throws SQLException {
        String[] stats = new String[STATS.values().length];
        StringBuilder sb = new StringBuilder();
        for (STATS func : STATS.values()) {
            if (sb.length() != 0) {
                sb.append(", ");
            }
            sb.append(func.name());
            sb.append("(");
            sb.append(columnName);
            sb.append("::double precision) ");
            sb.append(func.name());
        }
        try (Statement st = connection.createStatement()) {

            // Cancel select
            PropertyChangeListener listener = EventHandler.create(PropertyChangeListener.class, st, "cancel");
            pm.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, listener);
            try (ResultSet rs = st.executeQuery(String.format("SELECT %s FROM %s", sb.toString(), tableName))) {
                if (rs.next()) {
                    for (STATS func : STATS.values()) {
                        stats[func.ordinal()] = rs.getString(func.name());
                    }
                }
            } finally {
                pm.removePropertyChangeListener(listener);
            }
        }
        return stats;
    }

    /**
     * Compute numeric stats of the specified table column using a limited input rows. Stats are not done in the sql side.
     * @param connection Available connection
     * @param tableName Table name
     * @param columnName Column name
     * @param rowNum Row id
     * @param pm Progress monitor
     * @return An array of attributes {@link STATS}
     * @throws SQLException
     */
    public static String[] computeStatsLocal(Connection connection, String tableName, String columnName,
            SortedSet<Integer> rowNum, ProgressMonitor pm) throws SQLException {
        String[] res = new String[STATS.values().length];
        SummaryStatistics stats = new SummaryStatistics();
        try (Statement st = connection.createStatement()) {
            // Cancel select
            PropertyChangeListener listener = EventHandler.create(PropertyChangeListener.class, st, "cancel");
            pm.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, listener);
            try (ResultSet rs = st.executeQuery(String.format("SELECT %s FROM %s", columnName, tableName))) {
                ProgressMonitor fetchProgress = pm.startTask(rowNum.size());
                while (rs.next() && !pm.isCancelled()) {
                    if (rowNum.contains(rs.getRow())) {
                        stats.addValue(rs.getDouble(columnName));
                        fetchProgress.endTask();
                    }
                }
            } finally {
                pm.removePropertyChangeListener(listener);
            }
        }
        res[STATS.SUM.ordinal()] = Double.toString(stats.getSum());
        res[STATS.AVG.ordinal()] = Double.toString(stats.getMean());
        res[STATS.COUNT.ordinal()] = Long.toString(stats.getN());
        res[STATS.MIN.ordinal()] = Double.toString(stats.getMin());
        res[STATS.MAX.ordinal()] = Double.toString(stats.getMax());
        res[STATS.STDDEV_SAMP.ordinal()] = Double.toString(stats.getStandardDeviation());
        return res;
    }

    /**
     * Retrieve the envelope of selection of lines
     * @param manager Data Manager
     * @param tableName Table identifier [[catalog.]schema.]table
     * @param rowsId Line number [1-n]
     * @param pm Progress monitor
     * @return Envelope of rows
     * @throws SQLException
     */
    public static Envelope getTableSelectionEnvelope(DataManager manager, String tableName, SortedSet<Long> rowsId,
            ProgressMonitor pm) throws SQLException {
        try (Connection connection = manager.getDataSource().getConnection()) {
            Envelope selectionEnvelope = null;
            List<String> geomFields = SFSUtilities.getGeometryFields(connection, TableLocation.parse(tableName));
            if (geomFields.isEmpty()) {
                throw new SQLException(I18N.tr("Table table {0} does not contain any geometry fields", tableName));
            }
            String fieldQuery = "ST_EXTENT(" + TableLocation.quoteIdentifier(geomFields.get(0)) + ")";
            try (FilteredResultSet fRs = new FilteredResultSet(connection, tableName, rowsId, pm, false,
                    fieldQuery)) {
                SpatialResultSet rs = fRs.getResultSet();
                if (rs.next() && rs.getGeometry() != null) {
                    selectionEnvelope = rs.getGeometry().getEnvelopeInternal();
                }
                return selectionEnvelope;
            } catch (IOException ex) {
                throw new SQLException(ex.getLocalizedMessage(), ex);
            }
        }
    }

    /**
     *
     * @param dataManager RowSet factory
     * @param table Table identifier
     * @param geometryColumn Name of the geometry column
     * @param selection Selection polygon
     * @param contains If true selection is used with contains, else this is intersects.
     * @return List of row id.
     * @throws SQLException
     */
    public static Set<Long> getTablePkByEnvelope(DataManager dataManager, String table, String geometryColumn,
            Geometry selection, boolean contains) throws SQLException {
        Set<Long> newSelection = new HashSet<>(50);
        TableLocation tableLocation = TableLocation.parse(table);
        // There is a where condition then system row index can't be used
        try (Connection connection = dataManager.getDataSource().getConnection()) {
            String pkName = MetaData.getPkName(connection, tableLocation.toString(), true);
            boolean isH2 = JDBCUtilities.isH2DataBase(connection.getMetaData());
            if (!pkName.isEmpty()) {
                String from = tableLocation.toString();
                String sqlFunction = contains ? "ST_CONTAINS(?, %s)" : "ST_INTERSECTS(?, %s)";
                try (PreparedStatement st = connection.prepareStatement(String.format(
                        "SELECT %s FROM %s WHERE %s && ? AND " + sqlFunction, TableLocation.quoteIdentifier(pkName),
                        from, TableLocation.quoteIdentifier(geometryColumn),
                        TableLocation.quoteIdentifier(geometryColumn)))) {
                    st.setObject(1, selection);
                    st.setObject(2, selection);
                    try (SpatialResultSet rs = st.executeQuery().unwrap(SpatialResultSet.class)) {
                        while (rs.next()) {
                            newSelection.add(rs.getLong(1));
                        }
                    }
                }
            } else {
                throw new SQLException(
                        I18N.tr("Table " + table + " do not contain any information in order to identify row"));
            }
        }
        return newSelection;
    }

    public static class FilteredResultSet implements Closeable {
        private SpatialResultSet resultSet;
        private String selectionTable;
        private Statement st;
        private Connection connection;

        /**
         * @param connection Active connection
         * @param tableName Table to query
         * @param rowsId Primary keys to filter
         * @param pm Progress information and cancel
         * @param geometryOnly Retrieve only the first geometry field
         * @param customFields Query the table with custom field selection
         * @throws SQLException If an error occurred
         */
        public FilteredResultSet(Connection connection, String tableName, SortedSet<Long> rowsId,
                ProgressMonitor pm, boolean geometryOnly, String customFields) throws SQLException {
            this.connection = connection;
            st = connection.createStatement();
            PropertyChangeListener cancelListener = EventHandler.create(PropertyChangeListener.class, st, "cancel");
            pm.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, cancelListener);
            try {
                String fields = "t1.*";
                if (geometryOnly) {
                    List<String> geomFields = SFSUtilities.getGeometryFields(connection,
                            TableLocation.parse(tableName));
                    if (geomFields.isEmpty()) {
                        throw new SQLException(
                                I18N.tr("Table table {0} does not contain any geometry fields", tableName));
                    }
                    fields = TableLocation.quoteIdentifier(geomFields.get(0));
                } else if (customFields != null && !customFields.isEmpty()) {
                    fields = customFields;
                }
                // Create a temporary table that contain selected pk
                selectionTable = CreateTable.createIndexTempTable(connection, pm, rowsId, "pk", INSERT_BATCH_SIZE);
                String pkName = MetaData.getPkName(connection, tableName, true);
                StringBuilder pkEquality = new StringBuilder("t1." + pkName + " = ");
                if (!pkName.equals(MetaData.POSTGRE_ROW_IDENTIFIER)) {
                    pkEquality.append("t2.pk");
                } else {
                    pkEquality.append(MetaData.castLongToTid("t2.pk"));
                }
                // Join with temp table and compute the envelope on the server side
                resultSet = st.executeQuery("SELECT " + fields + " FROM " + tableName + " t1, " + selectionTable
                        + " t2 where " + pkEquality).unwrap(SpatialResultSet.class);
            } finally {
                pm.removePropertyChangeListener(cancelListener);
            }
        }

        public SpatialResultSet getResultSet() {
            return resultSet;
        }

        @Override
        public void close() throws IOException {
            try {
                resultSet.close();
                st.execute("DROP TABLE IF EXISTS " + selectionTable);
                st.close();
            } catch (SQLException ex) {
                throw new IOException(ex.getLocalizedMessage(), ex);
            }
        }
    }
}