/*******************************************************************************
 * Copyright (c) 1998, 2013 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
 *     08/01/2012-2.5 Chris Delahunt
 *       - 371950: Metadata caching 
 *     08/24/2012-2.5 Guy Pelletier 
 *       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
 *     08/11/2012-2.5 Guy Pelletier  
 *       - 393867: Named queries do not work when using EM level Table Per Tenant Multitenancy.
 ******************************************************************************/  
package org.eclipse.persistence.internal.jpa;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.EclipseLinkException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.sessions.AbstractSession;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;

import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.EntityResult;
import org.eclipse.persistence.queries.SQLResultSetMapping;
import org.eclipse.persistence.queries.StoredProcedureCall;
import org.eclipse.persistence.sessions.Session;

/**
 * <b>Purpose</b>: 
 * A JPA placeholder Query object to store JPQL strings so that processing the string is delayed 
 * until Login.<p>
 *
 * @author Chris Delahunt
 * @since TopLink Essentials
 */
public class JPAQuery extends DatabaseQuery  {
    private String lockMode;
    private String jpqlString;
    private String sqlString;
    private StoredProcedureCall call;
    private String resultClassName;
    private List<String> resultClassNames;
    private List<String> resultSetMappingNames;
    private Map<String, Object> hints;
    
    public JPAQuery() {
    }
    
    public JPAQuery(String jpqlString) {
        this.jpqlString=jpqlString;
    }
    
    /**
     * JPQL 
     * @param name
     * @param jpqlString
     * @param lockMode
     * @param hints
     */
    public JPAQuery(String name, String jpqlString, String lockMode, Map<String, Object> hints) {
        this.name = name;
        this.jpqlString = jpqlString;
        this.flushOnExecute = null;
        this.hints = hints;
        this.lockMode = lockMode;
        if (lockMode == null) {
            this.lockMode = "NONE";
        }
    }
    
    /*
     * SQL returning an entity
     */
    public JPAQuery(String queryName, String sqlString, Map<String, Object> hints) {
        this.name = queryName; 
        this.sqlString = sqlString;
        this.flushOnExecute = null;
        this.hints = hints;
        this.lockMode = null;
    }
    
    /*
     * Stored Proc returning an Entity
     */
    public JPAQuery(String queryName, StoredProcedureCall call, Map<String, Object> hints) {
        this.name = queryName;
        this.call = call;
        this.flushOnExecute = null;
        this.hints = hints;
        this.lockMode = null;
    }
    
    public void addResultClassNames(String className) {
        if (resultClassNames == null) {
            resultClassNames = new ArrayList<String>();
        }
        this.resultClassNames.add(className);
    }
    
    public void addResultSetMapping(String resultSetMapping){
        if (resultSetMappingNames == null) {
            resultSetMappingNames = new ArrayList<String>();
        }
        this.resultSetMappingNames.add(resultSetMapping);
    }
    
    /**
     * INTERNAL:
     * This should never be called and is only here because it is needed as an extension
     * to DatabaseQuery.  Perhaps exception should be thrown to warn users, but for now
     * it will execute the resulting query instead, this allows JPA style queries to be executed
     * on a normal EclipseLink Session.
     */
    public Object executeDatabaseQuery() throws DatabaseException, OptimisticLockException{
        return getSession().executeQuery(getDatabaseQuery());
    }
    
    public DatabaseQuery getDatabaseQuery() {
        return (DatabaseQuery)getProperty("databasequery");
    }
    
    /**
     * INTERNAL:
     * For table per tenant queries the descriptor list will extracted from
     * parsing the jpql query and cached here.
     */
    @Override
    public List<ClassDescriptor> getDescriptors() {
        return descriptors;
    }
    
    /**
     * Return the JPA query hints.
     */
    public Map<String, Object> getHints(){
        return hints;
    }
    
    /**
     * Return the JPQL string.
     */
    public String getJPQLString(){
        return jpqlString;
    }
    
    /**
     * Return true if this query is a jpql query.
     */
    public boolean isJPQLQuery() {
        return jpqlString != null;
    }
    
    /**
     * Return true if this query is an sql query.
     */
    public boolean isSQLQuery() {
        return sqlString != null;
    }
    
    /**
     * INTERNAL:
     * Generate the DatabaseQuery query from the JPA named query.
     */
    public void prepare() {
        DatabaseQuery query = null;
        ClassLoader loader = session.getDatasourcePlatform().getConversionManager().getLoader();
        
        if (isSQLQuery()) {
            query = processSQLQuery(getSession());
        } else if (isJPQLQuery()) {
            query = processJPQLQuery(getSession());
        } else if (call != null) {
            query = processStoredProcedureQuery(getSession());
            if (call.hasParameters() ) {
                //convert the type in the parameters;  query.convertClassNamesToClasses does not cascade to the call
                for (Object value: call.getParameters()) {
                    if (value instanceof Object[]) {
                        //must be inout type, and the out portion is a DatabaseField
                        ((DatabaseField) ((Object[])value)[1]).convertClassNamesToClasses(loader);
                        value =  ((Object[])value)[0];
                    } 
                    if (value instanceof DatabaseField) {
                        ((DatabaseField) value).convertClassNamesToClasses(loader);
                    }
                }
            }
        }
        
        // Make sure all class names have been converted.
        query.convertClassNamesToClasses(loader);
        
        setDatabaseQuery(query);
    }
    
    /**
     * INTERNAL:
     * Convert the JPA query into a DatabaseQuery.
     */
    public DatabaseQuery processJPQLQuery(Session session){
        ClassLoader classloader = session.getDatasourcePlatform().getConversionManager().getLoader();
        LockModeType lockModeEnum = null;
        
        // Must handle errors if a JPA 2.0 option is used in JPA 1.0.
        try {
            lockModeEnum = LockModeType.valueOf(lockMode);
        } catch (Exception ignore) {
            // Ignore JPA 2.0 in JPA 1.0, reverts to no lock.
        }
        
        DatabaseQuery ejbquery = EJBQueryImpl.buildEJBQLDatabaseQuery(getName(), jpqlString, (AbstractSession) session, lockModeEnum, hints, classloader);
        ejbquery.setName(getName());
        
        return ejbquery;
    }
    
    /**
     * INTERNAL:
     * Convert the SQL string into a DatabaseQuery.
     */
    public DatabaseQuery processSQLQuery(Session session){
        DatabaseQuery query = null;
        ClassLoader loader = session.getDatasourcePlatform().getConversionManager().getLoader();
        
        if (resultClassName != null) {
            Class clazz = session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(resultClassName);
            query = EJBQueryImpl.buildSQLDatabaseQuery(clazz, sqlString, hints, loader, (AbstractSession)session);
        } else if (resultSetMappingNames != null) {
            query = EJBQueryImpl.buildSQLDatabaseQuery(resultSetMappingNames.get(0), sqlString, hints, loader, (AbstractSession)session);
        } else {
            // Neither a resultClass or resultSetMapping is specified so place in a temp query on the session
            query = EJBQueryImpl.buildSQLDatabaseQuery(sqlString, hints, loader, (AbstractSession)session);  
        }
        
        query.setName(this.getName());
        return query;
    }
    
    /**
     * INTERNAL:
     * Convert the StoredProc call into a DatabaseQuery.
     */
    public DatabaseQuery processStoredProcedureQuery(Session session){
        DatabaseQuery query = null;
        ClassLoader loader = session.getDatasourcePlatform().getConversionManager().getLoader();
        
        if (resultClassNames != null) {
            List<SQLResultSetMapping> resultSetMappings = new ArrayList<SQLResultSetMapping>();
            
            for (String resultClass : resultClassNames) {
                SQLResultSetMapping mapping = new SQLResultSetMapping(resultClass);
                
                EntityResult entityResult = new EntityResult(resultClass);
                mapping.addResult(entityResult);

                resultSetMappings.add(mapping);
            }
            query = StoredProcedureQueryImpl.buildResultSetMappingQuery(resultSetMappings, call, hints, loader, (AbstractSession)session);
        } else if (resultSetMappingNames != null) {
            query = StoredProcedureQueryImpl.buildResultSetMappingNameQuery(resultSetMappingNames, call, hints, loader, (AbstractSession)session);
        } else if (resultClassName != null) {
            Class clazz = session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(resultClassName);
            query = StoredProcedureQueryImpl.buildStoredProcedureQuery(clazz, call, hints, loader, (AbstractSession)session);
        } else {
            // Neither a resultClass or resultSetMapping is specified so place in a temp query on the session.
            if (call.isStoredFunctionCall() || call.isStoredPLSQLProcedureCall()) {
                // If it is a function (plsql or not) or plsql procedure use the data read query.
                query = StoredProcedureQueryImpl.buildStoredProcedureQuery(call, hints, loader, (AbstractSession)session);
            } else {
                // Otherwise use a result set mapping query for stored procedure calls so users can use the execute
                // method on it (JPA 2.1 API). Will return the same result, that is, Object[] in this case.
                query = StoredProcedureQueryImpl.buildResultSetMappingQuery(new ArrayList<SQLResultSetMapping>(), call, hints, loader, (AbstractSession)session);
            }
        }
        query.setName(getName());
        
        return query;
    }
    
    public void setDatabaseQuery(DatabaseQuery databaseQuery) {
        setProperty("databasequery", databaseQuery);
    }
    
    /**
     * INTERNAL:
     * For table per tenant queries the descriptor list will extracted from
     * parsing the jpql query and cached here.
     */
    public void setDescriptors(List<ClassDescriptor> descriptors) {
        this.descriptors = descriptors;
    }
    
    public void setHints(Map<String, Object> hints){
        this.hints = hints;
    }
    
    public void setJPQLString(String jpqlString){
        this.jpqlString = jpqlString;
    }
    
    public void setResultClassName(String className){
        this.resultClassName = className;
    }
    
    public void setResultSetMappings(List<String> resultSetMappings){
        this.resultSetMappingNames = resultSetMappings;
    }
}
