/*
 * Copyright (c) 1998, 2018 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 v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.descriptors;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
import org.eclipse.persistence.queries.ObjectLevelModifyQuery;

/**
 * <p>
 * <b>Purpose</b>: An implementation of the OptimisticLockingPolicy interface.
 * This policy compares selected fields in the WHERE clause when doing an update
 * or a delete. If any field has been changed, an optimistic locking exception
 * will be thrown. Note that the fields specified must be mapped and not be
 * primary keys.
 * <p>
 * NOTE: This policy can only be used inside a unit of work.
 *
 * @since TopLink 2.5
 */
public class SelectedFieldsLockingPolicy extends FieldsLockingPolicy {
    protected Map<DatabaseTable, List<DatabaseField>> lockFieldsByTable;
    protected List<DatabaseField> lockFields;

    /**
     * PUBLIC: Create a new selected fields locking policy. A field locking
     * policy is based on locking on the specified fields by comparing with
     * their previous values to detect field-level collisions. Note: the unit of
     * work must be used for all updates when using field locking.
     */
    public SelectedFieldsLockingPolicy() {
        super();
        this.lockFieldsByTable = new HashMap(4);
        this.lockFields = new ArrayList();
    }

    /**
     * PUBLIC: Add a field name to lock on. All fields in this list will be
     * compared when updating if the value of any of the fields does not match
     * the value in memory, an OptimisticLockException will be thrown.
     *
     * Note: An Automatic update will not be done on this field, only a
     * comparison occurs.
     */
    public void addLockFieldName(String fieldName) {
        getLockFields().add(new DatabaseField(fieldName));
    }

    /**
     * INTERNAL: Values to be included in the locking mechanism are added to the
     * translation row. For changed fields the normal build row is ok as only
     * changed fields matter.
     */
    @Override
    public void addLockValuesToTranslationRow(ObjectLevelModifyQuery query) throws DatabaseException {
        Object object;
        verifyUsage(query.getSession());
        if (query.isDeleteObjectQuery()) {
            object = query.getObject();
        } else {
            object = query.getBackupClone();
        }

        // EL bug 319759
        if (query.isUpdateObjectQuery()) {
            query.setShouldValidateUpdateCallCacheUse(true);
        }

        for (Iterator<List<DatabaseField>> fields = getLockFieldsByTable().values().iterator(); fields.hasNext();) {
            for (DatabaseField field : fields.next()) {
                DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForField(field);
                // Bug5892889, Exception will be thrown if no matched database
                // field found
                if (mapping == null) {
                    throw DatabaseException.specifiedLockingFieldsNotFoundInDatabase(field.getQualifiedName());
                } else {
                    mapping.writeFromObjectIntoRow(object, query.getTranslationRow(), query.getSession(), WriteType.UNDEFINED);
                }
            }
        }
    }

    /**
     * INTERNAL: returns the lock fields to compare based on the passed in
     * table.
     */
    @Override
    protected List<DatabaseField> getFieldsToCompare(org.eclipse.persistence.internal.helper.DatabaseTable table, AbstractRecord transRow, AbstractRecord modifyRow) {
        return getLockFields(table);
    }

    /**
     * INTERNAL: Returns the lock fields
     */
    public List<DatabaseField> getLockFields() {
        return lockFields;
    }

    /**
     * INTERNAL: returns the lock fields based on the passed in table
     */
    protected List<DatabaseField> getLockFields(DatabaseTable table) {
        List<DatabaseField> temp = this.lockFieldsByTable.get(table);
        if (temp == null) {
            return Collections.EMPTY_LIST;
        }
        return temp;
    }

    /**
     * INTERNAL: returns the lock fields
     */
    protected Map<DatabaseTable, List<DatabaseField>> getLockFieldsByTable() {
        return lockFieldsByTable;
    }

    /**
     * INTERNAL: It is responsible for initializing the policy;
     */
    @Override
    public void initialize(AbstractSession session) {
        super.initialize(session);
        List<DatabaseField> lockFields = getLockFields();
        int size = lockFields.size();
        for (int index = 0; index < size; index++) {
            DatabaseField field = lockFields.get(index);
            field = descriptor.buildField(field);
            lockFields.set(index, field);
            List<DatabaseField> fieldsForTable = getLockFieldsByTable().get(field.getTable());
            if (fieldsForTable == null) {
                fieldsForTable = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance();
                getLockFieldsByTable().put(field.getTable(), fieldsForTable);
            }
            fieldsForTable.add(field);
        }
    }

    /**
     * PUBLIC: Set the field names to lock on. All fields in this list will be
     * compared when Updating. If the value of any of the fields does not match
     * the value in memory, an OptimisticLockException will be thrown.
     *
     * Note: An Automatic update will not be done on this field, only a
     * comparison occurs.
     */
    public void setLockFieldNames(List<String> lockFieldNames) {
        for (String name : lockFieldNames) {
            addLockFieldName(name);
        }
    }

    /**
     * INTERNAL: Sets the lock fields
     */
    protected void setLockFields(List<DatabaseField> lockFields) {
        this.lockFields = lockFields;
    }

    /**
     * INTERNAL: Used to set the field names to be used in this policy.
     */
    protected void setLockFieldsByTable(Map<DatabaseTable, List<DatabaseField>> lockFieldsByTable) {
        this.lockFieldsByTable = lockFieldsByTable;
    }
}
