/*
 * 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 org.apache.jackrabbit.core.persistence.pool;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;

import org.apache.jackrabbit.core.util.StringIndex;
import org.apache.jackrabbit.core.util.db.ConnectionHelper;
import org.apache.jackrabbit.core.util.db.DbUtility;

/**
 * Implements a {@link StringIndex} that stores and retrieves the names from a
 * table in a database.
 * <p/>
 * Note that this class is not threadsafe by itself. it needs to be synchronized
 * by the using application.
 * <p/>
 * Due to a bug with oracle that treats empty strings a null values
 * (see JCR-815), all empty strings are replaced by a ' '. since names never
 * start with a space, this it not problematic yet.
 */
public class DbNameIndex implements StringIndex {

    protected final ConnectionHelper conHelper;

    // name index statements
    protected String nameSelectSQL;
    protected String indexSelectSQL;
    protected String nameInsertSQL;

    // caches
    private final HashMap<String, Integer> string2Index = new HashMap<String, Integer>();
    private final HashMap<Integer, String> index2String = new HashMap<Integer, String>();

    /**
     * Creates a new index that is stored in a db.
     * @param conHelper the {@link ConnectionHelper}
     * @param schemaObjectPrefix the prefix for table names
     * @throws SQLException if the statements cannot be prepared.
     */
    public DbNameIndex(ConnectionHelper conHlpr, String schemaObjectPrefix)
            throws SQLException {
        conHelper = conHlpr;
        init(schemaObjectPrefix);
    }

    /**
     * Inits this index and prepares the statements.
     *
     * @param con the jdbc connection
     * @param schemaObjectPrefix the prefix for table names
     * @throws SQLException if the statements cannot be prepared.
     */
    protected void init(String schemaObjectPrefix)
            throws SQLException {
        nameSelectSQL = "select NAME from " + schemaObjectPrefix + "NAMES where ID = ?";
        indexSelectSQL = "select ID from " + schemaObjectPrefix + "NAMES where NAME = ?";
        nameInsertSQL = "insert into " + schemaObjectPrefix + "NAMES (NAME) values (?)";
    }

    /**
     * Closes this index and releases it's resources.
     */
    public void close() {
        // closing the database resources is done by the owning
        // BundleDbPersistenceManager that created this index
    }

    /**
     * {@inheritDoc}
     */
    public int stringToIndex(String string) {
        // check cache
        Integer index = string2Index.get(string);
        if (index == null) {
            String dbString = string.length() == 0 ? " " : string;
            int idx = getIndex(dbString);
            if (idx == -1) {
                idx = insertString(dbString);
            }
            index = Integer.valueOf(idx);
            string2Index.put(string, index);
            index2String.put(index, string);
            return idx;
        } else {
            return index.intValue();
        }
    }

    /**
     * {@inheritDoc}
     */
    public String indexToString(int idx) throws IllegalArgumentException {
        // check cache
        Integer index = Integer.valueOf(idx);
        String s = index2String.get(index);
        if (s == null) {
            s = getString(idx);
            if (s.equals(" ")) {
                s = "";
            }
            index2String.put(index, s);
            string2Index.put(s, index);
        }
        return s;
    }

    /**
     * Inserts a string into the database and returns the new index.
     *
     * @param string the string to insert
     * @return the new index.
     */
    protected int insertString(String string) {
        // assert index does not exist
        int result = -1;
        ResultSet rs = null;
        try {
            rs = conHelper.exec(nameInsertSQL, new Object[] { string }, true, 0);
            if (rs.next()) {
                result = rs.getInt(1);
            }
        } catch (Exception e) {
            IllegalStateException ise = new IllegalStateException(
                    "Unable to insert index for string: " + string);
            ise.initCause(e);
            throw ise;
        } finally {
            DbUtility.close(rs);
        }
        if (result != -1) {
            return result;
        } else {
            // Could not get the index with getGeneratedKeys, try with SELECT
            return getIndex(string);
        }
    }

    /**
     * Retrieves the index from the database for the given string.
     * @param string the string to retrieve the index for
     * @return the index or -1 if not found.
     */
    protected int getIndex(String string) {
        ResultSet rs = null;
        try {
            rs = conHelper.exec(indexSelectSQL, new Object[] { string }, false, 0);
            if (rs.next()) {
                return rs.getInt(1);
            } else {
                return -1;
            }
        } catch (Exception e) {
            IllegalStateException ise = new IllegalStateException(
                    "Unable to read index for string: " + string);
            ise.initCause(e);
            throw ise;
        } finally {
            DbUtility.close(rs);
        }
    }

    /**
     * Retrieves the string from the database for the given index.
     * @param index the index to retrieve the string for.
     * @return the string
     * @throws IllegalArgumentException if the string is not found
     */
    protected String getString(int index)
            throws IllegalArgumentException, IllegalStateException {
        String result = null;
        ResultSet rs = null;
        try {
            rs = conHelper.exec(nameSelectSQL, new Object[] { Integer.valueOf(index) }, false, 0);
            if (rs.next()) {
                result = rs.getString(1);
            }
        } catch (Exception e) {
            IllegalStateException ise = new IllegalStateException(
                    "Unable to read name for index: " + index);
            ise.initCause(e);
            throw ise;
        } finally {
            DbUtility.close(rs);
        }
        if (result == null) {
            throw new IllegalArgumentException("Index not found: " + index);
        }
        return result;
    }

}
