/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.persistence;

import java.lang.reflect.InvocationTargetException;

import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.MixedLockLevels;
import org.apache.openjpa.util.ExceptionInfo;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.LockException;
import org.apache.openjpa.util.NoTransactionException;
import org.apache.openjpa.util.ObjectExistsException;
import org.apache.openjpa.util.ObjectNotFoundException;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.OptimisticException;
import org.apache.openjpa.util.QueryException;
import org.apache.openjpa.util.RuntimeExceptionTranslator;
import org.apache.openjpa.util.StoreException;
import org.apache.openjpa.util.UserException;

/**
 * Converts from OpenJPA to persistence exception types.
 *
 * @author Abe White
 * @author Marc Prud'hommeaux
 */
public class PersistenceExceptions
    extends Exceptions {

    public static final RuntimeExceptionTranslator TRANSLATOR =
        new RuntimeExceptionTranslator() {
            
            private static final long serialVersionUID = 1L;

            @Override
            public RuntimeException translate(RuntimeException re) {
                return PersistenceExceptions.toPersistenceException(re);
            }
        };

    /**
     * Returns a {@link RuntimeExceptionTranslator} that will perform
     * the correct exception translation as well as roll back the
     * current transaction when for all but {@link NoResultException}
     * and {@link NonUniqueResultException} in accordance with
     * section 3.7 of the EJB 3.0 specification.
     */
    public static RuntimeExceptionTranslator getRollbackTranslator(
        final OpenJPAEntityManager em) {
        return new RuntimeExceptionTranslator() {
            
            private static final long serialVersionUID = 1L;
            private boolean throwing = false;

            @Override
            public RuntimeException translate(RuntimeException re) {
                RuntimeException ex = toPersistenceException(re);
                if (!(ex instanceof NonUniqueResultException)
                    && !(ex instanceof NoResultException)
                    && !(ex instanceof LockTimeoutException)
                    && !(ex instanceof QueryTimeoutException)
                    && !throwing) {
                    try {
                        throwing = true;
                        if (em.isOpen() && ((EntityManagerImpl) em).isActive())
                            ((EntityManagerImpl) em).setRollbackOnly(ex);
                    } finally {
                        // handle re-entrancy
                        throwing = false;
                    }
                }

                return ex;
            }
        };
    }

    /**
     * Convert the given throwable to the proper persistence exception.
     */
    public static RuntimeException toPersistenceException(Throwable t) {
        return (RuntimeException) translateException(t, true);
    }

    /**
     * Translate the given exception.
     *
     * @param checked whether to translate checked exceptions
     */
    private static Throwable translateException(Throwable t, boolean checked) {
        if (isPersistenceException(t))
            return t;

        // immediately throw errors
        if (t instanceof Error)
            throw (Error) t;

        OpenJPAException ke;
        if (!(t instanceof OpenJPAException)) {
            if (!checked || t instanceof RuntimeException)
                return t;
            ke = new org.apache.openjpa.util.GeneralException(t.getMessage());
            ke.setStackTrace(t.getStackTrace());
            return ke;
        }

        // if only nested exception is a persistence one, return it directly
        ke = (OpenJPAException) t;
        if (ke.getNestedThrowables().length == 1
            && isPersistenceException(ke.getCause()))
            return ke.getCause();

        // RuntimeExceptions thrown from callbacks should be thrown directly
        if (ke.getType() == ExceptionInfo.USER
            && ke.getSubtype() == UserException.CALLBACK
            && ke.getNestedThrowables().length == 1) {
            Throwable e = ke.getCause();
            if (e instanceof InvocationTargetException)
                e = e.getCause();

            if (e instanceof RuntimeException)
                return e;
        }

        // perform intelligent translation of openjpa exceptions
        switch (ke.getType()) {
            case ExceptionInfo.STORE:
                return translateStoreException(ke);
            case ExceptionInfo.USER:
                return translateUserException(ke);
            case ExceptionInfo.WRAPPED:
                return translateWrappedException(ke);
            default:
                return translateGeneralException(ke);
        }
    }

    /**
     * Translate the given store exception.
     */
    private static Throwable translateStoreException(OpenJPAException ke) {
        Exception e;
        int subtype = ke.getSubtype();
        String msg  = ke.getMessage();
        Throwable[] nested = getNestedThrowables(ke);
        Object failed = getFailedObject(ke);
        boolean fatal = ke.isFatal();
        Throwable cause = (ke.getNestedThrowables() != null
                        && ke.getNestedThrowables().length == 1)
                         ? ke.getNestedThrowables()[0] : null;
        if (subtype == StoreException.OBJECT_NOT_FOUND || cause instanceof ObjectNotFoundException) {
                e = new org.apache.openjpa.persistence.EntityNotFoundException(msg, nested, failed, fatal);
        } else if (subtype == StoreException.OPTIMISTIC	|| cause instanceof OptimisticException) {
            	e = new org.apache.openjpa.persistence.OptimisticLockException(msg, nested, failed, fatal);
        } else if (subtype == StoreException.LOCK || cause instanceof LockException) {
            LockException lockEx = (LockException) (ke instanceof LockException ? ke : cause);
            if (lockEx != null && lockEx.getLockLevel() >= MixedLockLevels.LOCK_PESSIMISTIC_READ) {
                if (!lockEx.isFatal()) {
                    e = new org.apache.openjpa.persistence.LockTimeoutException(msg, nested, failed);
                } else {
                    e = new org.apache.openjpa.persistence.PessimisticLockException(msg, nested, failed);
                }
            } else {
                e = new org.apache.openjpa.persistence.OptimisticLockException(msg, nested, failed, fatal);
            }
        } else if (subtype == StoreException.OBJECT_EXISTS || cause instanceof ObjectExistsException) {
                e = new org.apache.openjpa.persistence.EntityExistsException(msg, nested, failed, fatal);
        } else if (subtype == StoreException.QUERY || cause instanceof QueryException) {
            QueryException queryEx = (QueryException) (ke instanceof QueryException ? ke : cause);
            if (!queryEx.isFatal()) {
                e = new org.apache.openjpa.persistence.QueryTimeoutException(msg, nested, failed, false);
            } else {
                e = new org.apache.openjpa.persistence.PersistenceException(msg, nested, failed, true);
            }
        } else {
            e = new org.apache.openjpa.persistence.PersistenceException(msg, nested, failed, fatal);
        }
        e.setStackTrace(ke.getStackTrace());
        return e;
    }

    /**
     * Translate the given user exception.
     * If a {link {@link OpenJPAException#getSubtype() sub type} is set on the
     * given exception then a corresponding facade-level exception i.e. the
     * exceptions that inherit JPA-defined exceptions is generated.
     * If given exception is not further classified to a sub type, then
     * an [@link {@link #translateInternalException(OpenJPAException)} attempt}
     * is made to translate the given OpenJPAException by its internal cause.
     */
    private static Exception translateUserException(OpenJPAException ke) {
        Exception e;
        switch (ke.getSubtype()) {
            case UserException.NO_TRANSACTION:
                e = new
                    org.apache.openjpa.persistence.TransactionRequiredException
                        (ke.getMessage(), getNestedThrowables(ke),
                            getFailedObject(ke), ke.isFatal());
                break;
            case UserException.NO_RESULT:
                e = new org.apache.openjpa.persistence.NoResultException
                    (ke.getMessage(), getNestedThrowables(ke),
                        getFailedObject(ke), ke.isFatal());
                break;
            case UserException.NON_UNIQUE_RESULT:
                e = new org.apache.openjpa.persistence.NonUniqueResultException
                    (ke.getMessage(), getNestedThrowables(ke),
                        getFailedObject(ke), ke.isFatal());
                break;
            case UserException.INVALID_STATE:
                e = new org.apache.openjpa.persistence.InvalidStateException
                    (ke.getMessage(), getNestedThrowables(ke),
                        getFailedObject(ke), ke.isFatal());
                break;
            default:
            	e = translateCause(ke);
        }
        e.setStackTrace(ke.getStackTrace());
        return e;
    }

    /*
     * Translate the given wrapped exception.  If contains an Exception, return
     * the exception.  If contains a Throwable, wrap the throwable and
     * return it.
     */
    private static Exception translateWrappedException(OpenJPAException ke) {
        Throwable t = ke.getCause();
        if (t instanceof Exception)
        	return (Exception)t;
        return translateCause(ke);
    }

    /**
     * Translate to a facade-level exception if the given exception
     *     a) has a cause i.e. one and only nested Throwable
     * and b) that cause is one of the known internal exception which has a
     *        direct facade-level counterpart
     *        (for example, ObjectNotFoundException can be translated to
     *         EntityNotFoundException).
     * If the above conditions are not met then return generic
     *    ArgumentException.
     *
     * In either case, preserve all the details.
     */
    private static Exception translateCause(OpenJPAException ke) {
    	Throwable cause = ke.getCause();
    	if (cause instanceof ObjectNotFoundException) {
    		return new EntityNotFoundException(
    		        ke.getMessage(), getNestedThrowables(ke),
            	    getFailedObject(ke), ke.isFatal());
    	} else if (cause instanceof ObjectExistsException) {
    		return new EntityExistsException(
    		        ke.getMessage(), getNestedThrowables(ke),
            	    getFailedObject(ke), ke.isFatal());
    	} else if (cause instanceof NoTransactionException) {
    		return new TransactionRequiredException(
        		    ke.getMessage(), getNestedThrowables(ke),
                	getFailedObject(ke), ke.isFatal());
    	} else if (cause instanceof OptimisticException) {
    		return new OptimisticLockException(
        		    ke.getMessage(), getNestedThrowables(ke),
                	getFailedObject(ke), ke.isFatal());
    	} else {
    		return new org.apache.openjpa.persistence.ArgumentException(
        		ke.getMessage(), getNestedThrowables(ke),
        		getFailedObject(ke), ke.isFatal());
    	}
    }

    /**
     * Translate the given general exception.
     */
    private static Throwable translateGeneralException(OpenJPAException ke) {
        Exception e = new org.apache.openjpa.persistence.PersistenceException
            (ke.getMessage(), getNestedThrowables(ke),
                getFailedObject(ke), ke.isFatal());
        e.setStackTrace(ke.getStackTrace());
        return e;
    }

    /**
     * Return true if the given exception is a persistence exception.
     */
    private static boolean isPersistenceException(Throwable t) {
        return t.getClass().getName()
            .startsWith("org.apache.openjpa.persistence.");
    }

    /**
     * Translate the nested throwables of the given openjpa exception into
     * nested throwables for a persistence exception.
     */
    private static Throwable[] getNestedThrowables(OpenJPAException ke) {
        Throwable[] nested = ke.getNestedThrowables();
        if (nested.length == 0)
            return nested;

        Throwable[] trans = new Throwable[nested.length];
        for (int i = 0; i < nested.length; i++)
            trans[i] = translateException(nested[i], false);
        return trans;
    }

    /**
     * Return the failed object for the given exception, performing any
     * necessary conversions.
     */
    private static Object getFailedObject(OpenJPAException ke) {
        Object o = ke.getFailedObject();
        if (o == null)
            return null;
        if (o instanceof Broker)
            return JPAFacadeHelper.toEntityManager((Broker) o);
        return JPAFacadeHelper.fromOpenJPAObjectId(o);
    }

    /**
     * Helper method to extract a nested exception from an internal nested
     * array in a safe way.
     */
    static Throwable getCause(Throwable[] nested) {
        if (nested == null || nested.length == 0)
            return null;
		return nested[0];
	}
}
