/*
 * 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.queries;

import java.util.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.*;

/**
 * PUBLIC:
 * A Query Class used to perform a bulk update using TopLink's expression framework.
 * This class is provided to help optimize performance. It can be used in place
 * of reading in all the objects to be changed and issuing single updates per
 * instance. With this approach a single SQL UPDATE statement can be issued and
 * then, based on the Expression provided, any objects in the cache that are
 * effected by the update can be invalidated.
 * <p>
 * Notes: <ul>
 * <li>By default, if a UOW is being used, this query will be deferred until the UOW commits.
 * <li>UpdateAllQuery does not support foreign key updates
 * unless the relationship is 1-1 (without back pointers.)</ul>
 * <p>
 * <b>Example of Usage:</b> Adding an area code. <br>
 * <code>
 * UpdateAllQuery updateQuery = new UpdateAllQuery(Employee.class);   <br>
 * updateQuery.setSelectionCriteria(eb.get("areaCode").isNull());     <br>
 * updateQuery.addUpdate(eb.get("areaCode"), "613");                  <br>
 * </code>
 *
 * @author Guy Pelletier
 * @since  March 1, 2004
 */
public class UpdateAllQuery extends ModifyAllQuery {

    protected HashMap m_updateClauses;

    /**
     * PUBLIC:
     * Constructs a default update all query.
     */
    public UpdateAllQuery() {
        super();
    }

    /**
     * PUBLIC:
     * Constructs an update all query for the Class type specified.
     * @param referenceClass Class
     */
    public UpdateAllQuery(Class referenceClass) {
        super(referenceClass);
    }

    /**
     * PUBLIC:
     * Constructs an update all query for the specified Class type and selection criteria.
     * @param referenceClass Class type to be considered
     * @param selectionCriteria Expression
     */
    public UpdateAllQuery(Class referenceClass, Expression selectionCriteria) {
        super(referenceClass, selectionCriteria);
    }

    /**
     * PUBLIC:
     * Constructs an update all query for the Class type specified and the given
     * ExpressionBuilder. This sets the default builder which is used for all associated
     * expressions in the query.<br>
     * @param referenceClass Class type to be considered
     * @param expressionBuilder ExpressionBuilder
     */
    public UpdateAllQuery(Class referenceClass, ExpressionBuilder expressionBuilder) {
        super(referenceClass);
        this.defaultBuilder = expressionBuilder;
    }

    /**
     * PUBLIC:
     * Adds the update (SET) clause to the query. Uses default ExpressionBuilder.
     * @param field Expression Object level representation of a database query 'where' clause
     * @param value Object, the new value
     */
    public void addUpdate(Expression field, Object value) {
        addUpdateInternal(field, value);
    }

    /**
     * PUBLIC:
     * Adds the update (SET) clause to the query. Uses default ExpressionBuilder.
     * @param attributeName String, the name of the attribute
     * @param value Object, the new value
     */
    public void addUpdate(String attributeName, Object value) {
        addUpdateInternal(attributeName, value);
    }

    /**
     * PUBLIC:
     * Adds the update (SET) clause to the query. This method ensures that
     * the builder has the session and reference class set for both given Expressions.
     * Uses default ExpressionBuilder.
     * @param field Expression, representation of a database query 'where' clause that describes the field
     * @param value Expression, representation of a database query 'where' clause that describes the new value
     */
    public void addUpdate(Expression field, Expression value) {
        addUpdateInternal(field, value);
    }

    /**
     * PUBLIC:
     * Adds the update (SET) clause to the query. Uses default ExpressionBuilder.
     * @param attributeName String, the name of the attribute
     * @param value Expression, the new value
     */
    public void addUpdate(String attributeName, Expression value) {
        addUpdateInternal(attributeName, value);
    }

    /**
     * INTERNAL:
     */
    protected void addUpdateInternal(Object fieldObject, Object valueObject) {
        if(fieldObject == null) {
            throw QueryException.updateAllQueryAddUpdateFieldIsNull(this);
        }
        if(m_updateClauses == null) {
            m_updateClauses = new HashMap();
        }
        m_updateClauses.put(fieldObject, valueObject);
    }

    /**
     * INTERNAL:
     * Issue the SQL to the database and then merge into the cache.
     * If we are within a UoW, the merge to the cache must not be done until
     * the UoW merges into the parent. The UoW will trigger the merge to occur
     * at the correct time and will ensure the cache setting is set to none at
     * that time.
     */
    @Override
    public Object executeDatabaseQuery() throws DatabaseException {
        result = getQueryMechanism().updateAll();// fire the SQL to the database
        mergeChangesIntoSharedCache();
        return result;
    }

    /**
     * INTERNAL:
     * Returns the specific default redirector for this query type.  There are numerous default query redirectors.
     * See ClassDescriptor for their types.
     */
    @Override
    protected QueryRedirector getDefaultRedirector(){
        return descriptor.getDefaultUpdateObjectQueryRedirector();
    }

    /**
     * INTERNAL:
     * Return the updates stored for an update all query
     */
    public HashMap getUpdateClauses() {
        return m_updateClauses;
    }

    /**
     * INTERNAL:
     * Return true if this is an update all query.
     */
    @Override
    public boolean isUpdateAllQuery() {
        return true;
    }

    /**
     * INTERNAL:
     */
    @Override
    protected void prepare() throws QueryException {
        super.prepare();// will tell the query mechanism to prepare itself as well.

        Class referenceClass = getReferenceClass();

        // Check the reference class, must be set
        if (referenceClass == null) {
            throw QueryException.referenceClassMissing(this);
        }

        // Check the descriptor is set, if not try to get it from the session
        if (getDescriptor() == null) {
            ClassDescriptor desc = getSession().getDescriptor(referenceClass);

            if (desc == null) {
                throw QueryException.descriptorIsMissing(referenceClass, this);
            }

            setDescriptor(desc);
        }

        ClassDescriptor descriptor = getDescriptor();

        // Check the descriptor for an aggregate
        if (descriptor.isAggregateDescriptor()) {
            throw QueryException.aggregateObjectCannotBeDeletedOrWritten(descriptor, this);
        }

        // Check that the update statement is set. That is the bare minimum,
        // the user can execute this query without a selection criteria though.
        if ((getUpdateClauses() == null || getUpdateClauses().isEmpty()) && isExpressionQuery()) {
            throw QueryException.updateStatementsNotSpecified();
        }

        getQueryMechanism().prepareUpdateAll();
    }
    /**
     * INTERNAL:
     * Initialize the expression builder which should be used for this query. If
     * there is a where clause, use its expression builder.
     * If after this method defaultBuilder is still null,
     * then initializeDefaultBuilder method will generate and cache it.
     */
    @Override
    protected void initializeQuerySpecificDefaultBuilder() {
        super.initializeQuerySpecificDefaultBuilder();
        if(this.defaultBuilder == null && m_updateClauses != null) {
            Iterator it = m_updateClauses.values().iterator();
            while(it.hasNext() ) {
                Object value = it.next();
                if(value != null) {
                    if(value instanceof Expression) {
                        Expression valueExpression = (Expression)value;
                        this.defaultBuilder = valueExpression.getBuilder();
                        if(this.defaultBuilder != null) {
                            return;
                        }
                    }
                }
            }
            it = m_updateClauses.keySet().iterator();
            while(it.hasNext() ) {
                Object field = it.next();
                if(field instanceof Expression) {
                    Expression fieldExpression = (Expression)field;
                    this.defaultBuilder = fieldExpression.getBuilder();
                    if(this.defaultBuilder != null) {
                        return;
                    }
                }
            }
        }
    }
}

