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

import java.security.AccessController;
import java.security.PrivilegedAction;

import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.PrivilegedGetTccl;
import org.apache.tomcat.util.security.PrivilegedSetTccl;

/**
 * Manages the state transitions for async requests.
 *
 * <pre>
 * The internal states that are used are:
 * DISPATCHED       - Standard request. Not in Async mode.
 * STARTING         - ServletRequest.startAsync() has been called but the
 *                    request in which that call was made has not finished
 *                    processing.
 * STARTED          - ServletRequest.startAsync() has been called and the
 *                    request in which that call was made has finished
 *                    processing.
 * READ_WRITE_OP    - Performing an asynchronous read or write.
 * MUST_COMPLETE    - ServletRequest.startAsync() followed by complete() have
 *                    been called during a single Servlet.service() method. The
 *                    complete() will be processed as soon as the request
 *                    finishes.
 * COMPLETE_PENDING - ServletRequest.startAsync() has been called and before the
 *                    request in which that call was had finished processing,
 *                    complete() was called for a non-container thread. The
 *                    complete() will be processed as soon as the request
 *                    finishes. This is different to MUST_COMPLETE because of
 *                    differences required to avoid race conditions during error
 *                    handling.
 * COMPLETING       - The call to complete() was made once the request was in
 *                    the STARTED state. May or may not be triggered by a
 *                    container thread - depends if start(Runnable) was used.
 * TIMING_OUT       - The async request has timed out and is waiting for a call
 *                    to complete(). If that isn't made, the error state will
 *                    entered.
 * MUST_DISPATCH    - ServletRequest.startAsync() followed by dispatch() have
 *                    been called during a single Servlet.service() method. The
 *                    dispatch() will be processed as soon as the request
 *                    finishes.
 * DISPATCH_PENDING - ServletRequest.startAsync() has been called and before the
 *                    request in which that call was had finished processing,
 *                    dispatch() was called for a non-container thread. The
 *                    dispatch() will be processed as soon as the request
 *                    finishes. This is different to MUST_DISPATCH because of
 *                    differences required to avoid race conditions during error
 *                    handling.
 * DISPATCHING      - The dispatch is being processed.
 * MUST_ERROR       - ServletRequest.startAsync() has been called followed by an
 *                    I/O error on a non-container thread. The main purpose of
 *                    this state is to prevent additional async actions
 *                    (complete(), dispatch() etc.) on the non-container thread.
 *                    The container will perform the necessary error handling,
 *                    including ensuring that the AsyncLister.onError() method
 *                    is called.
 * ERROR            - Something went wrong.
 *
 *                           |-----«-------------------------------«------------------------------|
 *                           |                                                                    |
 *                           |      error()                                                       |
 * |-----------------»---|   |  |--«--------MUST_ERROR---------------«------------------------|   |
 * |                    \|/ \|/\|/                                                            |   |
 * |   |----------«-----E R R O R--«-----------------------«-------------------------------|  |   |
 * |   |      complete() /|\/|\\ \-«--------------------------------«-------|              |  |   |
 * |   |                  |  |  \                                           |              |  |   |
 * |   |    |-----»-------|  |   \-----------»----------|                   |              |  |   |
 * |   |    |                |                          |dispatch()         |              |  ^   |
 * |   |    |                |                         \|/                  ^              |  |   |
 * |   |    |                |          |--|timeout()   |                   |              |  |   |
 * |   |    |     post()     |          | \|/           |     post()        |              |  |   |
 * |   |    |    |---------- | --»DISPATCHED«---------- | --------------COMPLETING«-----|  |  |   |
 * |   |    |    |           |   /|\/|\ |               |                | /|\ /|\      |  |  |   |
 * |   |    |    |    |---»- | ---|  |  |startAsync()   |       timeout()|--|   |       |  |  |   |
 * |   |    ^    ^    |      |       |  |               |                       |       |  ^  |   |
 * |   |    |    |    |   |-- \ -----|  |   complete()  |                       |post() |  |  |   |
 * |   |    |    |    |   |    \        |     /--»----- | ---COMPLETE_PENDING-»-|       ^  |  |   |
 * |   |    |    |    |   |     \       |    /          |                               |  |  |   |
 * |   |    |    |    |   ^      \      |   /           |                    complete() |  |  |   |
 * |  \|/   |    |    |   |       \    \|/ /   post()   |                     /---»-----|  |  ^   |
 * | MUST_COMPLETE-«- | - | --«----STARTING--»--------- | ------------|      /             |  |   |
 * |  /|\    /|\      |   |  complete()  | \            |             |     /   error()    |  |   ^
 * |   |      |       |   |              |  \           |             |    //---»----------|  |   |
 * |   |      |       ^   |    dispatch()|   \          |    post()   |   //                  |   |
 * |   |      |       |   |              |    \         |    |-----|  |  //   nct-io-error    |   |
 * |   |      |       |   |              |     \        |    |     |  | ///---»---------------|   |
 * |   |      |       |   |             \|/     \       |    |    \|/\| |||                       |
 * |   |      |       |   |--«--MUST_DISPATCH-----«-----|    |--«--STARTED«---------«---------|   |
 * |   |      |       | dispatched() /|\   |      \               / |   |        post()       |   |
 * |   |      |       |               |    |       \             /  |   |                     |   |
 * |   |      |       |               |    |        \           /   |   |                     |   |
 * |   |      |       |               |    |post()  |           |   |   |                     ^   |
 * ^   |      ^       |               |    |       \|/          |   |   |asyncOperation()     |   |
 * |   |      |       ^               |    |  DISPATCH_PENDING  |   |   |                     |   |
 * |   |      |       |               |    |  |post()           |   |   |                     |   |
 * |   |      |       |               |    |  |      |----------|   |   |»-READ_WRITE_OP--»---|   |
 * |   |      |       |               |    |  |      |  dispatch()  |            |  |  |          |
 * |   |      |       |               |    |  |      |              |            |  |  |          |
 * |   |      |       |post()         |    |  |      |     timeout()|            |  |  |   error()|
 * |   |      |       |dispatched()   |   \|/\|/    \|/             |  dispatch()|  |  |-»--------|
 * |   |      |       |---«---------- | ---DISPATCHING«-----«------ | ------«----|  |
 * |   |      |                       |     |    ^                  |               |
 * |   |      |                       |     |----|                  |               |
 * |   |      |                       |    timeout()                |               |
 * |   |      |                       |                             |               |
 * |   |      |                       |       dispatch()           \|/              |
 * |   |      |                       |-----------«-----------TIMING_OUT            |
 * |   |      |                                                 |   |               |
 * |   |      |-------«----------------------------------«------|   |               |
 * |   |                          complete()                        |               |
 * |   |                                                            |               |
 * |«- | ----«-------------------«-------------------------------«--|               |
 *     |                           error()                                          |
 *     |                                                  complete()                |
 *     |----------------------------------------------------------------------------|
 * </pre>
 */
public class AsyncStateMachine {

    /**
     * The string manager for this package.
     */
    private static final StringManager sm =
        StringManager.getManager(Constants.Package);

    private enum AsyncState {
        DISPATCHED      (false, false, false, false),
        STARTING        (true,  true,  false, false),
        STARTED         (true,  true,  false, false),
        MUST_COMPLETE   (true,  true,  true,  false),
        COMPLETE_PENDING(true,  true,  false, false),
        COMPLETING      (true,  false, true,  false),
        TIMING_OUT      (true,  true,  false, false),
        MUST_DISPATCH   (true,  true,  false, true),
        DISPATCH_PENDING(true,  true,  false, false),
        DISPATCHING     (true,  false, false, true),
        READ_WRITE_OP   (true,  true,  false, false),
        MUST_ERROR      (true,  true,  false, false),
        ERROR           (true,  true,  false, false);

        private final boolean isAsync;
        private final boolean isStarted;
        private final boolean isCompleting;
        private final boolean isDispatching;

        private AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting,
                boolean isDispatching) {
            this.isAsync = isAsync;
            this.isStarted = isStarted;
            this.isCompleting = isCompleting;
            this.isDispatching = isDispatching;
        }

        public boolean isAsync() {
            return isAsync;
        }

        public boolean isStarted() {
            return isStarted;
        }

        public boolean isDispatching() {
            return isDispatching;
        }

        public boolean isCompleting() {
            return isCompleting;
        }
    }


    private volatile AsyncState state = AsyncState.DISPATCHED;
    // Need this to fire listener on complete
    private AsyncContextCallback asyncCtxt = null;
    private final Processor<?> processor;


    public AsyncStateMachine(Processor<?> processor) {
        this.processor = processor;
    }


    public boolean isAsync() {
        return state.isAsync();
    }

    public boolean isAsyncDispatching() {
        return state.isDispatching();
    }

    public boolean isAsyncStarted() {
        return state.isStarted();
    }

    public boolean isAsyncTimingOut() {
        return state == AsyncState.TIMING_OUT;
    }

    public boolean isAsyncError() {
        return state == AsyncState.ERROR;
    }

    public boolean isCompleting() {
        return state.isCompleting();
    }

    public synchronized void asyncStart(AsyncContextCallback asyncCtxt) {
        if (state == AsyncState.DISPATCHED) {
            state = AsyncState.STARTING;
            this.asyncCtxt = asyncCtxt;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncStart()", state));
        }
    }

    public synchronized void asyncOperation() {
        if (state==AsyncState.STARTED) {
            state = AsyncState.READ_WRITE_OP;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncOperation()", state));
        }
    }

    /*
     * Async has been processed. Whether or not to enter a long poll depends on
     * current state. For example, as per SRV.2.3.3.3 can now process calls to
     * complete() or dispatch().
     */
    public synchronized SocketState asyncPostProcess() {
        if (state == AsyncState.COMPLETE_PENDING) {
            doComplete();
            return SocketState.ASYNC_END;
        } else if (state == AsyncState.DISPATCH_PENDING) {
            doDispatch();
            return SocketState.ASYNC_END;
        } else  if (state == AsyncState.STARTING || state == AsyncState.READ_WRITE_OP) {
            state = AsyncState.STARTED;
            return SocketState.LONG;
        } else if (state == AsyncState.MUST_COMPLETE || state == AsyncState.COMPLETING) {
            asyncCtxt.fireOnComplete();
            state = AsyncState.DISPATCHED;
            return SocketState.ASYNC_END;
        } else if (state == AsyncState.MUST_DISPATCH) {
            state = AsyncState.DISPATCHING;
            return SocketState.ASYNC_END;
        } else if (state == AsyncState.DISPATCHING) {
            state = AsyncState.DISPATCHED;
            return SocketState.ASYNC_END;
        } else if (state == AsyncState.STARTED) {
            // This can occur if an async listener does a dispatch to an async
            // servlet during onTimeout
            return SocketState.LONG;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncPostProcess()", state));
        }
    }


    public synchronized boolean asyncComplete() {
        if (!ContainerThreadMarker.isContainerThread() && state == AsyncState.STARTING) {
            state = AsyncState.COMPLETE_PENDING;
            return false;
        } else {
            return doComplete();
        }
    }


    private synchronized boolean doComplete() {
        clearNonBlockingListeners();
        boolean doComplete = false;
        if (state == AsyncState.STARTING || state == AsyncState.TIMING_OUT ||
                state == AsyncState.ERROR || state == AsyncState.READ_WRITE_OP) {
            state = AsyncState.MUST_COMPLETE;
        } else if (state == AsyncState.STARTED || state == AsyncState.COMPLETE_PENDING) {
            state = AsyncState.COMPLETING;
            doComplete = true;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncComplete()", state));
        }
        return doComplete;
    }


    public synchronized boolean asyncTimeout() {
        if (state == AsyncState.STARTED) {
            state = AsyncState.TIMING_OUT;
            return true;
        } else if (state == AsyncState.COMPLETING ||
                state == AsyncState.DISPATCHING ||
                state == AsyncState.DISPATCHED) {
            // NOOP - App called complete() or dispatch() between the the
            // timeout firing and execution reaching this point
            return false;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncTimeout()", state));
        }
    }


    public synchronized boolean asyncDispatch() {
        if (!ContainerThreadMarker.isContainerThread() && state == AsyncState.STARTING) {
            state = AsyncState.DISPATCH_PENDING;
            return false;
        } else {
            return doDispatch();
        }
    }


    private synchronized boolean doDispatch() {
        clearNonBlockingListeners();
        boolean doDispatch = false;
        if (state == AsyncState.STARTING ||
                state == AsyncState.TIMING_OUT ||
                state == AsyncState.ERROR) {
            // In these three cases processing is on a container thread so no
            // need to transfer processing to a new container thread
            state = AsyncState.MUST_DISPATCH;
        } else if (state == AsyncState.STARTED || state == AsyncState.DISPATCH_PENDING) {
            state = AsyncState.DISPATCHING;
            // A dispatch is always required.
            // If on a non-container thread, need to get back onto a container
            // thread to complete the processing.
            // If on a container thread the current request/response are not the
            // request/response associated with the AsyncContext so need a new
            // container thread to process the different request/response.
            doDispatch = true;
        } else if (state == AsyncState.READ_WRITE_OP) {
            state = AsyncState.DISPATCHING;
            // If on a container thread then the socket will be added to the
            // poller poller when the thread exits the
            // AbstractConnectionHandler.process() method so don't do a dispatch
            // here which would add it to the poller a second time.
            if (!ContainerThreadMarker.isContainerThread()) {
                doDispatch = true;
            }
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncDispatch()", state));
        }
        return doDispatch;
    }


    public synchronized void asyncDispatched() {
        if (state == AsyncState.DISPATCHING ||
                state == AsyncState.MUST_DISPATCH) {
            state = AsyncState.DISPATCHED;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncDispatched()", state));
        }
    }


    public synchronized void asyncMustError() {
        if (state == AsyncState.STARTED) {
            clearNonBlockingListeners();
            state = AsyncState.MUST_ERROR;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncMustError()", state));
        }
    }


    public synchronized void asyncError() {
        if (state == AsyncState.STARTING ||
                state == AsyncState.STARTED ||
                state == AsyncState.DISPATCHED ||
                state == AsyncState.TIMING_OUT ||
                state == AsyncState.MUST_COMPLETE ||
                state == AsyncState.READ_WRITE_OP ||
                state == AsyncState.COMPLETING ||
                state == AsyncState.MUST_ERROR) {
            clearNonBlockingListeners();
            state = AsyncState.ERROR;
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncError()", state));
        }
    }

    public synchronized void asyncRun(Runnable runnable) {
        if (state == AsyncState.STARTING || state ==  AsyncState.STARTED ||
                state == AsyncState.READ_WRITE_OP) {
            // Execute the runnable using a container thread from the
            // Connector's thread pool. Use a wrapper to prevent a memory leak
            ClassLoader oldCL;
            if (Constants.IS_SECURITY_ENABLED) {
                PrivilegedAction<ClassLoader> pa = new PrivilegedGetTccl();
                oldCL = AccessController.doPrivileged(pa);
            } else {
                oldCL = Thread.currentThread().getContextClassLoader();
            }
            try {
                if (Constants.IS_SECURITY_ENABLED) {
                    PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                            this.getClass().getClassLoader());
                    AccessController.doPrivileged(pa);
                } else {
                    Thread.currentThread().setContextClassLoader(
                            this.getClass().getClassLoader());
                }

                processor.getExecutor().execute(runnable);
            } finally {
                if (Constants.IS_SECURITY_ENABLED) {
                    PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                            oldCL);
                    AccessController.doPrivileged(pa);
                } else {
                    Thread.currentThread().setContextClassLoader(oldCL);
                }
            }
        } else {
            throw new IllegalStateException(
                    sm.getString("asyncStateMachine.invalidAsyncState",
                            "asyncRun()", state));
        }

    }


    public synchronized void recycle() {
        // Ensure in case of error that any non-container threads that have been
        // paused are unpaused.
        notifyAll();
        asyncCtxt = null;
        state = AsyncState.DISPATCHED;
    }


    private void clearNonBlockingListeners() {
        processor.getRequest().listener = null;
        processor.getRequest().getResponse().listener = null;
    }
}
