Package com.google.enterprise.connector.persist

Source Code of com.google.enterprise.connector.persist.JdbcStore

// Copyright 2010 Google Inc.
//
// 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 com.google.enterprise.connector.persist;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.enterprise.connector.common.PropertiesException;
import com.google.enterprise.connector.common.PropertiesUtils;
import com.google.enterprise.connector.instantiator.Configuration;
import com.google.enterprise.connector.scheduler.Schedule;
import com.google.enterprise.connector.spi.DatabaseResourceBundle;
import com.google.enterprise.connector.util.database.DatabaseResourceBundleManager;
import com.google.enterprise.connector.util.database.JdbcDatabase;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sql.DataSource;

/**
* Manage persistence for schedule and state and configuration
* for a named connector. The persistent store for these data items
* are columns in a database table, accessed via JDBC.
*/
public class JdbcStore implements PersistentStore {

  private static final Logger LOGGER =
      Logger.getLogger(JdbcStore.class.getName());

  /* Property Names */
  static final String SCHEDULE = "schedule";
  static final String STATE = "checkpoint";
  static final String TYPE = "configuration_type";
  static final String MAP = "configuration_map";
  static final String XML = "configuration_xml";

  static final String RESOURCE_BUNDLE_NAME = "sql.connector-manager.JdbcStore";
  private DatabaseResourceBundle resourceBundle = null;

  // Classloader tailored to test environment.
  private ClassLoader classLoader = null;

  private JdbcDatabase database = null;

  /* Cached SQL Resources */
  private String inventoryStampsQuery;
  private String inventoryTypesQuery;
  private String getValueQuery;
  private String setValueQuery;
  private String connectorNameColumn;
  private String modifyStampColumn;
  private String propertyNameColumn;
  private String propertyValueColumn;

  private synchronized void init() {
    if (resourceBundle != null) {
      return;
    }
    if (database == null) {
      throw new IllegalStateException("Must set JdbcDatabase");
    }

    // Locate our SQL DatabaseResourceBundle.
    DatabaseResourceBundleManager mgr = new DatabaseResourceBundleManager();
    resourceBundle = mgr.getResourceBundle(RESOURCE_BUNDLE_NAME,
        database.getResourceBundleExtension(), classLoader);
    if (resourceBundle == null) {
      // TODO: PersistentStore interface methods should be able to throw PersistentStoreExceptions.
      throw new RuntimeException("Failed to load SQL ResourceBundle "
                                 + RESOURCE_BUNDLE_NAME);
   }

    // Verify that the connector instance table exists.
    String tableName = getResource("table.name");
    if (!database.verifyTableExists(tableName,
         resourceBundle.getStringArray("table.create.ddl"))) {
      // TODO: PersistentStore interface methods should be able to throw PersistentStoreExceptions.
      throw new RuntimeException("Persistent Store Table does not exist "
                                 + tableName);
    }

    // Cache some SQL resources.
    // TODO: These queries should really be PreparedStatements.
    inventoryStampsQuery = getResource("getinventory.stamps.query");
    inventoryTypesQuery = getResource("getinventory.types.query");
    getValueQuery = getResource("getvalue.query");
    setValueQuery = getResource("setvalue.query");

    connectorNameColumn = getResource("column.connector_name");
    modifyStampColumn = getResource("column.modify_stamp");
    propertyNameColumn = getResource("column.property_name");
    propertyValueColumn = getResource("column.property_value");
  }

  /**
   * Sets the JDBC {@link DataSource} used to access the
   * {@code Connectors} table.
   *
   * @param dataBase a JDBC {@link DataSource}
   */
  public void setDatabase(JdbcDatabase dataBase) {
    this.database = dataBase;
  }

  @VisibleForTesting
  public JdbcDatabase getDatabase() {
    return database;
  }

  /* Sets the ClassLoader that will be used to locate SQL Resources. */
  @VisibleForTesting
  void setResourceClassLoader(ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  /**
   * Returns the SQL resource for the supplied key.
   */
  private String getResource(String key) {
    String value = resourceBundle.getString(key);
    if (value == null) {
      LOGGER.log(Level.WARNING, "Failed to resolve SQL resource " + key);
    }
    return value;
  }

  /**
   * Returns {@code true} if the configured {@link JdbcDatabase} is unavailable.
   *
   * @return {@code true} if this PersistentStore is disabled, {@code false}
   * otherwise.
   */
  @Override
  public boolean isDisabled() {
    return (database == null) ? true : database.isDisabled();
  }

  /**
   * Gets the version stamps of all persistent objects.  Reads the entire
   * connector instance table and extracts the MODIFY_STAMPS for all peristed
   * data.
   *
   * @return an immutable map containing the version stamps; may be
   * empty but not {@code null}
   */
  @Override
  public ImmutableMap<StoreContext, ConnectorStamps> getInventory() {
    ImmutableMap.Builder<StoreContext, ConnectorStamps> mapBuilder =
        new ImmutableMap.Builder<StoreContext, ConnectorStamps>();
    try {
      init();
      Connection connection = database.getConnectionPool().getConnection();
      try {
        // TODO: We should consider using a PreparedStatement - however this
        // is non-trivial when using connection pools.  Try using
        // MapMaker.makeComputingMap() to map connections to PreparedStatements.
        Map<String, Map<String, JdbcStamp>> stampAlbum =
            new HashMap<String, Map<String, JdbcStamp>>();

        // Collect the Stamps for the various interesting properties.
        Statement statement = connection.createStatement(
            ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
        try {
          ResultSet resultSet = statement.executeQuery(inventoryStampsQuery);
          while (resultSet.next()) {
            String connectorName = resultSet.getString(connectorNameColumn);
            Map<String, JdbcStamp> stamps = stampAlbum.get(connectorName);
            if (stamps == null) {
              stamps = new HashMap<String, JdbcStamp>();
              stampAlbum.put(connectorName, stamps);
            }
            stamps.put(resultSet.getString(propertyNameColumn),
                       new JdbcStamp(resultSet.getLong(modifyStampColumn)));
          }
        } finally {
          statement.close();
        }

        // Find all connectors with non-null Type, construct a StoreContext
        // for the connector+type, and build an inventory of that connector's
        // stamps from the previous query.
        // (Connectors with no Type have been deleted.)
        statement = connection.createStatement(
            ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
        try {
          Object[] params = { quoteValue(TYPE) };
          String query = MessageFormat.format(inventoryTypesQuery, params);
          ResultSet resultSet = statement.executeQuery(query);
          while (resultSet.next()) {
            String connectorName = resultSet.getString(connectorNameColumn);
            StoreContext storeContext = new StoreContext(connectorName,
                resultSet.getString(propertyValueColumn));
            Map<String, JdbcStamp> stamps = stampAlbum.get(connectorName);
            ConnectorStamps connectorStamps;
            if (stamps == null) {
              connectorStamps = new ConnectorStamps(null, null, null);
            } else {
              JdbcStamp mapStamp = stamps.get(MAP);
              JdbcStamp xmlStamp = stamps.get(XML);
              JdbcStamp configStamp = new JdbcStamp(
                  ((mapStamp == null) ? 0L : mapStamp.version) +
                  ((xmlStamp == null) ? 0L : xmlStamp.version));
              connectorStamps = new ConnectorStamps(
                  stamps.get(STATE), configStamp, stamps.get(SCHEDULE));
            }
            mapBuilder.put(storeContext, connectorStamps);
            if (LOGGER.isLoggable(Level.FINE)) {
              LOGGER.fine("Found connector: name = " + connectorName
                          + "  type = " + storeContext.getTypeName()
                          + "  stamps = " + connectorStamps);
            }
          }
        } finally {
          statement.close();
        }
      } finally {
        database.getConnectionPool().releaseConnection(connection);
      }
    } catch (SQLException e) {
      LOGGER.log(Level.WARNING, "Failed to retrieve Connector Inventory", e);
    }
      // Finally, construct the inventory.
    return mapBuilder.build();
  }

  /**
   * A version stamp based upon the MODIFY_STAMP database field.
   */
  private static class JdbcStamp implements Stamp {
    final long version;

    /** Constructs a File version stamp. */
    JdbcStamp(long version) {
      this.version = version;
    }

    /** {@inheritDoc} */
    @Override
    public int compareTo(Stamp other) {
      return (int) (version - ((JdbcStamp) other).version);
    }

    @Override
    public String toString() {
      return Long.toString(version);
    }
  }

  /**
   * Retrieves connector schedule.
   *
   * @param context a StoreContext
   * @return connectorSchedule schedule of the corresponding connector.
   */
  @Override
  public Schedule getConnectorSchedule(StoreContext context) {
    return Schedule.of(getField(context, SCHEDULE));
  }

  /**
   * Stores connector schedule.
   *
   * @param context a StoreContext
   * @param connectorSchedule schedule of the corresponding connector.
   */
  @Override
  public void storeConnectorSchedule(StoreContext context,
      Schedule connectorSchedule) {
    String schedule = (connectorSchedule == null)
        ? null : connectorSchedule.toString();
    setField(context, SCHEDULE, schedule);
  }

  /**
   * Remove a connector schedule.
   *
   * @param context a StoreContext
   */
  @Override
  public void removeConnectorSchedule(StoreContext context) {
    storeConnectorSchedule(context, null);
  }

  /**
   * Gets the stored state of a named connector.
   *
   * @param context a StoreContext
   * @return the state, or null if no state has been stored for this connector.
   */
  @Override
  public String getConnectorState(StoreContext context) {
    return getField(context, STATE);
  }

  /**
   * Stores connector state.
   *
   * @param context a StoreContext
   * @param connectorState state of the corresponding connector
   */
  @Override
  public void storeConnectorState(StoreContext context, String connectorState) {
    setField(context, STATE, connectorState);
  }

  /**
   * Remove connector state.
   *
   * @param context a StoreContext
   */
  @Override
  public void removeConnectorState(StoreContext context) {
    storeConnectorState(context, null);
  }

  /**
   * Gets the stored configuration of a named connector.
   *
   * @param context a StoreContext
   * @return the configuration map, or null if no configuration
   *         has been stored for this connector.
   */
  @Override
  public Configuration getConnectorConfiguration(StoreContext context) {
    String config = getField(context, MAP);
    String configXml = getField(context, XML);
    String type = getField(context, TYPE);
    if (type == null && config == null && configXml == null) {
      return null;
    }
    try {
      Properties props = PropertiesUtils.loadFromString(config);
      return new Configuration(type, PropertiesUtils.toMap(props), configXml);
    } catch (PropertiesException e) {
      LOGGER.log(Level.WARNING, "Failed to read connector configuration for "
                 + context.getConnectorName(), e);
      return null;
    }
  }

  /**
   * Stores the configuration of a named connector.
   *
   * @param context a StoreContext
   * @param configuration map to store
   */
  @Override
  public void storeConnectorConfiguration(StoreContext context,
      Configuration configuration) {
    testStoreContext(context);
    String configMap = null;
    String configXml = null;
    String type = null;
    if (configuration != null) {
      Properties properties = PropertiesUtils.fromMap(configuration.getMap());
      try {
        configMap = PropertiesUtils.storeToString(properties, null);
      } catch (PropertiesException e) {
        LOGGER.log(Level.WARNING, "Failed to store connector configuration for "
                   + context.getConnectorName(), e);
        return;
      }
      configXml = configuration.getXml();
      type = configuration.getTypeName();
    }
    setField(context, TYPE, type);
    setField(context, XML, configXml);
    setField(context, MAP, configMap);
  }

  /**
   * Remove a stored connector configuration.
   *
   * @param context a StoreContext
   */
  @Override
  public void removeConnectorConfiguration(StoreContext context) {
    storeConnectorConfiguration(context, null);
  }

  /**
   * Test the StoreContext to make sure it is sane.
   *
   * @param context a StoreContext
   */
  private static void testStoreContext(StoreContext context) {
    Preconditions.checkNotNull(context, "StoreContext may not be null.");
  }

  /**
   * Quotes the supplied value using single qoutes.  MessageFormat
   * considers embedded single-quotes special, and doesn't do
   * substitutions within them.  Unfortunately, this is exactly
   * where we want to use substitutions: in SQL queries like:
   * {@code ... WHERE ( connector_name='{0}' ...}.
   * <p>
   * One solution is to add the quote characters to the value being
   * substituted in (the purpose of this method).  Another solution would
   * be to avoid MessageFormat, possibly trying PreparedStatement syntax.
   */
  // TODO: Use PreparedStatements.
  private String quoteValue(String value) {
    return "'" + value.replace("'", "''") + "'";
  }

  /**
   * Retrieve a database field value.
   *
   * @param context a StoreContext
   * @param fieldName the name of the field
   * @return String value of the field, or {@code null} if not stored
   */
  private String getField(StoreContext context, String fieldName) {
    testStoreContext(context);
    try {
      init();
      Connection connection = database.getConnectionPool().getConnection();
      try {
        Object[] params =
            { quoteValue(context.getConnectorName()), quoteValue(fieldName) };
        String query = MessageFormat.format(getValueQuery, params);
        Statement stmt = connection.createStatement();
        try {
          ResultSet rs = stmt.executeQuery(query);
          if (rs.next()) {
            return rs.getString(propertyValueColumn);
          }
        } finally {
          stmt.close();
        }
      } finally {
        database.getConnectionPool().releaseConnection(connection);
      }
    } catch (SQLException e) {
      LOGGER.log(Level.WARNING, "Failed to retrieve " + fieldName
          + " for connector " + context.getConnectorName(), e);
    }
    return null;
  }

  /**
   * Update a database field value.
   *
   * @param context a StoreContext
   * @param fieldName the name of the field
   * @param fieldValue the value of the field
   */
  private void setField(StoreContext context,
                        String fieldName, String fieldValue) {
    testStoreContext(context);
    Connection connection = null;
    boolean originalAutoCommit = true;
    try {
      init();
      connection = database.getConnectionPool().getConnection();
      try {
        originalAutoCommit = connection.getAutoCommit();
        connection.setAutoCommit(false);

        Object[] params =
            { quoteValue(context.getConnectorName()), quoteValue(fieldName) };
        String query = MessageFormat.format(setValueQuery, params);
        Statement stmt = connection.createStatement(
            ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
        try {
          ResultSet rs = stmt.executeQuery(query);
          if (rs.next()) {
            // This connector property exists, update the property value.
            if (fieldValue == null) {
              rs.updateNull(propertyValueColumn);
            } else {
              rs.updateString(propertyValueColumn, fieldValue);
            }
            // Bump the ModifyStamp, so others may know the value has changed.
            rs.updateInt(modifyStampColumn, rs.getInt(modifyStampColumn) + 1);
            rs.updateRow();
          } else {
            // This connector property does not exist, insert it with new value.
            rs.moveToInsertRow();
            rs.updateInt(modifyStampColumn, 1); // Bootstrap the ModifyStamp
            rs.updateString(connectorNameColumn, context.getConnectorName());
            rs.updateString(propertyNameColumn, fieldName);
            if (fieldValue == null) {
              rs.updateNull(propertyValueColumn);
            } else {
              rs.updateString(propertyValueColumn, fieldValue);
            }
            rs.insertRow();
          }
          connection.commit();
          rs.close();
        } finally {
          stmt.close();
        }
      } catch (SQLException e) {
        try {
          connection.rollback();
        } catch (SQLException ignored) {}
        throw e;
      } finally {
        try {
          connection.setAutoCommit(originalAutoCommit);
        } catch (SQLException ignored) {}
        database.getConnectionPool().releaseConnection(connection);
      }
    } catch (SQLException e) {
      LOGGER.log(Level.WARNING, "Failed to store " + fieldName
          + " for connector " + context.getConnectorName(), e);
    }
  }
}
TOP

Related Classes of com.google.enterprise.connector.persist.JdbcStore

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
ga('create', 'UA-20639858-1', 'auto'); ga('send', 'pageview');