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

import java.util.HashMap;
import java.util.Map;

import javax.transaction.Status;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.transaction.GenericXaResource;
import org.apache.ofbiz.entity.transaction.TransactionUtil;

/**
 * ServiceXaWrapper - XA Resource wrapper for running services on commit() or rollback()
 * @deprecated - Use ServiceSynchronization instead (via LocalDispatcher)
 */
@Deprecated
public class ServiceXaWrapper extends GenericXaResource {

    public static final String module = ServiceXaWrapper.class.getName();
    public static final int TYPE_ROLLBACK = 600;
    public static final int TYPE_COMMIT = 500;
    public static final int MODE_ASYNC = 100;
    public static final int MODE_SYNC = 200;

    protected DispatchContext dctx = null;
    protected String rollbackService = null;
    protected String commitService = null;
    protected String runAsUser = null;
    protected Map<String, ? extends Object> rollbackContext = null;
    protected Map<String, ? extends Object> commitContext = null;
    protected boolean rollbackAsync = true;
    protected boolean rollbackAsyncPersist = true;
    protected boolean commitAsync = false;
    protected boolean commitAsyncPersist = false;

    protected ServiceXaWrapper() {}
    public ServiceXaWrapper(DispatchContext dctx) {
        this.dctx = dctx;
    }

    /**
     * Sets the service to run on commit()
     * @param serviceName Name of service to run
     * @param context Context to use when running
     */
    public void setCommitService(String serviceName, Map<String, ? extends Object> context) {
        this.setCommitService(serviceName, null, context, commitAsync, commitAsyncPersist);
    }

    /**
     * Sets the service to run on commit()
     * @param serviceName Name of service to run
     * @param context Context to use when running
     * @param async override default async behavior
     */
    public void setCommitService(String serviceName, Map<String, ? extends Object> context, boolean async, boolean persist) {
        this.setCommitService(serviceName, null, context, async, persist);
    }

    /**
     * Sets the service to run on commit()
     * @param serviceName Name of service to run
     * @param runAsUser UserLoginID to run as
     * @param context Context to use when running
     * @param async override default async behavior
     */
    public void setCommitService(String serviceName, String runAsUser, Map<String, ? extends Object> context, boolean async, boolean persist) {
        this.commitService = serviceName;
        this.runAsUser = runAsUser;
        this.commitContext = context;
        this.commitAsync = async;
        this.commitAsyncPersist = persist;
    }


    /**
     * @return The name of the service to run on commit()
     */
    public String getCommitService() {
        return this.commitService;
    }

    /**
     * @return The context used when running the commit() service
     */
    public Map<String, ? extends Object> getCommitContext() {
        return this.commitContext;
    }

    /**
     * Sets the service to run on rollback()
     * @param serviceName Name of service to run
     * @param context Context to use when running
     */
    public void setRollbackService(String serviceName, Map<String, ? extends Object> context) {
        this.setRollbackService(serviceName, context, rollbackAsync, rollbackAsyncPersist);
    }

    /**
     * Sets the service to run on rollback()
     * @param serviceName Name of service to run
     * @param context Context to use when running
     * @param async override default async behavior
     */
    public void setRollbackService(String serviceName, Map<String, ? extends Object> context, boolean async, boolean persist) {
        this.setRollbackService(serviceName, null, context, async, persist);
    }

    /**
     * Sets the service to run on rollback()
     * @param serviceName Name of service to run
     * @param runAsUser userLoginId to run the service as
     * @param context Context to use when running
     * @param async override default async behavior
     */
    public void setRollbackService(String serviceName, String runAsUser, Map<String, ? extends Object> context, boolean async, boolean persist) {
        this.rollbackService = serviceName;
        this.runAsUser = runAsUser;
        this.rollbackContext = context;
        this.rollbackAsync = async;
        this.rollbackAsyncPersist = persist;
    }

    /**
     * @return The name of the service to run on rollback()
     */
    public String getRollbackService() {
        return this.rollbackService;
    }

    /**
     * @return The context used when running the rollback() service
     */
    public Map<String, ? extends Object> getRollbackContext() {
        return this.rollbackContext;
    }

    @Override
    public void enlist() throws XAException {
        super.enlist();
        if (Debug.verboseOn()) Debug.logVerbose("Enlisted in transaction : " + this.toString(), module);
    }

    // -- XAResource Methods
    /**
     * @see javax.transaction.xa.XAResource#commit(javax.transaction.xa.Xid xid, boolean onePhase)
     */
    @Override
    public void commit(Xid xid, boolean onePhase) throws XAException {
        if (Debug.verboseOn()) Debug.logVerbose("ServiceXaWrapper#commit() : " + onePhase + " / " + xid.toString(), module);
        // the commit listener
        if (this.active) {
            Debug.logWarning("commit() called without end()", module);
        }
        if (this.xid == null || !this.xid.equals(xid)) {
            throw new XAException(XAException.XAER_NOTA);
        }

        final String service = commitService;
        final Map<String, ? extends Object> context = commitContext;
        final boolean persist = commitAsyncPersist;
        final boolean async = commitAsync;

        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    runService(service, context, persist, (async ? MODE_ASYNC : MODE_SYNC), TYPE_COMMIT);
                } catch (XAException e) {
                    Debug.logError(e, module);
                }
            }
        };
        thread.start();

        this.xid = null;
        this.active = false;
    }

    /**
     * @see javax.transaction.xa.XAResource#rollback(javax.transaction.xa.Xid xid)
     */
    @Override
    public void rollback(Xid xid) throws XAException {
        if (Debug.verboseOn()) Debug.logVerbose("ServiceXaWrapper#rollback() : " + xid.toString(), module);
        // the rollback listener
        if (this.active) {
            Debug.logWarning("rollback() called without end()", module);
        }
        if (this.xid == null || !this.xid.equals(xid)) {
            throw new XAException(XAException.XAER_NOTA);
        }

        final String service = rollbackService;
        final Map<String, ? extends Object> context = rollbackContext;
        final boolean persist = rollbackAsyncPersist;
        final boolean async = rollbackAsync;

        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    runService(service, context, persist, (async ? MODE_ASYNC : MODE_SYNC), TYPE_ROLLBACK);
                } catch (XAException e) {
                    Debug.logError(e, module);
                }
            }
        };
        thread.start();

        this.xid = null;
        this.active = false;
    }

    @Override
    public int prepare(Xid xid) throws XAException {
        // overriding to log two phase commits
        if (Debug.verboseOn()) Debug.logVerbose("ServiceXaWrapper#prepare() : " + xid.toString(), module);
        int rtn;
        try {
            rtn = super.prepare(xid);
        } catch (XAException e) {
            Debug.logError(e, module);
            throw e;
        }
        if (Debug.verboseOn()) Debug.logVerbose("ServiceXaWrapper#prepare() : " + rtn + " / " + (rtn == XA_OK) , module);
        return rtn;
    }



    protected final void runService(String service, Map<String, ? extends Object> context, boolean persist, int mode, int type) throws XAException {
        // set the logging prefix
        String msgPrefix = "[XaWrapper] ";
        switch (type) {
            case TYPE_ROLLBACK:
                msgPrefix = "[Rollback] ";
                break;
            case TYPE_COMMIT:
                msgPrefix = "[Commit] ";
                break;
            default:
                Debug.logWarning("There was another type instead of [Commit] or [Rollback] in runService: " + type, module);
        }

        // if a service exists; run it
        if (UtilValidate.isNotEmpty(service)) {

            // suspend this transaction
            Transaction parentTx = null;
            boolean beganTx;

            try {
                int currentTxStatus = Status.STATUS_UNKNOWN;
                try {
                    currentTxStatus = TransactionUtil.getStatus();
                } catch (GenericTransactionException e) {
                    Debug.logWarning(e, module);
                }

                // suspend the parent tx
                if (currentTxStatus != Status.STATUS_NO_TRANSACTION) {
                    parentTx = TransactionUtil.suspend();
                }

                // begin the new tx
                beganTx = TransactionUtil.begin();

                // configure and run the service
                try {
                    // obtain the model and get the valid context
                    ModelService model = dctx.getModelService(service);
                    Map<String, Object> thisContext;
                    if (model.validate) {
                        thisContext = model.makeValid(context, ModelService.IN_PARAM);
                    } else {
                        thisContext = new HashMap<>();
                        thisContext.putAll(context);
                    }

                    // set the userLogin object
                    thisContext.put("userLogin", ServiceUtil.getUserLogin(dctx, thisContext, runAsUser));

                    // invoke based on mode
                    switch (mode) {
                        case MODE_ASYNC:
                            Debug.logInfo(msgPrefix + "Invoking [" + service + "] via runAsync", module);
                            dctx.getDispatcher().runAsync(service, thisContext, persist);
                            break;

                        case MODE_SYNC:
                            Debug.logInfo(msgPrefix + "Invoking [" + service + "] via runSyncIgnore", module);
                            dctx.getDispatcher().runSyncIgnore(service, thisContext);
                            break;
                    }
                } catch (Throwable t) {
                    Debug.logError(t, "Problem calling " + msgPrefix + "service : " + service + " / " + context, module);
                    try {
                        TransactionUtil.rollback(beganTx, t.getMessage(), t);
                    } catch (GenericTransactionException e) {
                        Debug.logError(e, module);
                    }

                    // async calls are assumed to not effect this TX
                    if (mode != MODE_ASYNC) {
                        throw new XAException(XAException.XA_RBOTHER);
                    }
                } finally {
                    // commit the transaction
                    try {
                        TransactionUtil.commit(beganTx);
                    } catch (GenericTransactionException e) {
                        Debug.logError(e, module);
                    }
                }
            } catch (GenericTransactionException e) {
                Debug.logError(e, module);
            } finally {
                // resume the transaction
                if (parentTx != null) {
                    try {
                        TransactionUtil.resume(parentTx);
                    } catch (Exception e) {
                        Debug.logError(e, module);
                    }
                }
            }
        } else {
            if (Debug.verboseOn()) Debug.logVerbose("No " + msgPrefix + "service defined; nothing to do", module);
        }

        this.xid = null;
        this.active = false;
    }
}
