/*******************************************************************************
 * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.testing.tests.dbchangenotification;

import java.util.*;

import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * This class translates a database change notification message
 * into invalidation of a corresponding object in TopLink cache.
 *
 * The class expects the invalidation notification to be found in the following
 * JMSMessage properties:
 * String property "TABLE" should contain table name;
 * Properties of appropriate type should contain value(s) of PK field(s).
 *
 * Examples based on Employee demo:
 * 1. EMPLOYEE table:
 * getStringProperty("TABLE") == "EMPLOYEE";
 * getObjectProperty("EMP_ID") == value of EMP_ID column for the modified row
 *
 * 2. SALARY table:
 * getStringProperty("TABLE") == "SALARY";
 * getObjectProperty("EMP_ID") == value of EMP_ID column for the modified row
 *
 * 3. PHONE table:
 * getStringProperty("TABLE") == "PHONE";
 * getObjectProperty("EMP_ID") == value of EMP_ID column for the modified row
 * getObjectProperty("TYPE") == value of TYPE column for the modified row
 */
public class

CacheInvalidator {
    // maps a table name to Class mapped to this table
    Hashtable tableNameToClass;
    // maps table name to a vector of primary key fields' names
    Hashtable tableNameToPkFieldNames;

    // Create a CacheInvalidator object that invalidates cache if the changed table
    // is mapped by one of the descriptors of the passed session

    public CacheInvalidator(Session session) {
        // HashSet is used to avoid duplications
        HashSet tableNames = new HashSet();

        // fill out tableNames collection with all tables' names mapped by all descriptors
        Iterator descriptors = session.getDescriptors().values().iterator();
        while (descriptors.hasNext()) {
            ClassDescriptor desc = (ClassDescriptor)descriptors.next();

            // Create a Vector containing names of all tables mapped to the descriptor
            Vector descTableNames = desc.getTableNames();
            // Remove schema names (if any) converting "SCHEMA_NAME.TABLE_NAME" to "TABLE_NAME"
            removePrefixFromDatabaseObjectNames(descTableNames);
            // add descTableNames to the collection
            tableNames.addAll(descTableNames);
        }
        // initialize
        initializeWithTableNames(session, tableNames);
    }

    // Create a CacheInvalidator object that invalidates cache if the changed table's name
    // is in tableNames collection
    // and the table is mapped by one of the descriptors of the passed session.
    // Note that the Collection tableNames will be altered - only names
    // of tables not found in descriptors of the passed session will remain.

    public CacheInvalidator(Session session, Collection tableNames) {
        // initialize
        initializeWithTableNames(session, tableNames);
    }

    protected void initializeWithTableNames(Session session, Collection tableNames) {
        tableNameToClass = new Hashtable(tableNames.size());
        tableNameToPkFieldNames = new Hashtable(tableNames.size());

        // pkFieldVectors cached here to avoid calculating it more than once per class
        Hashtable classToPkFieldNames = new Hashtable();
        // loop through the descriptors to fill out tableNameToClass and tableNameToPkFieldNames
        Iterator descriptors = session.getDescriptors().values().iterator();
        while (descriptors.hasNext() && !tableNames.isEmpty()) {
            ClassDescriptor desc = (ClassDescriptor)descriptors.next();

            // Create a Vector containing names of all tables mapped to the descriptor
            Vector descTableNames = desc.getTableNames();

            // bypass descriptors with no tables
            if (descTableNames.isEmpty()) {
                continue;
            }

            // Remove schema names (if any) converting "SCHEMA_NAME.TABLE_NAME" to "TABLE_NAME"
            removePrefixFromDatabaseObjectNames(descTableNames);

            // handle inheritance: table name should be mapped to the base mapped class
            Class baseClass = desc.getJavaClass();
            while (desc.isChildDescriptor()) {
                desc = session.getDescriptor(desc.getInheritancePolicy().getParentClass());
                baseClass = desc.getJavaClass();
            }

            Iterator it = tableNames.iterator();
            while (it.hasNext()) {
                // for each tableName specified by the user
                String tableName = (String)it.next();
                // verify whether the descriptor maps a table with the same name
                if (descTableNames.contains(tableName)) {
                    // map the table name to the baseClass corresponding to the descriptor
                    tableNameToClass.put(tableName, baseClass);

                    // try to obtain cached pkFieldNames Vector corresponding to baseClass
                    Vector pkFieldNames = (Vector)classToPkFieldNames.get(baseClass);
                    if (pkFieldNames == null) {
                        // Create a Vector containing names of all primary key fields
                        pkFieldNames = desc.getPrimaryKeyFieldNames();
                        // Remove table name converting from "TABLE_NAME.FIELD_NAME" to "FIELD_NAME"
                        removePrefixFromDatabaseObjectNames(pkFieldNames);
                        // cache pkFieldNames Vector corresponding to baseClass
                        classToPkFieldNames.put(baseClass, pkFieldNames);
                    }
                    // map the table name to the Vector of names of primary key fields.
                    tableNameToPkFieldNames.put(tableName, pkFieldNames);

                    // the table name is mapped - remove it from the list of table names to be mapped.
                    it.remove();
                }
            }
        }
    }

    // invalidates in tjhe cache the object corresponding to the massage

    public void invalidateObject(Session session, javax.jms.Message msg) throws javax.jms.JMSException {
        String tableName = msg.getStringProperty("TABLE");
        if (tableName == null) {
            return;
        }
        Class baseClass = (Class)tableNameToClass.get(tableName);
        if (baseClass == null) {
            return;
        }
        Vector pkFieldNames = (Vector)tableNameToPkFieldNames.get(tableName);
        if (pkFieldNames == null) {
            return;
        }

        // create DatabaseRecord corresponding to the message
        DatabaseRecord row = new DatabaseRecord(pkFieldNames.size());
        for (int i = 0; i < pkFieldNames.size(); i++) {
            String fieldName = (String)pkFieldNames.elementAt(i);
            Object value = msg.getObjectProperty(fieldName);
            row.put(fieldName, value);
        }

        // invalidate in TopLink cache the object corresponding to the row and the baseClass
        session.getIdentityMapAccessor().invalidateObject(row, baseClass);
    }

    // converts "SCHEMA_NAME.TABLE_NAME" to "TABLE_NAME"

    protected void removePrefixFromDatabaseObjectNames(Vector names) {
        for (int i = 0; i < names.size(); i++) {
            String qualifiedName = (String)names.elementAt(i);
            String name = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
            names.setElementAt(name, i);
        }
    }
}
