/*
 * 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.internal.helper.*;
import org.eclipse.persistence.config.ResultType;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.queries.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.converters.Converter;

/**
 * <p><b>Purpose</b>:
 * Concrete class to perform read using raw SQL.
 *
 * <p><b>Responsibilities</b>:
 * Execute a selecting raw SQL string.
 * This returns a Collection of the Records representing the result set.
 *
 * @author Yvon Lavoie
 * @since TOPLink/Java 1.0
 */
public class DataReadQuery extends ReadQuery {
    protected ContainerPolicy containerPolicy;

    /**
     * Allow return type to be configured, MAP, ARRAY, VALUE, ATTRIBUTE (MAP is the default, i.e. DatabaseRecord).
     */
    protected int resultType;

    /** A Map (DatabaseRecord) is returned for each row. */
    public static final int MAP = 0;
    /** An Object[] of values is returned for each row. */
    public static final int ARRAY = 1;
    /** A single value is returned. */
    public static final int VALUE = 2;
    /** A single value is returned for each row. */
    public static final int ATTRIBUTE = 3;
    /** Auto, a single value if a single field is selected, otherwise an Object[] (JPA default). */
    public static final int AUTO = 4;

    /**
     * PUBLIC:
     * Initialize the state of the query.
     */
    public DataReadQuery() {
        super();
        this.shouldMaintainCache = false;
        this.resultType = MAP;
        setContainerPolicy(ContainerPolicy.buildDefaultPolicy());
    }

    /**
     * PUBLIC:
     * Initialize the query to use the specified SQL string.
     * Warning: Allowing an unverified SQL string to be passed into this
     * method makes your application vulnerable to SQL injection attacks.
     */
    public DataReadQuery(String sqlString) {
        this();
        setSQLString(sqlString);
    }

    /**
     * PUBLIC:
     * Initialize the query to use the specified call.
     */
    public DataReadQuery(Call call) {
        this();
        setCall(call);
    }

    /**
     * INTERNAL:
     * <P> This method is called by the object builder when building an original.
     * It will cause the original to be cached in the query results if the query
     * is set to do so.
     */
    @Override
    public void cacheResult(Object results) {
        setTemporaryCachedQueryResults(results);
    }

    /**
     * INTERNAL:
     * Clone the query.
     */
    @Override
    public Object clone() {
        DataReadQuery cloneQuery = (DataReadQuery)super.clone();
        cloneQuery.containerPolicy = this.containerPolicy.clone(cloneQuery);
        return cloneQuery;
    }

    /**
     * INTERNAL:
     * Execute the query. If there are cached results return those.
     * This must override the super to support result caching.
     *
     * @param session - the session in which the receiver will be executed.
     * @return An object or collection, the result of executing the query.
     * @exception DatabaseException - an error has occurred on the database
     */
    @Override
    public Object execute(AbstractSession session, AbstractRecord row) throws DatabaseException {
        if (shouldCacheQueryResults()) {
            if (this.containerPolicy.overridesRead()) {
                throw QueryException.cannotCacheCursorResultsOnQuery(this);
            }
            if (this.isPrepared) {// only prepared queries can have cached results.
                Object results = getQueryResults(session, row, true);
                // Bug6138532 - if results are "cached no results", return null or an empty collection.
                if (results == InvalidObject.instance) {
                    if (this.resultType == VALUE) {
                        return null;
                    } else {
                        return this.containerPolicy.containerInstance(0);
                    }
                }
                if (results != null) {
                    return results;
                }
            }
        }
        return super.execute(session, row);
    }

    /**
     * INTERNAL:
     * Execute the query.
     * Perform the work to execute the SQL string.
     * @exception DatabaseException an error has occurred on the database
     * @return a collection or cursor of Records representing the result set
     */
    @Override
    public Object executeDatabaseQuery() throws DatabaseException {
        if (getContainerPolicy().overridesRead()) {
            return getContainerPolicy().execute();
        }
        return executeNonCursor();
    }


    /**
     * INTERNAL:
     * Conversion not supported.
     */
    public Converter getValueConverter() {
        return null;
    }


    /**
     * INTERNAL:
     * Build the result value for the row.
     */
    @Override
    public Object buildObject(AbstractRecord row) {
        if (this.resultType == AUTO) {
            List values = row.getValues();
            if (values.size() == 1) {
                return row.getValues().get(0);
            } else {
                return row.getValues().toArray();
            }
        } else if (this.resultType == ARRAY) {
            return row.getValues().toArray();
        } else if (this.resultType == ATTRIBUTE) {
            // Use get with field for XML records.
            Object value = row.get(row.getFields().get(0));
            if (getValueConverter() != null) {
                value = getValueConverter().convertDataValueToObjectValue(value, this.session);
            }
            return value;
        }
        return row;
    }

    /**
     * INTERNAL:
     * The results are *not* in a cursor, build the collection.
     * Cache the results in temporaryCachedQueryResults.
     */
    protected Object executeNonCursor() throws DatabaseException {
        Vector rows = getQueryMechanism().executeSelect();
        Object results = null;
        if (this.resultType == VALUE) {
            if (!rows.isEmpty()) {
                AbstractRecord record = (AbstractRecord)rows.get(0);
                // Use get with field for XML records.
                results = record.get(record.getFields().get(0));
                if (getValueConverter() != null) {
                    results = getValueConverter().convertDataValueToObjectValue(results, this.session);
                }
            }
        } else {
            int size = rows.size();
            ContainerPolicy containerPolicy = getContainerPolicy();
            results = containerPolicy.containerInstance(size);
            if(containerPolicy.shouldAddAll()) {
                if(size > 0) {
                    List values = new ArrayList(size);
                    for (int index = 0;  index < size; index++) {
                        AbstractRecord row = (AbstractRecord)rows.get(index);
                        Object value = buildObject(row);
                        values.add(value);
                    }
                    containerPolicy.addAll(values, results, this.session, rows, this, null, true);
                }
            } else {
                for (int index = 0;  index < size; index++) {
                    AbstractRecord row = (AbstractRecord)rows.get(index);
                    Object value = buildObject(row);
                    containerPolicy.addInto(value, results, this.session, row, this, null, true);
                }
            }
        }
        // Bug 6135563 - cache DataReadQuery results verbatim, as ObjectBuilder is not invoked
        cacheResult(results);
        return results;
    }

    /**
     * PUBLIC:
     * Return the query's ContainerPolicy.
     */
    public ContainerPolicy getContainerPolicy() {
        return containerPolicy;
    }

    /**
     * PUBLIC:
     * Return if this is a data read query.
     */
    @Override
    public boolean isDataReadQuery() {
        return true;
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    @Override
    protected void prepare() {
        super.prepare();
        this.containerPolicy.prepare(this, this.session);
        if (this.containerPolicy.overridesRead()) {
            return;
        }
        getQueryMechanism().prepareExecuteSelect();
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    @Override
    public void prepareForExecution() throws QueryException {
        super.prepareForExecution();
        this.containerPolicy.prepareForExecution();
    }

    /**
     * INTERNAL:
     * Used by RemoteSession.
     */
    @Override
    public Object remoteExecute() {
        if (getContainerPolicy().overridesRead()) {
            return getContainerPolicy().remoteExecute();
        }
        return super.remoteExecute();
    }

    /**
     * PUBLIC:
     * Set the container policy.
     */
    public void setContainerPolicy(ContainerPolicy containerPolicy) {
        // Fix for BUG 3337003 - TopLink OX will try to set this to null if
        // it is not set in the deployment XML. So don't allow it to do that.
        if (containerPolicy == null) {
            return;
        }

        this.containerPolicy = containerPolicy;
    }

    /**
     * PUBLIC:
     * Configure the query to use an instance of the specified container class
     * to hold the target objects.
     * The container class must implement (directly or indirectly) the Collection interface.
     */
    public void useCollectionClass(Class concreteClass) {
        setContainerPolicy(ContainerPolicy.buildPolicyFor(concreteClass));
    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * The initial read size is 10 and page size is 5.
     */
    public void useCursoredStream() {
        useCursoredStream(10, 5);
    }

    /**
     * Return the result type to be configured, MAP, ARRAY, VALUE, ATTRIBUTE (MAP is the default, DatabaseRecord).
     * @see ResultType
     */
    public int getResultType() {
        return resultType;
    }

    /**
     * Set the result type to be configured, MAP, ARRAY, VALUE, ATTRIBUTE (MAP is the default, DatabaseRecord).
     */
    public void setResultType(int resultType) {
        this.resultType = resultType;
    }

    /**
     * Set the result type to be configured, Map, Array, Value, Attribute (Map is the default, DatabaseRecord).
     * @see ResultType
     */
    public void setResultType(String resultType) {
        if (ResultType.Map.equals(resultType)) {
            this.resultType = MAP;
        } else if (ResultType.Array.equals(resultType)) {
            this.resultType = ARRAY;
        } else if (ResultType.Value.equals(resultType)) {
            this.resultType = VALUE;
        } else if (ResultType.Attribute.equals(resultType)) {
            this.resultType = ATTRIBUTE;
        }
    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * @param initialReadSize the initial number of objects to read
     * @param pageSize the number of objects to read when more objects
     * are needed from the database
     */
    public void useCursoredStream(int initialReadSize, int pageSize) {
        setContainerPolicy(new CursoredStreamPolicy(this, initialReadSize, pageSize));
    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * @param initialReadSize the initial number of objects to read
     * @param pageSize the number of objects to read when more objects
     * are needed from the database
     * @param sizeQuery a query that will return the size of the result set;
     * this must be set if an expression is not used (i.e. custom SQL)
     */
    public void useCursoredStream(int initialReadSize, int pageSize, ValueReadQuery sizeQuery) {
        setContainerPolicy(new CursoredStreamPolicy(this, initialReadSize, pageSize, sizeQuery));
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     */
    public void useScrollableCursor() {
        useScrollableCursor(10);
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     * @param pageSize the number of elements to be read into a the cursor
     * when more elements are needed from the database.
     */
    public void useScrollableCursor(int pageSize) {
        setContainerPolicy(new ScrollableCursorPolicy(this, pageSize));
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     * @param policy the scrollable cursor policy allows for additional result set options.
     * Example:<p>
     * ScrollableCursorPolicy policy = new ScrollableCursorPolicy()<p>
     * policy.setResultSetType(ScrollableCursorPolicy.TYPE_SCROLL_INSENSITIVE);<p>
     * query.useScrollableCursor(policy);
     */
    public void useScrollableCursor(ScrollableCursorPolicy policy) {
        policy.setQuery(this);
        setContainerPolicy(policy);
    }
}
