org.rhq.plugins.postgres.PostgresTableComponent.java Source code

Java tutorial

Introduction

Here is the source code for org.rhq.plugins.postgres.PostgresTableComponent.java

Source

/*
 * RHQ Management Platform
 * Copyright (C) 2005-2014 Red Hat, Inc.
 * All rights reserved.
 *
 * This program 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 version 2 of the License.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

package org.rhq.plugins.postgres;

import static org.rhq.core.domain.measurement.AvailabilityType.DOWN;
import static org.rhq.core.domain.measurement.AvailabilityType.UNKNOWN;
import static org.rhq.core.domain.measurement.AvailabilityType.UP;
import static org.rhq.core.util.StringUtil.isBlank;
import static org.rhq.plugins.database.DatabasePluginUtil.getNumericQueryValues;
import static org.rhq.plugins.database.DatabasePluginUtil.getSingleNumericQueryValue;
import static org.rhq.plugins.database.DatabasePluginUtil.safeClose;
import static org.rhq.plugins.postgres.PostgresTableDiscoveryComponent.SCHEMA_SEPARATOR;

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.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.plugins.database.ConnectionPoolingSupport;
import org.rhq.plugins.database.DatabaseComponent;
import org.rhq.plugins.database.DatabaseQueryUtility;
import org.rhq.plugins.database.PooledConnectionProvider;

/**
 * Represents a postgres table
 *
 * @author Greg Hinkle
 */
public class PostgresTableComponent implements DatabaseComponent<PostgresDatabaseComponent>,
        ConnectionPoolingSupport, MeasurementFacet, ConfigurationFacet, DeleteResourceFacet, OperationFacet {

    private static final Log LOG = LogFactory.getLog(PostgresTableComponent.class);

    private static final String TABLE_EXISTS_QUERY = "select 1 from pg_stat_user_tables "
            + "where schemaname = ? and relname = ?";

    private static final String TABLE_STATS_QUERY = "select ts.*,  "
            + "pg_relation_size(ts.relid) AS table_size, pg_total_relation_size(ts.relid) AS total_size, "
            + "ios.heap_blks_read, ios.heap_blks_hit, ios.idx_blks_read, ios.idx_blks_hit, "
            + "ios.toast_blks_read, ios.toast_blks_hit, ios.tidx_blks_read, ios.tidx_blks_hit "
            + "from pg_stat_user_tables ts left join pg_statio_user_tables ios on ts.relid = ios.relid "
            + "where ts.schemaname = ? and ts.relname = ?";

    private static final String TABLE_ROW_COUNT_APPROX_QUERY = "select pgc.reltuples "
            + "from pg_class pgc, pg_namespace pgn "
            + "where pgn.nspname = ? and pgc.relname = ? and pgc.relnamespace = pgn.oid";

    /**
     * @deprecated as of RHQ4.11. No longer used (and shouldn't have been exposed anyway).
     */
    @Deprecated
    public static final String PG_STAT_USER_TABLES_QUERY = "SELECT ts.*,  pg_relation_size(ts.relid) AS table_size, pg_total_relation_size(ts.relid) AS total_size, \n"
            + "  ios.heap_blks_read, ios.heap_blks_hit, ios.idx_blks_read, ios.idx_blks_hit, \n"
            + "  ios.toast_blks_read, ios.toast_blks_hit, ios.tidx_blks_read, ios.tidx_blks_hit \n"
            + "FROM pg_stat_user_tables ts LEFT JOIN pg_statio_user_tables ios on ts.relid = ios.relid \n"
            + "WHERE ts.relname = ?";

    /**
     * @deprecated as of RHQ4.11. No longer used (and shouldn't have been exposed anyway).
     */
    @Deprecated
    public static final String PG_COUNT_ROWS = "SELECT COUNT(*) FROM ";

    /**
     * @deprecated as of RHQ4.11. No longer used (and shouldn't have been exposed anyway).
     */
    @Deprecated
    public static final String PG_COUNT_ROWS_APPROX = "SELECT reltuples FROM pg_class WHERE relname = ? ";

    private ResourceContext<PostgresDatabaseComponent> resourceContext;

    public void start(ResourceContext<PostgresDatabaseComponent> context) {
        if (!context.getResourceKey().contains(SCHEMA_SEPARATOR)) {
            throw new InvalidPluginConfigurationException("Resource key in old format (missing schema name)");
        }
        if (isBlank(getSchemaNameFromContext(context))) {
            throw new InvalidPluginConfigurationException("schemaName is not defined");
        }
        if (isBlank(getTableNameFromContext(context))) {
            throw new InvalidPluginConfigurationException("tableName is not defined");
        }
        try {
            if (!tableExists(context)) {
                throw new InvalidPluginConfigurationException("table does not exist");
            }
        } catch (SQLException e) {
            throw new InvalidPluginConfigurationException("Exception while checking table existence", e);
        }
        this.resourceContext = context;
    }

    private boolean tableExists(ResourceContext<PostgresDatabaseComponent> context) throws SQLException {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = context.getParentResourceComponent().getPooledConnectionProvider().getPooledConnection();
            statement = connection.prepareStatement(TABLE_EXISTS_QUERY);
            statement.setString(1, getSchemaNameFromContext(context));
            statement.setString(2, getTableNameFromContext(context)); // Do not use quoted name here
            resultSet = statement.executeQuery();
            return resultSet.next();
        } finally {
            safeClose(connection, statement, resultSet);
        }
    }

    public void stop() {
        this.resourceContext = null;
    }

    @Override
    public boolean supportsConnectionPooling() {
        return true;
    }

    @Override
    public PooledConnectionProvider getPooledConnectionProvider() {
        return resourceContext.getParentResourceComponent().getPooledConnectionProvider();
    }

    /**
     * May be useful for child components.
     *
     * @return the name of the schema the table belongs to
     */
    public String getSchemaName() {
        return getSchemaNameFromContext(resourceContext);
    }

    /**
     * May be useful for child components.
     *
     * @return the name of the table
     */
    public String getTableName() {
        return getTableNameFromContext(resourceContext);
    }

    public AvailabilityType getAvailability() {
        try {
            return tableExists(resourceContext) ? UP : DOWN;
        } catch (SQLException e) {
            LOG.debug("Exception while checking table existence", e);
            return UNKNOWN;
        }
    }

    public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
        String tableName = getTableNameFromContext(resourceContext);
        String schemaName = getSchemaNameFromContext(resourceContext);
        Map<String, Double> results = getNumericQueryValues(this, TABLE_STATS_QUERY, schemaName, tableName);
        for (MeasurementScheduleRequest request : requests) {
            String metricName = request.getName();
            Double value;
            if (metricName.equals("rows")) {
                value = getSingleNumericQueryValue(this, getCountQuery(schemaName, tableName));
            } else if (metricName.equals("rows_approx")) {
                value = getSingleNumericQueryValue(this, TABLE_ROW_COUNT_APPROX_QUERY, schemaName, tableName);
            } else {
                value = results.get(metricName);
            }
            if (value != null) {
                MeasurementDataNumeric mdn = new MeasurementDataNumeric(request, value);
                report.addData(mdn);
            }
        }
    }

    private String getCountQuery(String schemaName, String tableName) {
        return "select count(1) from " + getFullyQualifiedTableName(schemaName, getQuoted(tableName));
    }

    private String getFullyQualifiedTableName(String schemaName, String tableName) {
        return schemaName + SCHEMA_SEPARATOR + tableName;
    }

    public void deleteResource() throws Exception {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = getPooledConnectionProvider().getPooledConnection();
            statement = connection.prepareStatement(
                    "drop table " + getFullyQualifiedTableName(getSchemaNameFromContext(resourceContext),
                            getQuoted(getTableNameFromContext(resourceContext))));
            statement.executeUpdate();
        } finally {
            safeClose(connection, statement);
        }
    }

    public Configuration loadResourceConfiguration() throws Exception {
        Configuration config = new Configuration();
        config.put(new PropertySimple("schemaName",
                resourceContext.getPluginConfiguration().getSimple("schemaName").getStringValue()));
        config.put(new PropertySimple("tableName",
                resourceContext.getPluginConfiguration().getSimple("tableName").getStringValue()));

        Connection connection = null;
        ResultSet columns;
        try {
            connection = this.resourceContext.getParentResourceComponent().getConnection();
            DatabaseMetaData databaseMetaData = connection.getMetaData();
            columns = databaseMetaData.getColumns("", getSchemaNameFromContext(resourceContext),
                    getTableNameFromContext(resourceContext), "");
            PropertyList columnList = new PropertyList("columns");

            while (columns.next()) {
                PropertyMap col = new PropertyMap("columnDefinition");

                col.put(new PropertySimple("columnName", columns.getString("COLUMN_NAME")));
                col.put(new PropertySimple("columnType", columns.getString("TYPE_NAME")));
                col.put(new PropertySimple("columnLength", columns.getInt("COLUMN_SIZE")));
                col.put(new PropertySimple("columnPrecision", columns.getInt("DECIMAL_DIGITS")));
                col.put(new PropertySimple("columnDefault", columns.getString("COLUMN_DEF")));
                col.put(new PropertySimple("columnNullable",
                        Boolean.valueOf(isNullableToBoolean(columns.getInt("NULLABLE")))));

                columnList.add(col);
            }

            config.put(columnList);
        } finally {
            safeClose(connection);
        }

        return config;
    }

    public void updateResourceConfiguration(ConfigurationUpdateReport report) {
        try {
            Configuration updatedConfiguration = report.getConfiguration();
            PropertyList updatedColumns = updatedConfiguration.getList("columns");

            Connection connection = this.resourceContext.getParentResourceComponent().getConnection();

            DatabaseMetaData dmd = connection.getMetaData();
            ResultSet rs = dmd.getColumns("", "", getTableNameFromContext(resourceContext), "");
            Map<String, ColumnDefinition> existingDefs = new HashMap<String, ColumnDefinition>();
            try {
                while (rs.next()) {
                    ColumnDefinition def = new ColumnDefinition(rs);
                    existingDefs.put(def.columnName, def);
                }
            } finally {
                rs.close();
            }

            for (Property newColumnDefinition : updatedColumns.getList()) {
                PropertyMap colDef = (PropertyMap) newColumnDefinition;

                ColumnDefinition existingDef = existingDefs.get(colDef.getSimple("columnName").getStringValue());
                ColumnDefinition newDef = new ColumnDefinition(colDef);
                if (existingDef == null) {
                    // This is a new column to add
                    String sql = "ALTER TABLE " + getQuoted(getTableNameFromContext(resourceContext))
                            + " ADD COLUMN " + newDef.getColumnSql();
                    if (DatabaseQueryUtility.executeUpdate(this, sql) != 0) {
                        throw new RuntimeException("Couldn't add column using SQL: " + sql);
                    }
                } else {
                    existingDefs.remove(existingDef.columnName);
                    boolean columnLengthChanged = ((existingDef.columnLength != null
                            && !existingDef.columnLength.equals(newDef.columnLength))
                            || (existingDef.columnLength == null && existingDef.columnLength != null));
                    boolean columnPrecisionChanged = ((existingDef.columnPrecision != null
                            && !existingDef.columnPrecision.equals(newDef.columnPrecision))
                            || (existingDef.columnPrecision == null && existingDef.columnPrecision != null));
                    if (!existingDef.columnType.equals(newDef.columnType) || columnLengthChanged
                            || columnPrecisionChanged) {
                        String sql = "ALTER TABLE " + getQuoted(getTableNameFromContext(resourceContext))
                                + " ALTER COLUMN " + getQuoted(newDef.columnName) + " TYPE " + newDef.columnType;
                        if (newDef.columnLength != null) {
                            sql += " ( " + newDef.columnLength;
                            // TODO: Implement a more robust check to figure out if this column has a numeric type.
                            if (newDef.columnPrecision != null && !newDef.columnType.startsWith("varchar"))
                                sql += ", " + newDef.columnPrecision;
                            sql += " ) ";
                        }

                        if (DatabaseQueryUtility.executeUpdate(this, sql) != 1) {
                            throw new RuntimeException("Couldn't alter column type using SQL: " + sql);
                        }
                    }

                    // Set default separately.
                    boolean columnDefaultChanged = ((existingDef.columnDefault != null
                            && !existingDef.columnDefault.equals(newDef.columnDefault))
                            || (existingDef.columnDefault == null && newDef.columnDefault != null));
                    if (columnDefaultChanged) {
                        String sql = "ALTER TABLE " + getQuoted(getTableNameFromContext(resourceContext))
                                + " ALTER COLUMN " + getQuoted(newDef.columnName);
                        if (newDef.columnDefault == null) {
                            sql += " DROP DEFAULT";
                        } else {
                            sql += " SET DEFAULT " + newDef.columnDefault;
                        }

                        if (DatabaseQueryUtility.executeUpdate(this, sql) != 1) {
                            throw new RuntimeException("Couldn't update column default using SQL: " + sql);
                        }
                    }
                }
            }

            // Cols left in existdef map have been removed and need to be dropped
            for (ColumnDefinition def : existingDefs.values()) {
                DatabaseQueryUtility.executeUpdate(this,
                        "ALTER TABLE " + getQuoted(getTableNameFromContext(resourceContext)) + " DROP COLUMN "
                                + getQuoted(def.columnName));
            }

            report.setStatus(ConfigurationUpdateStatus.SUCCESS);
        } catch (SQLException e) {
            report.setErrorMessageFromThrowable(e);
            report.setStatus(ConfigurationUpdateStatus.FAILURE);
        }
    }

    public Connection getConnection() {
        return this.resourceContext.getParentResourceComponent().getConnection();
    }

    public void removeConnection() {
        this.resourceContext.getParentResourceComponent().removeConnection();
    }

    public OperationResult invokeOperation(String name, Configuration parameters)
            throws InterruptedException, Exception {

        if ("vacuum".equals(name)) {
            Connection connection = null;
            PreparedStatement statement = null;
            try {
                connection = getPooledConnectionProvider().getPooledConnection();
                statement = connection.prepareStatement(
                        "vacuum " + getFullyQualifiedTableName(getSchemaNameFromContext(resourceContext),
                                getQuoted(getTableNameFromContext(resourceContext))));
                statement.executeUpdate();
            } finally {
                safeClose(connection, statement);
            }
        }
        return null;
    }

    static class ColumnDefinition {
        String columnName;
        String columnType;
        Integer columnLength;
        Integer columnPrecision;
        String columnDefault;
        boolean columnNullable;

        public ColumnDefinition(ResultSet rs) throws SQLException {
            columnName = rs.getString("COLUMN_NAME");
            columnType = rs.getString("TYPE_NAME");
            columnLength = rs.getInt("COLUMN_SIZE");
            columnPrecision = rs.getInt("DECIMAL_DIGITS");
            columnDefault = rs.getString("COLUMN_DEF");
            columnNullable = isNullableToBoolean(rs.getInt("NULLABLE"));
        }

        public ColumnDefinition(PropertyMap column) {
            columnName = column.getSimple("columnName").getStringValue();
            columnType = column.getSimple("columnType").getStringValue();
            columnLength = (column.getSimple("columnLength") == null) ? null
                    : column.getSimple("columnLength").getIntegerValue();
            columnPrecision = (column.getSimple("columnPrecision") == null) ? null
                    : column.getSimple("columnPrecision").getIntegerValue();
            columnDefault = (column.getSimple("columnDefault") == null) ? null
                    : column.getSimple("columnDefault").getStringValue();
            columnNullable = !(column.getSimple("columnNullable") == null
                    || column.getSimple("columnNullable").getBooleanValue() == null)
                    && column.getSimple("columnNullable").getBooleanValue().booleanValue();
        }

        public String getColumnSql() {
            StringBuilder buf = new StringBuilder();
            buf.append(getQuoted(columnName)).append(" ").append(columnType);
            if (!isArrayColumnType(columnType)) {
                if (columnLength != null) {
                    buf.append("(").append(columnLength).append(")");
                }

                if (columnPrecision != null) {
                    buf.append("(").append(columnPrecision).append(")");
                }
            }
            if (columnDefault != null) {
                buf.append(" DEFAULT ").append(columnDefault);
            }

            if (!columnNullable) {
                buf.append(" NOT NULL");
            }

            return buf.toString();
        }

        private boolean isArrayColumnType(String columnType) {
            return columnType != null && columnType.trim().endsWith("[]");
        }
    }

    private static String getSchemaNameFromContext(ResourceContext<PostgresDatabaseComponent> resourceContext) {
        return resourceContext.getPluginConfiguration().getSimpleValue("schemaName");
    }

    private static String getTableNameFromContext(ResourceContext<PostgresDatabaseComponent> resourceContext) {
        return resourceContext.getPluginConfiguration().getSimpleValue("tableName");
    }

    private static String getQuoted(String s) {
        return "\"" + s + "\"";
    }

    private static boolean isNullableToBoolean(int isNullable) {
        return isNullable == ResultSetMetaData.columnNoNulls ? false : true;
    }
}