/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.util;

import com.sun.enterprise.Switch;
import com.sun.enterprise.admin.monitor.callflow.Agent;
import com.sun.enterprise.admin.monitor.callflow.EntityManagerQueryMethod;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TransactionRequiredException;

/**
 * Wrapper class for javax.persistence.Query objects returned from
 * non-transactional access of a container-managed transactional
 * EntityManager.  Proxying the Query object prevents the 
 * EntityManagerWrapper from having to keep a physical EntityManager
 * open when returning Query objects for non-transactional access.
 *
 * This results in a cleaner implementation of the non-transactional
 * EntityManager behavior and minimizes the amount of time 
 * non-transactional EntityManager objects are left open.  It is likely
 * that physical EntityManager objects will have heavy-weight resources
 * such as DB connections open even after clear() is called.  This is
 * one of the main reasons to minimize the number of open non-transactional
 * EntityManager objects held internally within injected/looked-up
 * container-managed EntityManagers.  
 *
 * The EntityManager and Query delegate objects are provided at
 * QueryWrapper creation time.  These objects must exist in order
 * for the EntityManagerWrapper to provide the correct exception
 * behavior to the application when a Query is requested.   
 * Likewise, the actual delegates must be available
 * to handle the majority of the Query API operations such as
 * performing validation on the various setter parameters.  
 * 
 * The Query/EntityManager delegates
 * are closed/discarded after each call to getSingleResult/getResultList.
 * A new Query/EntityManager delegate pair is then created lazily
 * the next time the Query delegate is needed.  The QueryWrapper
 * maintains a list of all setter operations invoked by the application.
 * These are re-applied in the same order whenever a new Query delegate
 * is created to ensure that the state of the Query delegate object matches
 * what it would have been if there wasn't any QueryWrapper.   
 * 
 */
public class QueryWrapper implements Query {

    // Holds current query/em delegates.  These are cleared out after
    // query execution to minimize potential entity manager resource leakage.
    private Query queryDelegate;
    private EntityManager entityManagerDelegate;

    // Used if new query/em delegates need to be created.
    private EntityManagerFactory entityMgrFactory;
    private Map entityMgrProperties;

    // State used to construct query delegate object itself.
    private QueryType queryType;
    private String queryString;
    private Class queryResultClass;
    private String queryResultSetMapping;

    // State used to recreate sequence of setter methods applied to the
    // QueryWrapper when a new Query delegate is created.
    private List<SetterData> setterInvocations;


    public static Query createQueryWrapper(EntityManagerFactory emf, 
                                           Map emProperties, 
                                           EntityManager emDelegate,
                                           Query queryDelegate,
                                           String ejbqlString) {

        return new QueryWrapper(emf, emProperties, emDelegate,
                                queryDelegate, QueryType.EJBQL,
                                ejbqlString, null, null);
    }

    public static Query createNamedQueryWrapper(EntityManagerFactory emf, 
                                                Map emProperties, 
                                                EntityManager emDelegate,
                                                Query queryDelegate,
                                                String name) {
        return new QueryWrapper(emf, emProperties, emDelegate,
                                queryDelegate, QueryType.NAMED,
                                name, null, null);
    }

    public static Query createNativeQueryWrapper(EntityManagerFactory emf, 
                                                 Map emProperties, 
                                                 EntityManager emDelegate,
                                                 Query queryDelegate,
                                                 String sqlString) {

        return new QueryWrapper(emf, emProperties, emDelegate,
                                queryDelegate, QueryType.NATIVE,
                                sqlString, null, null);
        
    }

    public static Query createNativeQueryWrapper(EntityManagerFactory emf, 
                                                 Map emProperties, 
                                                 EntityManager emDelegate,
                                                 Query queryDelegate,
                                                 String sqlString,
                                                 Class resultClass) {

        return new QueryWrapper(emf, emProperties, emDelegate,
                                queryDelegate, QueryType.NATIVE,
                                sqlString, resultClass, null);
        
    }

    public static Query createNativeQueryWrapper(EntityManagerFactory emf, 
                                                 Map emProperties, 
                                                 EntityManager emDelegate,
                                                 Query queryDelegate,
                                                 String sqlString,
                                                 String resultSetMapping) {

        return new QueryWrapper(emf, emProperties, emDelegate,
                                queryDelegate, QueryType.NATIVE,
                                sqlString, null, resultSetMapping);
        
    } 

    private QueryWrapper(EntityManagerFactory emf, Map emProperties,
                         EntityManager emDelegate, Query qDelegate,
                         QueryType type, String query,
                         Class resultClass, String resultSetMapping)
    {
        entityMgrFactory = emf;
        entityMgrProperties = emProperties;

        entityManagerDelegate = emDelegate;
        queryDelegate = qDelegate;

        queryType = type;
        queryString = query;
        queryResultClass = resultClass;
        queryResultSetMapping = resultSetMapping;

        setterInvocations = new LinkedList<SetterData>();
    }

    public List getResultList() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.GET_RESULT_LIST);
            }
            Query delegate = getQueryDelegate();
            return delegate.getResultList();
        } finally {
            clearDelegates();
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
    }

    public Object getSingleResult() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.GET_SINGLE_RESULT);
            }
            Query delegate = getQueryDelegate();
            return delegate.getSingleResult();

        } finally {
            clearDelegates();
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
    }

    public int executeUpdate() {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        if(callFlowAgent.isEnabled()) {
            callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.EXECUTE_UPDATE);
            callFlowAgent.entityManagerQueryEnd();
        }
        throw new TransactionRequiredException("executeUpdate is not supported for a Query object obtained through non-transactional access of a container-managed transactional EntityManager");
    }

    public Query setMaxResults(int maxResults) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_MAX_RESULTS);
            }
            if( maxResults < 0 ) {
                throw new IllegalArgumentException("maxResult cannot be negative");
            }
            
            Query delegate = getQueryDelegate();
            delegate.setMaxResults(maxResults);
            
            SetterData setterData = SetterData.createMaxResults(maxResults);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setFirstResult(int startPosition) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_FIRST_RESULT);
            }
            if( startPosition < 0 ) {
                throw new IllegalArgumentException
                        ("startPosition cannot be negative");
            }
            
            Query delegate = getQueryDelegate();
            delegate.setFirstResult(startPosition);
            
            SetterData setterData = SetterData.createFirstResult(startPosition);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setHint(String hintName, Object value) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_HINT);
            }
            Query delegate = getQueryDelegate();
            delegate.setHint(hintName, value);
            
            SetterData setterData = SetterData.createHint(hintName, value);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setParameter(String name, Object value) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_STRING_OBJECT);
            }
            Query delegate = getQueryDelegate();
            delegate.setParameter(name, value);
            
            SetterData setterData = SetterData.createParameter(name, value);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setParameter(String name, Date value, 
                              TemporalType temporalType) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_STRING_DATE_TEMPORAL_TYPE);
            }
            Query delegate = getQueryDelegate();
            delegate.setParameter(name, value, temporalType);
            
            SetterData setterData = SetterData.createParameter(name, value,
                    temporalType);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        return this;
    }

    public Query setParameter(String name, Calendar value, 
                              TemporalType temporalType) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_STRING_CALENDAR_TEMPORAL_TYPE);
            }
            Query delegate = getQueryDelegate();
            delegate.setParameter(name, value, temporalType);
            
            SetterData setterData = SetterData.createParameter(name, value,
                    temporalType);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        return this;
    }

    public Query setParameter(int position, Object value) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_INT_OBJECT);
            }
        Query delegate = getQueryDelegate();
        delegate.setParameter(position, value);

        SetterData setterData = SetterData.createParameter(position, value);
        setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }

        return this;
    }

    public Query setParameter(int position, Date value, 
                              TemporalType temporalType) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_INT_DATE_TEMPORAL_TYPE);
            }
            Query delegate = getQueryDelegate();
            delegate.setParameter(position, value, temporalType);
            
            SetterData setterData = SetterData.createParameter(position, value,
                    temporalType);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setParameter(int position, Calendar value, 
                              TemporalType temporalType) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_PARAMETER_INT_CALENDAR_TEMPORAL_TYPE);
            }
            Query delegate = getQueryDelegate();
            delegate.setParameter(position, value, temporalType);
            
            SetterData setterData = SetterData.createParameter(position, value,
                    temporalType);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    public Query setFlushMode(FlushModeType flushMode) {
        Agent callFlowAgent = Switch.getSwitch().getCallFlowAgent();
        
        try {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryStart(EntityManagerQueryMethod.SET_FLUSH_MODE);
            }
            Query delegate = getQueryDelegate();
            delegate.setFlushMode(flushMode);
            
            SetterData setterData = SetterData.createFlushMode(flushMode);
            setterInvocations.add(setterData);
        } finally {
            if(callFlowAgent.isEnabled()) {
                callFlowAgent.entityManagerQueryEnd();
            }
        }
        
        return this;
    }

    private void clearDelegates() {

        queryDelegate = null;

        if( entityManagerDelegate != null ) {
            entityManagerDelegate.close();
            entityManagerDelegate = null;
        }
        
    }

    private Query getQueryDelegate() {

        if( queryDelegate == null ) {

            entityManagerDelegate = 
                entityMgrFactory.createEntityManager(entityMgrProperties);

            switch(queryType) {

              case EJBQL :

                  queryDelegate = 
                      entityManagerDelegate.createQuery(queryString);
                  break;
                  
              case NAMED :

                  queryDelegate = 
                      entityManagerDelegate.createNamedQuery(queryString);
                  break;

              case NATIVE :

                  if( queryResultClass != null ) {

                      queryDelegate = entityManagerDelegate.createNativeQuery
                          (queryString, queryResultClass);

                  } else if( queryResultSetMapping != null ) {

                      queryDelegate = entityManagerDelegate.createNativeQuery
                          (queryString, queryResultSetMapping);

                  } else {

                      queryDelegate = entityManagerDelegate.createNativeQuery
                          (queryString);
                  }

                  break;
            }

            // Now recreate the sequence of valid setter invocations applied 
            // to this query.
            for(SetterData setterData : setterInvocations) {
                setterData.apply(queryDelegate);
            }

        }


        return queryDelegate;

    }

    private enum QueryType {

        EJBQL,
        NAMED,
        NATIVE

    }

    private enum SetterType {

        MAX_RESULTS,
        FIRST_RESULT,
        HINT,
        PARAM_NAME_OBJECT,
        PARAM_NAME_DATE_TEMPORAL,
        PARAM_NAME_CAL_TEMPORAL,
        PARAM_POSITION_OBJECT,
        PARAM_POSITION_DATE_TEMPORAL,
        PARAM_POSITION_CAL_TEMPORAL,
        FLUSH_MODE

    }

    private static class SetterData {

        SetterType type;

        int int1;
        String string1;
        Object object1;

        Date date;
        Calendar calendar;
        TemporalType temporalType;

        FlushModeType flushMode;

        static SetterData createMaxResults(int maxResults) {
            SetterData data = new SetterData();
            data.type = SetterType.MAX_RESULTS;
            data.int1 = maxResults;
            return data;
        }

        static SetterData createFirstResult(int firstResult) {
            SetterData data = new SetterData();
            data.type = SetterType.FIRST_RESULT;
            data.int1 = firstResult;
            return data;
        }

        static SetterData createHint(String hintName, Object value) {
            SetterData data = new SetterData();
            data.type = SetterType.HINT;
            data.string1 = hintName;
            data.object1 = value;
            return data;
        }

        static SetterData createParameter(String name, Object value) {

            SetterData data = new SetterData();
            data.type = SetterType.PARAM_NAME_OBJECT;
            data.string1 = name;
            data.object1 = value;
            return data;
        }

        static SetterData createParameter(String name, Date value,
                                          TemporalType temporalType) {

            SetterData data = new SetterData();
            data.type = SetterType.PARAM_NAME_DATE_TEMPORAL;
            data.string1 = name;
            data.date = value;
            data.temporalType = temporalType;
            return data;
        }

        static SetterData createParameter(String name, Calendar value,
                                          TemporalType temporalType) {

            SetterData data = new SetterData();
            data.type = SetterType.PARAM_NAME_CAL_TEMPORAL;
            data.string1 = name;
            data.calendar = value;
            data.temporalType = temporalType;
            return data;
        }
        
        static SetterData createParameter(int position, Object value) {

            SetterData data = new SetterData();
            data.type = SetterType.PARAM_POSITION_OBJECT;
            data.int1 = position;
            data.object1 = value;
            return data;
        }

        static SetterData createParameter(int position, Date value, 
                                          TemporalType temporalType) {
            SetterData data = new SetterData();
            data.type = SetterType.PARAM_POSITION_DATE_TEMPORAL;
            data.int1 = position;
            data.date = value;
            data.temporalType = temporalType;
            return data;
        }

       static SetterData createParameter(int position, Calendar value, 
                                          TemporalType temporalType) {
            SetterData data = new SetterData();
            data.type = SetterType.PARAM_POSITION_CAL_TEMPORAL;
            data.int1 = position;
            data.calendar = value;
            data.temporalType = temporalType;
            return data;
        } 
        
        static SetterData createFlushMode(FlushModeType flushMode) {

            SetterData data = new SetterData();
            data.type = SetterType.FLUSH_MODE;
            data.flushMode = flushMode;
            return data;

        }
        
        void apply(Query query) {

            switch(type) {

            case MAX_RESULTS :
                
                query.setMaxResults(int1);
                break;

            case FIRST_RESULT :
                
                query.setFirstResult(int1);
                break;

            case HINT :
                
                query.setHint(string1, object1);
                break;

            case PARAM_NAME_OBJECT :
                
                query.setParameter(string1, object1);
                break;

            case PARAM_NAME_DATE_TEMPORAL :
                
                query.setParameter(string1, date, temporalType);
                break;

            case PARAM_NAME_CAL_TEMPORAL :
                
                query.setParameter(string1, calendar, temporalType);
                break;

            case PARAM_POSITION_OBJECT :
                
                query.setParameter(int1, object1);
                break;

            case PARAM_POSITION_DATE_TEMPORAL :
                
                query.setParameter(int1, date, temporalType);
                break;

            case PARAM_POSITION_CAL_TEMPORAL :
                
                query.setParameter(int1, calendar, temporalType);
                break;

            case FLUSH_MODE :
                
                query.setFlushMode(flushMode);
                break;

            }
        }
        
    }
    

}
