All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arakelian.jdbc.store.JdbcStore Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.arakelian.jdbc.store;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.arakelian.jdbc.conn.ConnectionFactory;
import com.arakelian.jdbc.conn.IgnoreCloseConnection;
import com.arakelian.jdbc.conn.SingletonConnectionFactory;
import com.arakelian.jdbc.handler.ResultSetHandler;
import com.arakelian.jdbc.model.Index;
import com.arakelian.jdbc.store.strategy.JdbcStrategy;
import com.arakelian.jdbc.utils.DatabaseUtils;
import com.arakelian.jdbc.utils.DatabaseUtils.JdbcIterator;
import com.arakelian.store.AbstractMutableStore;
import com.arakelian.store.IndexedStore;
import com.arakelian.store.StoreException;
import com.arakelian.store.feature.HasId;
import com.google.common.base.Preconditions;

public class JdbcStore extends AbstractMutableStore implements IndexedStore {
    /**
     * Converts the row into a Java bean.
     */
    private class JsonHandler implements ResultSetHandler {
        @Override
        public T handle(final ResultSet rs, final ResultSetMetaData rsmd) throws SQLException {
            if (rs.next()) {
                return config.getStrategy().get(rs);
            }
            return null;
        }

        @Override
        public boolean wasLast(final T result) {
            return result == null;
        }
    }

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

    public static final String ID = "id";

    public static final String ALL = "_all";

    private static final String DELETE_BY_ID = "delete_id";

    protected final JdbcStoreConfig config;

    protected final Map secondaryIndexes;

    protected List indexes;

    public JdbcStore(final JdbcStoreConfig config) {
        super(config);
        this.config = config;

        // predefined: queries for selecting and deleting a single record by id
        this.secondaryIndexes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        this.secondaryIndexes.put(ALL, config.getSelectSql());
        this.secondaryIndexes.put(ID, config.getSelectSql() + " where id=?");
        this.secondaryIndexes.put(DELETE_BY_ID, config.getDeleteSql() + " where id=?");

        // fetch list of indexes and create named query for each one
        this.indexes = addIndexes(config.getConnectionFactory());
    }

    /**
     * Returns the name of a new secondary index, which is generated by concatenating the field
     * names that are in the index with double underscores, and converting the result to lower case.
     *
     * @param fields
     *            list of field names that are in the secondary index
     * @return name of secondary index
     */
    public String addSecondaryIndex(final String... fields) {
        if (fields == null || fields.length == 0) {
            return null;
        }

        // name will resemble: "first__second__third"
        final StringBuilder name = new StringBuilder();

        // where will resemble: "first=? and second=? and third=?"
        final StringBuilder where = new StringBuilder();

        for (int i = 0, size = fields.length; i < size; i++) {
            // force field names to use LOWER_UNDERSCORE case format
            final String field = fields[i].toLowerCase().replace(' ', '_');
            name.append(i != 0 ? "__" : "").append(field);
            where.append(i != 0 ? " and " : "").append(field).append("=?");
        }

        // add named query
        final String indexName = name.toString();
        addSecondaryIndex(indexName, where.toString());
        return indexName;
    }

    /**
     * Adds a secondary index that consists of the given where clause.
     *
     * @param name
     *            secondary index name
     * @param whereClause
     *            where clause, without the WHERE keyword, like this: "first=? and second=? and
     *            third=?"
     */
    public void addSecondaryIndex(final String name, final String whereClause) {
        Preconditions.checkArgument(!StringUtils.isEmpty(name), "name must be non-empty");
        Preconditions.checkArgument(!StringUtils.isEmpty(whereClause), "whereClause must be non-empty");
        secondaryIndexes.put(name, config.getSelectSql() + " where " + whereClause);
    }

    @Override
    public void deleteBy(final String indexName, final Object... args) {
        final String sql = getNamedQuery(indexName, args);
        try {
            DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, args);
        } catch (final SQLException e) {
            throw new StoreException(e);
        }
    }

    @Override
    public T get(final String id) {
        return getBy(ID, id);
    }

    @Override
    public List getAllBy(final String indexName, final Object... args) {
        List beans = null;
        try (JdbcIterator it = iterateBy(indexName, args)) {
            while (it.hasNext()) {
                if (beans == null) {
                    beans = new ArrayList<>();
                }
                beans.add(it.next());
            }
        }

        if (beans == null) {
            return Collections. emptyList();
        }
        return beans;
    }

    @Override
    public T getBy(final String indexName, final Object... args) {
        final String sql = getNamedQuery(indexName, args);

        try {
            final T value = DatabaseUtils.executeQuery( //
                    config.getConnectionFactory(), //
                    sql,
                    new JsonHandler(),
                    args);
            return value;
        } catch (final SQLException e) {
            throw new StoreException(e);
        }
    }

    public final JdbcStoreConfig getConfig() {
        return config;
    }

    public final List getIndexes() {
        return indexes;
    }

    public JdbcIterator iterateBy(final String indexName, final Object... args) {
        return DatabaseUtils.iterator( //
                config.getConnectionFactory(), //
                getNamedQuery(indexName, args), //
                new JsonHandler(), //
                args);
    }

    /**
     * Refreshes the list of indexes that are in the database using our internal connection factory.
     */
    public void refreshIndexes() {
        this.indexes = addIndexes(config.getConnectionFactory());
    }

    /**
     * Refresh the list of indexes that are in the database using the provided connection. This is
     * useful when our connection factory may not yet see the changes available inside the
     * connection provided, due to caching or transactionality.
     *
     * @param connection
     *            connect to use for fetching JDBC metadata.
     */
    public void refreshIndexes(final Connection connection) {
        this.indexes = addIndexes(new SingletonConnectionFactory(new IgnoreCloseConnection<>(connection)));
    }

    /**
     * Fetches a list of indexes from the database using a JDBC metadata query, and creates name
     * queries for each of them.
     *
     * @param connectionFactory
     *            connection factory
     * @return list of indexes from the database
     */
    private List addIndexes(final ConnectionFactory connectionFactory) {
        LOGGER.info("Fetching list of indexes for table {}", config.getTable());

        final List indexes;
        try {
            indexes = DatabaseUtils.getIndexes(connectionFactory, null, config.getTable(), false, false);
        } catch (final SQLException e) {
            throw new StoreException("Unable to fetch list of indexes from table " + config.getTable(), e);
        }

        for (final Index index : indexes) {
            final String[] fields = index.getFieldNames();
            LOGGER.info("Table \"{}\" has index for {}", config.getTable(), fields);
            addSecondaryIndex(fields);
        }
        return indexes;
    }

    private final int countParameters(final String sql) {
        if (StringUtils.isEmpty(sql)) {
            return 0;
        }
        int count = 0;
        for (int i = 0, size = sql.length(); i < size; i++) {
            if (sql.charAt(i) == '?') {
                count++;
            }
        }
        return count;
    }

    private void doDeleteAll(final List idsOrValues) {
        final Object[] ids = idsOf(idsOrValues);
        if (ids == null || ids.length == 0) {
            return;
        }

        try {
            final String sql = secondaryIndexes
                    .get(appendInClause(config.getDeleteSql(), ids.length, DELETE_BY_ID));
            DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, ids);
        } catch (final SQLException e) {
            throw new StoreException("Unable to delete from table " + config.getTable(), e);
        }
    }

    private String getNamedQuery(final String name, final Object... args) {
        // fetch sql associated with given name
        if (!secondaryIndexes.containsKey(name)) {
            throw new StoreException(
                    "Named query (\"" + name + "\") does not exist for table " + config.getTable());
        }

        // verify we have been provided correct number of arguments
        final String sql = secondaryIndexes.get(name);
        final int numParams = countParameters(sql);
        if (numParams != args.length) {
            throw new StoreException("Named query (\"" + name + "\") with sql \"" + sql + "\" requires "
                    + numParams + " arguments: " + sql);
        }
        return sql;
    }

    protected String appendInClause(final String baseSql, final int paramCount, final String namePrefix) {
        if (paramCount < 1 || paramCount > config.getPartitionSize()) {
            throw new StoreException("paramCount " + paramCount + " is invalid; must be be between 1 and "
                    + config.getPartitionSize());
        }

        final String name = namePrefix + "_" + paramCount;
        if (secondaryIndexes.containsKey(name)) {
            // we have already build the select list of that size
            return name;
        }

        // build select list
        final StringBuilder buf = new StringBuilder(
                baseSql.length() + " where id in (".length() + 2 * paramCount - 1);
        buf.append(baseSql);
        buf.append(" where id in (");
        for (int i = 0; i < paramCount; i++) {
            if (i != 0) {
                buf.append(",");
            }
            buf.append("?");
        }
        buf.append(")");
        final String sql = buf.toString();

        // store for future use
        secondaryIndexes.put(name, sql);
        return name;
    }

    @Override
    protected void doDelete(final String id) {
        try {
            final String sql = secondaryIndexes.get(DELETE_BY_ID);
            DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, id);
        } catch (final SQLException e) {
            throw new StoreException("Unable to delete " + id + " from table " + config.getTable(), e);
        }
    }

    @Override
    protected void doDeleteAllIds(final List ids) {
        doDeleteAll(ids);
    }

    @Override
    protected void doDeleteAllValues(final List values) {
        doDeleteAll(values);
    }

    @Override
    protected List doGetAll(List result, final List partition) {
        final String indexName = appendInClause(config.getSelectSql(), partition.size(), ID);
        final Object[] partitionIds = partition.toArray();
        try (JdbcIterator it = iterateBy(indexName, partitionIds)) {
            while (it.hasNext()) {
                if (result == null) {
                    result = new ArrayList<>();
                }
                result.add(it.next());
            }
        }
        return result;
    }

    @Override
    protected void doPut(final T value) {
        try {
            final JdbcStrategy strategy = config.getStrategy();
            final ConnectionFactory connectionFactory = config.getConnectionFactory();
            strategy.put(connectionFactory, config.getInsertSql(), value);
        } catch (final SQLException e) {
            final String id = value.getId();
            throw new StoreException("Unable to save " + id + " to table " + config.getTable(), e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy