/*
 * 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.ee;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAResource;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.enhance.AsmAdaptor;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.NoTransactionException;

import serp.bytecode.BCClass;
import serp.bytecode.Project;

/**
 * {@link ManagedRuntime} implementation that allows synchronization with a
 * WebSphere managed transaction.
 *
 * <P>
 * WebSphere Application Server does not expose the TransactionManager to an
 * application. Instead it provides a proprietary interface to register for
 * synchronization and obtain transaction ids.
 *
 * <P>
 * WASManagedRuntime provides the wrapper classes needed to interact with the
 * WAS proprietary interface and the OpenJPA kernel.
 *
 * @author Kevin Sutter
 */
public class WASManagedRuntime extends AbstractManagedRuntime
        implements ManagedRuntime, Configurable {

    private static final Localizer _loc =
        Localizer.forPackage(WASManagedRuntime.class);

    private Object _extendedTransaction = null;
    private Method _getGlobalId = null;
    private Method _registerSync = null;
    private OpenJPAConfiguration _conf = null;
    private Log _log = null;

    /**
     * Gets an extendedJTATransaction from JNDI and creates a transaction
     * wrapper
     */
    @Override
    public javax.transaction.TransactionManager getTransactionManager()
        throws Exception {
        return new WASTransaction();
    }

    /**
     * Transaction wrapper for WebSphere. WebSphere exposes a subset of the
     * Transaction and TransactionManager interfaces to the customer. Any
     * methods which are not exposed by WebSphere will throw an
     * IllegalStateException to the caller.
     *
     * <P>
     * Methods supported by WAS are
     * <UL>
     * <LI>RegisterSynchronization </LI>
     * <LI>GetStatus</LI>
     * </UL>
     */
    class WASTransaction implements javax.transaction.TransactionManager,
        javax.transaction.Transaction {

        @Override
        public int getStatus() throws SystemException {
            int rval;
            try {
                if (getGlobalId() != null) {
                    rval = Status.STATUS_ACTIVE;
                } else {
                    rval = Status.STATUS_NO_TRANSACTION;
                }
            } catch (Exception e) {
                throw new NoTransactionException(_loc
                        .get("was-transaction-id-exception")).setCause(e);
            }
            return rval;
        }

        /**
         * Provides a Transaction wrapper. The transaction wrapper mayb only be
         * used to determine the status of the current transaction. WebSphere
         * does not allow direct control of container transactions.
         *
         * @return A WebSphere transaction wrapper.
         */
        @Override
        public Transaction getTransaction() throws SystemException {
            return this;
        }

        /**
         * Register for synchronization with a WebSphere managed transaction via
         * the extendedJTATransaction interface.
         */
        @Override
        public void registerSynchronization(Synchronization arg0)
            throws IllegalStateException, RollbackException, SystemException {
            if (_extendedTransaction != null) {
                try {
                    _registerSync.invoke(_extendedTransaction,
                        new Object[] { new WASSynchronization(arg0) });
                } catch (Exception e) {
                    throw new InvalidStateException(_loc
                        .get("was-reflection-exception")).setCause(e);
                }
            } else {
                throw new InvalidStateException(_loc.get("was-lookup-error"));
            }
        }

        /**
         * Gets the GlobalTransaction ID of the WebSphere managed transaction.
         * If no Global Transaction is active null will be returned.
         *
         * @return Null if a global transaction is not active or if an error
         *         occurs. byte[] id if a global transaction is active.
         */
        private byte[] getGlobalId() {
            try {
                return (byte[]) _getGlobalId.invoke(_extendedTransaction, null);
            } catch (Exception e) {
                throw new InvalidStateException(_loc
                    .get("was-reflection-exception")).setCause(e);
            }
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public void begin() throws NotSupportedException, SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "begin"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public void commit() throws HeuristicMixedException,
            HeuristicRollbackException, IllegalStateException,
            RollbackException, SecurityException, SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "commit"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public void resume(Transaction arg0) throws IllegalStateException,
            InvalidTransactionException, SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "resume"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Log a
         * trace instead of throwing an exception. Rollback may be used in
         * some error paths, throwing another exception may result in the
         * original exception being lost.
         */
        @Override
        public void rollback() throws IllegalStateException, SecurityException,
            SystemException {
            if (_log.isTraceEnabled()) {
                _log.trace(_loc.get("was-unsupported-op", "rollback"));
            }
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Log a
         * trace instead of throwing an exception. SetRollbackOnly may be used
         * in some error paths, throwing another exception may result in the
         * original exception being lost.
         */
        @Override
        public void setRollbackOnly() throws IllegalStateException,
            SystemException {
            if (_log.isTraceEnabled()) {
                _log.trace(_loc.get("was-unsupported-op", "setRollbackOnly"));
            }
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public void setTransactionTimeout(int arg0) throws SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "setTransactionTimeout"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public Transaction suspend() throws SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "suspend"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public boolean delistResource(XAResource arg0, int arg1)
            throws IllegalStateException, SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "delistResource"));
        }

        /**
         * Unimplemented, WAS does not provide this level of control. Throws an
         * IllegalStateException
         */
        @Override
        public boolean enlistResource(XAResource arg0)
            throws IllegalStateException, RollbackException, SystemException {
            throw new InvalidStateException(_loc.get("was-unsupported-op",
                "enlistResource"));
        }
    }

    /**
     * WASSynchronization wrapper. This class translates the WAS proprietary
     * synchronization callback methods to javax.transaction.Synchronization
     * methods.
     *
     * <P>
     * This class implements the
     * com.ibm.websphere.jtaextensions.SynchronizationCallback interface. Since
     * SynchronizationCallback is not available at compile time we use Serp to
     * add the interface to the class after all classes have been compiled.
     *
     * <P>
     * SynchronizationCallback is expected to be available whenever this class
     * is instantiated, therefore this class should only be used when running in
     * WebSphere.
     *
     * @see org.apache.openjpa.util.WASTransformer
     */
    static class WASSynchronization {

        Synchronization _sync = null;

        WASSynchronization(Synchronization sync) {
            _sync = sync;
        }

        /**
         * AfterCompletion wrapper. Translates the WAS proprietary call to a
         * javax.transaction.Synchronization call.
         */
        public void afterCompletion(int localTransactionId,
            byte[] globalTransactionId, boolean committed) {
            if (_sync != null) {
                if (committed) {
                    _sync.afterCompletion(Status.STATUS_COMMITTED);
                } else {
                    _sync.afterCompletion(Status.STATUS_UNKNOWN);
                }
            }
        }

        /**
         * BeforeCompletion wrapper. Translates WAS proprietary call to a
         * javax.transaction.Synchronization call.
         */
        public void beforeCompletion(int arg0, byte[] arg1) {
            if (_sync != null) {
                _sync.beforeCompletion();
            }
        }
    }

    /**
     * Caches a copy of the configuration. The configuration is used to obtain
     * the logger and classloader.
     */
    @Override
    public void setConfiguration(Configuration conf) {
        _conf = (OpenJPAConfiguration) conf;
        _log = _conf.getLog(OpenJPAConfiguration.LOG_RUNTIME);
    }

    /**
     * EndConfiguration stub.
     */
    @Override
    public void endConfiguration() {
        try {
            Context ctx = new InitialContext();
            try {
                _extendedTransaction =
                    ctx.lookup("java:comp/websphere/ExtendedJTATransaction");
            } finally {
                ctx.close();
            }

            Class extendedJTATransaction = Class.forName(
                    "com.ibm.websphere.jtaextensions.ExtendedJTATransaction");
            Class synchronizationCallback = Class.forName(
                    "com.ibm.websphere.jtaextensions.SynchronizationCallback");

            _registerSync = extendedJTATransaction.getMethod(
                    "registerSynchronizationCallbackForCurrentTran",
                    new Class[] { synchronizationCallback });
            _getGlobalId = extendedJTATransaction.
                getMethod("getGlobalId", null);
        } catch (Exception e) {
            throw new InvalidStateException(_loc
                .get("was-reflection-exception"), e).setFatal(true);
        }
    }

    /**
     * StartConfiguration stub.
     */
    @Override
    public void startConfiguration() {
        // Nothing to do
    }

    /**
     * Class that will be modified
     */
    static final String CLASS =
        "org.apache.openjpa.ee.WASManagedRuntime$WASSynchronization";

    /**
     * Interface which will be added
     */
    static final String INTERFACE =
        "com.ibm.websphere.jtaextensions.SynchronizationCallback";

    public static void main(String[] args)
        throws IOException {
        Project project = new Project();

        InputStream in = WASManagedRuntime.class.getClassLoader()
            .getResourceAsStream(CLASS.replace('.', '/') + ".class");
        BCClass bcClass = project.loadClass(in);

        String [] interfaces = bcClass.getInterfaceNames();

        if(interfaces != null) {
            for (String anInterface : interfaces) {
                if (anInterface.equals(INTERFACE)) {
                    return;
                }
            }
        }
        bcClass.declareInterface(INTERFACE);
        AsmAdaptor.write(bcClass);
    }

    @Override
    public void setRollbackOnly(Throwable cause)
        throws Exception {
        // there is no generic support for setting the rollback cause
        getTransactionManager().getTransaction().setRollbackOnly();
    }

    @Override
    public Throwable getRollbackCause()
        throws Exception {
        // there is no generic support for setting the rollback cause
        return null;
    }
}
