/*
 * Copyright (c) 2012, 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:
//     02/08/2012-2.4 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     08/24/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     02/06/2013-2.5 Guy Pelletier
//       - 382503: Use of @ConstructorResult with createNativeQuery(sqlString, resultSetMapping) results in NullPointerException
package org.eclipse.persistence.queries;

import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;
import org.eclipse.persistence.internal.security.PrivilegedGetConstructorFor;
import org.eclipse.persistence.internal.security.PrivilegedInvokeConstructor;
import org.eclipse.persistence.sessions.DatabaseRecord;

/**
 * <p><b>Purpose</b>:
 * Concrete class to represent the ConstructorResult structure as defined by
 * the JPA 2.1 Persistence specification.
 *
 * @author Guy Pelletier
 * @since EclipseLink 2.4
 */
public class ConstructorResult extends SQLResult {
    /** Stores the class of result  */
    protected String targetClassName;
    protected transient Class targetClass;

    /** Stored the column results of this constructor result */
    protected List<ColumnResult> columnResults;

    protected transient Constructor constructor;
    protected Class[] constructorArgTypes;

    /**
     * Default constructor is protected. Users must initialize the constructor
     * result with a target class.
     */
    protected ConstructorResult() {
        columnResults = new ArrayList<ColumnResult>();
    }

    /**
     * Constructor accepting target class.
     */
    public ConstructorResult(Class targetClass){
        this();

        if (targetClass == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("null_value_for_constructor_result"));
        }

        this.targetClass = targetClass;
        this.targetClassName = targetClass.getName();
    }

    /**
     * Constructor accepting target class name.
     */
    public ConstructorResult(String targetClassName){
        this();

        if (targetClassName == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("null_value_for_constructor_result"));
        }

        this.targetClassName = targetClassName;
    }

    /**
     * Add a column result to this constructor result.
     */
    public void addColumnResult(ColumnResult columnResult) {
        columnResults.add(columnResult);
    }

    /**
     * INTERNAL:
     * Convert all the class-name-based settings in this query to actual class-based
     * settings. This method is used when converting a project that has been built
     * with class names to a project with classes.
     * @param classLoader
     */
    @Override
    public void convertClassNamesToClasses(ClassLoader classLoader){
        super.convertClassNamesToClasses(classLoader);

        // Make sure the column results have been converted.
        for (ColumnResult columnResult : columnResults) {
            columnResult.convertClassNamesToClasses(classLoader);
        }

        //no need to get the class if we already have it
        if (targetClass == null && targetClassName!=null) {
            try{
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                    try {
                        targetClass = AccessController.doPrivileged(new PrivilegedClassForName(targetClassName, true, classLoader));
                    } catch (PrivilegedActionException exception) {
                        throw ValidationException.classNotFoundWhileConvertingClassNames(targetClassName, exception.getException());
                    }
                } else {
                    targetClass = org.eclipse.persistence.internal.security.PrivilegedAccessHelper.getClassForName(targetClassName, true, classLoader);
                }
            } catch (ClassNotFoundException exc){
                throw ValidationException.classNotFoundWhileConvertingClassNames(targetClassName, exc);
            }
        }
    }

    /**
     * Return the columns result of this constructor result.
     */
    public List<ColumnResult> getColumnResults() {
        return columnResults;
    }

    /**
     * INTERNAL:
     * This method is a convenience method for extracting values from results/
     */
    @Override
    public Object getValueFromRecord(DatabaseRecord record, ResultSetMappingQuery query) {
        if (constructor == null) {
            initialize(record, query);
        }

        int columnResultsSize = getColumnResults().size();
        Object[] constructorArgs = new Object[columnResultsSize];

        for (int i = 0; i < columnResultsSize; i++) {
            constructorArgs[i] = ConversionManager.getDefaultManager().convertObject(record.get(getColumnResults().get(i).getColumn()), constructorArgTypes[i]);
        }

        try {
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                try {
                    return AccessController.doPrivileged(new PrivilegedInvokeConstructor(constructor, constructorArgs));
                } catch (PrivilegedActionException exception) {
                    throw QueryException.exceptionWhileInitializingConstructor(exception.getException(), query, targetClass);
                }
            } else {
                return PrivilegedAccessHelper.invokeConstructor(constructor, constructorArgs);
            }
        } catch (IllegalAccessException exception) {
            throw QueryException.exceptionWhileInitializingConstructor(exception, query, targetClass);
        } catch (java.lang.reflect.InvocationTargetException exception) {
            throw QueryException.exceptionWhileInitializingConstructor(exception, query, targetClass);
        } catch (InstantiationException exception) {
            throw QueryException.exceptionWhileInitializingConstructor(exception, query, targetClass);
        }
    }

    /**
     * INTERNAL:
     */
    protected void initialize(DatabaseRecord record, ResultSetMappingQuery query) {
        int columnResultsSize = getColumnResults().size();
        constructorArgTypes = new Class[columnResultsSize];

        for (int i = 0; i < columnResultsSize; i++) {
            ColumnResult result = getColumnResults().get(i);
            DatabaseField resultField = result.getColumn();

            if (resultField.getType() == null) {
                Object recordResultField = record.get(resultField);

                if (recordResultField == null) {
                    throw QueryException.columnResultNotFound(resultField);
                } else {
                    constructorArgTypes[i] = recordResultField.getClass();
                }
            } else {
                constructorArgTypes[i] = resultField.getType();
            }
        }

        try {
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                try {
                    constructor = AccessController.doPrivileged(new PrivilegedGetConstructorFor(targetClass, constructorArgTypes, true));
                } catch (PrivilegedActionException exception) {
                    throw QueryException.exceptionWhileInitializingConstructor(exception.getException(), query, targetClass);
                }
            } else {
                constructor = PrivilegedAccessHelper.getConstructorFor(targetClass, constructorArgTypes, true);
            }
        } catch (NoSuchMethodException exception) {
            throw QueryException.exceptionWhileInitializingConstructor(exception, query, targetClass);
        }
    }

    /**
     * Return true if this is a constructor result.
     */
    @Override
    public boolean isConstructorResult(){
        return true;
    }

    /**
     * Set columns result of this constructor result.
     */
    public void setColumnResults(List<ColumnResult> columnResults) {
        this.columnResults = columnResults;
    }
}
