/* JFox, the OpenSource J2EE Application Server
 *
 * Copyright (C) 2002 huihoo.com
 * Distributable under GNU LGPL license
 * See the GNU Lesser General Public License for more details.
 */

package org.jfox.tm;

import java.util.TimerTask;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import javax.transaction.Transaction;
import javax.transaction.RollbackException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.SystemException;
import javax.transaction.Synchronization;
import javax.transaction.Status;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import javax.transaction.xa.XAException;

import org.huihoo.jfox.logging.Logger;

/**
 * 实现 javax.mw.Transaction 接口
 * 增加了 timeout 和 suspend, resume 功能
 * 因为涉及到 XAResource 的比较,因此重新封装了 XAResource 为 KeyXAResource
 *
 * @author <a href="mailto:young_yy@hotmail.com">Young Yang</a>
 */

class TransactionImpl extends TimerTask implements Transaction {
  private static Logger logger = Logger.getLogger(TransactionImpl.class.getName());
  private int status = Status.STATUS_NO_TRANSACTION;

  // global tx id
  private Xid xid = null;
  // 用来生成事务分支的计数器
  private static int branchCount = 0;

  // 事务同步器列表
  private List syncs = new ArrayList();
  //已经征用的资源集合
  private List enlistedXARes = new ArrayList();
  //暂停的资源集合
  private List suspendedXARes = new ArrayList();

  public TransactionImpl() {
    status = Status.STATUS_ACTIVE;
    xid = new XidImpl();
    logger.debug(
            new StringBuffer()
            .append("begin a new Transaction, tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());
  }

  /**
   *
   * @param xaResource
   * @param flag XAResource.TMSUCCESS or XAResource.TMSUSPEND or XAResource.TMFAIL
   * @return
   * @throws IllegalStateException
   * @throws SystemException
   */
  public synchronized boolean delistResource(XAResource xaResource, int flag) throws IllegalStateException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.delistResource(),flag = " + flag + " tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());

    if (xaResource == null) throw new IllegalArgumentException("null xaRes");
    if (flag != XAResource.TMSUCCESS &&
            flag != XAResource.TMSUSPEND &&
            flag != XAResource.TMFAIL)
      throw new IllegalArgumentException("wrong flag, flag must be one of XAResource.TMSUCCESS, XAResource.TMSUSPEND, XAResource.TMFAIL");

    KeyXAResource keyXAres = new KeyXAResource(xaResource);
    int index = enlistedXARes.indexOf(keyXAres);
    if(index < 0) {
      throw new IllegalArgumentException("xaResource not enlisted");
    }
    switch (status) {
      case Status.STATUS_ACTIVE:
      case Status.STATUS_MARKED_ROLLBACK:
        break;
      case Status.STATUS_PREPARING:
        throw new IllegalStateException("Already started preparing.");
      case Status.STATUS_ROLLING_BACK:
        throw new IllegalStateException("Already started rolling back.");
      case Status.STATUS_PREPARED:
        throw new IllegalStateException("Already prepared.");
      case Status.STATUS_COMMITTING:
        throw new IllegalStateException("Already started committing.");
      case Status.STATUS_COMMITTED:
        throw new IllegalStateException("Already committed.");
      case Status.STATUS_ROLLEDBACK:
        throw new IllegalStateException("Already rolled back.");
      case Status.STATUS_NO_TRANSACTION:
        throw new IllegalStateException("No Transaction.");
      case Status.STATUS_UNKNOWN:
        throw new IllegalStateException("Unknown state");
      default:
        throw new IllegalStateException("Illegal status: " + StatusHelper.toString(status));
    }

    keyXAres = (KeyXAResource)enlistedXARes.get(index);
    try {
      logger.debug("delist resource " + keyXAres.getXid() + " with flag " + flag);
      xaResource.end(keyXAres.getXid(),flag);
      enlistedXARes.remove(index);
      return true;
    }
    catch(XAException e){
      logger.warn(e);
      status = Status.STATUS_MARKED_ROLLBACK;
      return false;
    }
  }

  public synchronized boolean enlistResource(XAResource xaResource) throws RollbackException, IllegalStateException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.enlistResource(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());
    if (xaResource == null) throw new IllegalArgumentException("null xaRes");
    switch (status) {
      case Status.STATUS_ACTIVE :
      case Status.STATUS_PREPARING :
        break;
      case Status.STATUS_PREPARED :
        throw new IllegalStateException("Transaction already prepared.");
      case Status.STATUS_COMMITTING :
        throw new IllegalStateException("Transaction already started committing.");
      case Status.STATUS_COMMITTED :
        throw new IllegalStateException("Transaction already committed.");
      case Status.STATUS_MARKED_ROLLBACK :
        throw new RollbackException("Transaction already marked for rollback");
      case Status.STATUS_ROLLING_BACK :
        throw new RollbackException("Transaction already started rolling back.");
      case Status.STATUS_ROLLEDBACK :
        throw new RollbackException("Transaction already rolled back.");
      case Status.STATUS_NO_TRANSACTION :
        throw new IllegalStateException("No current Transaction.");
      case Status.STATUS_UNKNOWN :
        throw new IllegalStateException("Unknown Transaction status");
      default :
        throw new IllegalStateException(
                "Illegal Transaction status : " + StatusHelper.toString(status));
    }

    int flag = XAResource.TMNOFLAGS;

    KeyXAResource keyXAres = new KeyXAResource(xaResource);
    int index = enlistedXARes.indexOf(keyXAres);
    if(index >=0) {
      keyXAres = (KeyXAResource)enlistedXARes.get(index);
      /**
       * 如果是同一个引用,就不再征用了,只针对 isSameRM
       */
      if(keyXAres.getXaResource() == xaResource) {
        return false;
      }
      flag = XAResource.TMJOIN;
    }
    else if((index=suspendedXARes.indexOf(keyXAres)) >= 0) { // 已经 suspended
      flag = XAResource.TMRESUME;
      keyXAres = (KeyXAResource)suspendedXARes.get(index);
      suspendedXARes.remove(index);
      enlistedXARes.add(keyXAres);
    }
    else {
      enlistedXARes.add(keyXAres);
    }
    try {
      logger.debug("resource start new branch Transaction " + keyXAres.getXid() + " with flag " + flag);
      xaResource.start(keyXAres.getXid(),flag);
      return true;
    }
    catch(XAException e){
      logger.warn(e);
      return false;
    }
  }

  public int getStatus() throws SystemException {
    return status;
  }

  public synchronized void registerSynchronization(Synchronization synchronization) throws RollbackException, IllegalStateException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.registerSynchronization(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());
    if (synchronization == null) throw new IllegalArgumentException("Null synchronization");
    switch (status) {
      case Status.STATUS_ACTIVE:
      case Status.STATUS_PREPARING:
      case Status.STATUS_MARKED_ROLLBACK:
        break;
      case Status.STATUS_PREPARED:
        throw new IllegalStateException("Already prepared.");
      case Status.STATUS_COMMITTING:
        throw new IllegalStateException("Already started committing.");
      case Status.STATUS_COMMITTED:
        throw new IllegalStateException("Already committed.");
      case Status.STATUS_ROLLING_BACK:
        throw new RollbackException("Already started rolling back.");
      case Status.STATUS_ROLLEDBACK:
        throw new RollbackException("Already rolled back.");
      case Status.STATUS_NO_TRANSACTION:
        throw new IllegalStateException("No Transaction.");
      case Status.STATUS_UNKNOWN:
        throw new IllegalStateException("Unknown state");
      default:
        throw new IllegalStateException("Illegal status: " + StatusHelper.toString(status));
    }
    syncs.add(synchronization);
  }

  public synchronized void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.commit(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());

    switch (status) {
      case Status.STATUS_PREPARING:
        throw new IllegalStateException("Already started preparing.");
      case Status.STATUS_PREPARED:
        throw new IllegalStateException("Already prepared.");
      case Status.STATUS_ROLLING_BACK:
        throw new IllegalStateException("Already started rolling back.");
      case Status.STATUS_ROLLEDBACK:
        throw new IllegalStateException("Already rolled back.");
      case Status.STATUS_COMMITTING:
        throw new IllegalStateException("Already started committing.");
      case Status.STATUS_COMMITTED:
        throw new IllegalStateException("Already committed.");
      case Status.STATUS_NO_TRANSACTION:
        throw new IllegalStateException("No Transaction.");
      case Status.STATUS_UNKNOWN:
        throw new IllegalStateException("Unknown state");
      case Status.STATUS_MARKED_ROLLBACK:
        this.rollback();
        throw new RollbackException("Already marked for rollback");
      case Status.STATUS_ACTIVE:
        break;
      default:
        throw new IllegalStateException("Illegal status: " + StatusHelper.toString(status));
    }

    doBeforeCompletion(); // 可能已经把 StatusHelper 设为 ROLLBACK_ONLY

    try {
      if(status == Status.STATUS_ACTIVE) {
        if (enlistedXARes.size() == 0) {
          // do nothing, just set status
          status = Status.STATUS_COMMITTED;
        }
        else if (enlistedXARes.size() == 1) {
          doOnePhaseCommit();
        }
        else { // Two phase commit
          doTwoPhaseCommint();
        }
      }
      else if(status == Status.STATUS_MARKED_ROLLBACK){
        this.rollback();
      }
    }
    finally {
      doAfterCompletion();
      enlistedXARes.clear();
    }
  }

  private void doOnePhaseCommit() throws RollbackException {
    status = Status.STATUS_COMMITTING;
    KeyXAResource keyXARes = (KeyXAResource)enlistedXARes.get(0);
    try {
      endResource(keyXARes,XAResource.TMSUCCESS);
    }
    catch(XAException e){
      logger.warn("end resource error, to be roll back", e);
      status = Status.STATUS_MARKED_ROLLBACK;
    }

    if(status == Status.STATUS_MARKED_ROLLBACK){
      try {
        status = Status.STATUS_ROLLING_BACK;
        keyXARes.getXaResource().rollback(keyXARes.getXid());
        status = Status.STATUS_ROLLEDBACK;
      }
      catch(XAException e){
        logger.warn(e);
      }
      throw new RollbackException("commit failed, already rolled back");
    }
    else {
      try {
        keyXARes.getXaResource().commit(keyXARes.getXid(),true);
      }
      catch(XAException e){
        logger.warn(e);
      }
    }
    status = Status.STATUS_COMMITTED;
  }

  /**
   * 两阶段提交,首先对所有的资源进行 prepare 操作,
   * 如果 prepare 没有混乱并且返回值也不全是 XA_READON,进行真正提交
   */
  private void doTwoPhaseCommint() throws
          HeuristicMixedException,
          HeuristicRollbackException,
          RollbackException{
    logger.debug("start two phase commit");
    status = Status.STATUS_PREPARING;
    int heuristicCode = XAException.XA_RETRY;
    // do prepare
    // 如果所有的 XAResource 都是 XA_RDONLY,最后将不再 commit
    int prepareStatus = XAResource.XA_RDONLY;
    for(Iterator it = enlistedXARes.iterator();it.hasNext();){
      KeyXAResource keyXARes = (KeyXAResource)it.next();
      try {
        logger.debug("prepare " + keyXARes.getXid());
        endResource(keyXARes,XAResource.TMSUCCESS);
        int xaStatus = keyXARes.getXaResource().prepare(keyXARes.getXid());
        if(xaStatus == XAResource.XA_OK){
          prepareStatus = XAResource.XA_OK;
        }
        else if(xaStatus == XAResource.XA_RDONLY){
        }
        else { // Illegal prepare status
          status = Status.STATUS_MARKED_ROLLBACK;
          break;
        }
      }
      catch(XAException e){
        // 虽然出错了,但是因为要 rollback,所以状态仍然是 XA_OK
        logger.warn(e);
        prepareStatus = XAResource.XA_OK;
        switch (e.errorCode) {
          case XAException.XA_HEURCOM:
            // 有些资源已经提交了,无法 rollback,所以另外一些也得提交
            heuristicCode = decideHeuristic(heuristicCode,e.errorCode);
            break;
          case XAException.XA_HEURRB:
          case XAException.XA_HEURMIX:
          case XAException.XA_HEURHAZ:
            heuristicCode = decideHeuristic(heuristicCode,e.errorCode);
            if (status == Status.STATUS_PREPARING)
              status = Status.STATUS_MARKED_ROLLBACK;
            break;
          default:
            if (status == Status.STATUS_PREPARING)
              status = Status.STATUS_MARKED_ROLLBACK;
            break;
        }
        try {
          keyXARes.getXaResource().forget(keyXARes.getXid());
        }
        catch (XAException ex) {
          logger.warn(e);
        }
      }
      catch(Throwable e){
        logger.warn(e);
        status = Status.STATUS_MARKED_ROLLBACK;
      }
    }
    // prepare 完毕,开始第二阶段提交
    status = Status.STATUS_PREPARED;

    if(prepareStatus == XAResource.XA_RDONLY) { // 全部都是只读,无需再提交
      status = Status.STATUS_COMMITTED;
    }
    // 可以进行第二阶段提交,但有可能提交,也有可能回滚
    else {
      // 应该 commit
      if(heuristicCode == XAException.XA_RETRY || heuristicCode == XAException.XA_HEURCOM){
        status = Status.STATUS_COMMITTING;
        List temp = new ArrayList(enlistedXARes);
        for(Iterator it = temp.iterator();it.hasNext();) {
          KeyXAResource keyXARes = (KeyXAResource)it.next();
          try {
            keyXARes.getXaResource().commit(keyXARes.getXid(),false);
          }
          catch (XAException e) {
            logger.warn(e);
            switch (e.errorCode) {
              case XAException.XA_HEURRB:
              case XAException.XA_HEURCOM:
              case XAException.XA_HEURMIX:
              case XAException.XA_HEURHAZ:
                heuristicCode = decideHeuristic(heuristicCode,e.errorCode);
                break;
              default: break;
            }
            try {
              keyXARes.getXaResource().forget(keyXARes.getXid());
            }
            catch (XAException ex) {
              ex.printStackTrace();
            }
          }
          catch (Throwable t) {
            t.printStackTrace();
          }
        }
        status = Status.STATUS_COMMITTED;
        checkHeuristics(heuristicCode);
      }
      // 由于启发式代码,只能 rollback
      else {
        status = Status.STATUS_ROLLING_BACK;
        List temp = new ArrayList(enlistedXARes);
        for(Iterator it = temp.iterator();it.hasNext();) {
          KeyXAResource keyXARes = (KeyXAResource)it.next();
          try {
            keyXARes.getXaResource().rollback(keyXARes.getXid());
          }
          catch (XAException e) {
            logger.warn(e);
//             Heuristic rollback is not that bad when rolling back.
            switch (e.errorCode) {
              case XAException.XA_HEURRB:
              case XAException.XA_HEURCOM:
              case XAException.XA_HEURMIX:
              case XAException.XA_HEURHAZ:
                decideHeuristic(heuristicCode,e.errorCode);
                break;
              default: break;
            }
            try {
              keyXARes.getXaResource().forget(keyXARes.getXid());
            }
            catch (XAException ex) {
              ex.printStackTrace();
            }
          }
          catch(Throwable t){
            t.printStackTrace();
          }
        }
        status = Status.STATUS_ROLLEDBACK;
        checkHeuristics(heuristicCode);
        throw new RollbackException("Unable to commit because of heuristic code, tx=" + toString());
      }
    }
  }

  /**
   * 根据当前的启发式代码,根据 XAException 的启发式代码,计算新的启发式代码
   * @param heuristicCode
   * @param xaExceptionCode
   * @return
   */
  private int decideHeuristic(int heuristicCode, int xaExceptionCode){
    switch (xaExceptionCode) {
      case XAException.XA_HEURMIX:
        heuristicCode = XAException.XA_HEURMIX;
        break;
      case XAException.XA_HEURRB:
        if (heuristicCode == XAException.XA_RETRY)
          heuristicCode = XAException.XA_HEURRB;
        else if (heuristicCode == XAException.XA_HEURCOM ||
                heuristicCode == XAException.XA_HEURHAZ)
          heuristicCode = XAException.XA_HEURMIX;
        break;
      case XAException.XA_HEURCOM:
        if (heuristicCode ==  XAException.XA_RETRY)
          heuristicCode = XAException.XA_HEURCOM;
        else if (heuristicCode == XAException.XA_HEURRB ||
                heuristicCode == XAException.XA_HEURHAZ)
          heuristicCode = XAException.XA_HEURMIX;
        break;
      case XAException.XA_HEURHAZ:
        if (heuristicCode ==  XAException.XA_RETRY)
          heuristicCode = XAException.XA_HEURHAZ;
        else if (heuristicCode == XAException.XA_HEURCOM ||
                heuristicCode == XAException.XA_HEURRB)
          heuristicCode = XAException.XA_HEURMIX;
        break;
      default:
        throw new IllegalArgumentException();
    }
    return heuristicCode;
  }

  /**
   * 根据启发式代码,抛出合适的异常
   * @param heuristicCode
   * @throws HeuristicMixedException
   * @throws HeuristicRollbackException
   */
  private void checkHeuristics(int heuristicCode) throws HeuristicMixedException,
          HeuristicRollbackException
  {
    switch (heuristicCode) {
      case XAException.XA_HEURHAZ:
      case XAException.XA_HEURMIX:
        throw new HeuristicMixedException();
      case XAException.XA_HEURRB:
        throw new HeuristicRollbackException();
      case XAException.XA_HEURCOM:
        return;
    }
  }


  public synchronized void rollback() throws IllegalStateException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.rollback(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());

    switch (status) {
      case Status.STATUS_ACTIVE :
      case Status.STATUS_MARKED_ROLLBACK :
        break;
      case Status.STATUS_ROLLEDBACK :
        logger.warn("Transaction.rollback(): already rolled back");
        return;
      default :
        logger.error("Transaction.rollback(): bad status");
        throw new IllegalStateException("Cannot rollback(), " +
                "tx=" + toString() +
                " status=" +
                StatusHelper.toString(status));
    }

    doBeforeCompletion();
    status = Status.STATUS_ROLLING_BACK;
    List temp = new ArrayList(enlistedXARes);
    int size = temp.size();
    for (int i = 0; i < size; i++) {
      KeyXAResource keyXARes = ((KeyXAResource)temp.get(i));
      try {
        endResource(keyXARes,XAResource.TMSUCCESS);
        keyXARes.getXaResource().rollback(keyXARes.getXid());
      }
      catch (XAException e) {
        logger.error("rollback XAResource " + keyXARes.getXaResource() + " failed.", e);
      }
    }
    status = Status.STATUS_ROLLEDBACK;
    doAfterCompletion();
  }

  public synchronized void setRollbackOnly() throws IllegalStateException, SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.setRollbackOnly(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());
    switch (status) {
      case Status.STATUS_ACTIVE:
      case Status.STATUS_PREPARING:
      case Status.STATUS_PREPARED:
        status = Status.STATUS_MARKED_ROLLBACK;
        return;
      case Status.STATUS_MARKED_ROLLBACK:
      case Status.STATUS_ROLLING_BACK:
        return;
      case Status.STATUS_COMMITTING:
        throw new IllegalStateException("started committing.");
      case Status.STATUS_COMMITTED:
        throw new IllegalStateException("already committed.");
      case Status.STATUS_ROLLEDBACK:
        throw new IllegalStateException("already rolled back.");
      case Status.STATUS_NO_TRANSACTION:
        throw new IllegalStateException("no Transaction.");
      case Status.STATUS_UNKNOWN:
        throw new IllegalStateException("unknown state");
      default:
        throw new IllegalStateException("Illegal status: " + StatusHelper.toString(status));
    }
  }

  public String toString() {
    return "Transaction [" + xid.toString() + "]";
  }

  /**
   * timeout trigger
   */
  public synchronized void run() {
    timeout();
  }

  void timeout() {
    try {
      logger.debug(
              new StringBuffer()
              .append("Transaction timeout, tx= ")
              .append(this.toString())
              .append(" status=")
              .append(StatusHelper.toString(status))
              .toString());

      this.setRollbackOnly();
    }
    catch(Exception e){
      logger.warn(e.getMessage(),e);
    }
  }

  private void doBeforeCompletion() {
    try {
      for (int i = 0; i < syncs.size(); i++) {
        ((Synchronization)syncs.get(i)).beforeCompletion();
      }
    }
    catch (Throwable t) {
      logger.warn("Synchronization.beforeCompletion()",t);
      status = Status.STATUS_MARKED_ROLLBACK;
    }
  }

  private void doAfterCompletion() {
    try {
      for (int i = 0; i < syncs.size(); i++) {
        ((Synchronization)syncs.get(i)).afterCompletion(status);
      }
    }
    catch (Throwable t) {
      logger.warn("Synchronization.afterCompletion()",t);
    }
  }

  /**
   * 把已经征用的资源放到解除资源集合中,并清除征集资源集合
   * @throws SystemException
   */
  synchronized void suspend() throws SystemException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.suspend(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());

    suspendedXARes.addAll(enlistedXARes);
    for (int i = 0; i < suspendedXARes.size(); i++) {
      delistResource(((KeyXAResource) suspendedXARes.get(i)).getXaResource(), XAResource.TMSUSPEND);
    }
    enlistedXARes.clear();
  }

  /**
   * 重新征用 supend 的资源
   * @throws SystemException
   * @throws RollbackException
   */
  synchronized void resume() throws SystemException, RollbackException {
    logger.debug(
            new StringBuffer()
            .append("Transaction.resume(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .toString());
    List temp = new ArrayList(suspendedXARes);
    for (int i = 0; i < temp.size(); i++) {
      enlistResource(((KeyXAResource)temp.get(i)).getXaResource());
    }
    suspendedXARes.clear();
  }


  private void endResource(KeyXAResource keyXAres, int flag) throws XAException {
    logger.debug(
            new StringBuffer()
            .append("XAResource.end(), tx= ")
            .append(this.toString())
            .append(" status=")
            .append(StatusHelper.toString(status))
            .append(" flag = " + flag)
            .toString());

    keyXAres.getXaResource().end(keyXAres.getXid(),flag);
    if (flag == XAResource.TMFAIL) status = Status.STATUS_MARKED_ROLLBACK;
    enlistedXARes.remove(keyXAres);
  }

  public static void main(String[] args) {

  }

  /**
   * KeyXAResource 功能
   * 1。关联 xid
   * 2。处理 isSameRM,使能成为 List 的元素
   */
  private class KeyXAResource {
    private XAResource xaRes = null;
    private Xid branchXid = null;

    public KeyXAResource(XAResource xaRes) {
      this.xaRes = xaRes;
    }

    public synchronized Xid getXid(){
      if(branchXid == null) {
        branchXid = XidImpl.createBranchXid(xid,branchCount++);
      }
      return branchXid;
    }

    public XAResource getXaResource() {
      return xaRes;
    }

    public int hashCode() {
      return 0;
    }

    public boolean equals(Object obj) {
      if(!(obj instanceof KeyXAResource)) {
        return false;
      }
      else {
        try {
          return ((KeyXAResource)obj).getXaResource().isSameRM(xaRes);
        }
        catch(XAException e){
          logger.warn(e);
          return false;
        }
      }
    }
  }
}