/*
 * 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.jackrabbit.core.session;

import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.jackrabbit.core.WorkspaceManager;
import org.apache.jackrabbit.core.observation.ObservationDispatcher;
import org.apache.jackrabbit.core.stats.RepositoryStatisticsImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Internal session state. This class keeps track of the lifecycle of
 * a session and controls concurrent access to the session internals.
 * <p>
 * The session lifecycle is pretty simple: there are only two lifecycle
 * states, "alive" and "closed", and only one possible state transition,
 * from "alive" to "closed".
 * <p>
 * Concurrent access to session internals is controlled by the
 * {@link #perform(SessionOperation)} method that guarantees that no two
 * {@link SessionOperation operations} are performed concurrently on the
 * same session.
 *
 * @see <a href="https://issues.apache.org/jira/browse/JCR-890">JCR-890</a>
 */
public class SessionState {

    /**
     * Logger instance.
     */
    private static final Logger log =
        LoggerFactory.getLogger(SessionState.class);

    /**
     * Component context of this session.
     */
    private final SessionContext context;

    /**
     * Counter of read operations.
     */
    private final AtomicLong readCounter;

    /**
     * Counter of write operations.
     */
    private final AtomicLong writeCounter;

    /**
     * Duration of read operations.
     */
    private final AtomicLong readDuration;

    /**
     * Duration of write operations.
     */
    private final AtomicLong writeDuration;
    
    /**
     * Number of open sessions.
     */
    private final AtomicLong sessionCount;

    /**
     * The lock used to guarantee synchronized execution of repository
     * operations. An explicit lock is used instead of normal Java
     * synchronization in order to be able to log attempts to concurrently
     * use a session.
     */
    private final Lock lock = new ReentrantLock();

    /**
     * Flag to indicate that the current operation is a write operation.
     * Used to detect concurrent writes.
     */
    private volatile boolean isWriteOperation = false;

    /**
     * Flag to indicate a closed session. When <code>null</code>, the session
     * is still alive. And when the session is closed, this reference is set
     * to an exception that contains the stack trace of where the session was
     * closed. The stack trace is used as extra information in possible
     * warning logs caused by clients attempting to access a closed session.
     */
    private volatile Exception closed = null;

    /**
     * Creates a state instance for a session.
     *
     * @param context component context of this session
     */
    public SessionState(SessionContext context) {
        this.context = context;

        RepositoryStatisticsImpl statistics =
                context.getRepositoryContext().getRepositoryStatistics();
        this.readCounter = statistics.getCounter(Type.SESSION_READ_COUNTER);
        this.writeCounter = statistics.getCounter(Type.SESSION_WRITE_COUNTER);
        this.readDuration = statistics.getCounter(Type.SESSION_READ_DURATION);
        this.writeDuration = statistics.getCounter(Type.SESSION_WRITE_DURATION);
        this.sessionCount = statistics.getCounter(Type.SESSION_COUNT);
        statistics.getCounter(Type.SESSION_LOGIN_COUNTER).incrementAndGet();
        sessionCount.incrementAndGet();
    }

    /**
     * Checks whether this session is alive. This method should generally
     * only be called from within a performed {@link SessionOperation}, as
     * otherwise there's no guarantee against another thread closing the
     * session right after this method has returned.
     *
     * @see Session#isLive()
     * @return <code>true</code> if the session is alive,
     *         <code>false</code> otherwise
     */
    public boolean isAlive() {
        return closed == null;
    }

    /**
     * Throws an exception if this session is not alive.
     *
     * @throws RepositoryException throw if this session is not alive
     */
    public void checkAlive() throws RepositoryException {
        if (!isAlive()) {
            throw new RepositoryException(
                    "This session has been closed. See the chained exception"
                    + " for a trace of where the session was closed.", closed);
        }
    }

    /**
     * Performs the given operation within a synchronized block. Special care
     * is made to detect attempts to access the session concurrently and to
     * log detailed warnings in such cases.
     *
     * @param operation session operation
     * @return the return value of the executed operation
     * @throws RepositoryException if the operation fails or
     *                             if the session has already been closed
     */
    public <T> T perform(SessionOperation<T> operation)
            throws RepositoryException {
        String session = context.getSessionImpl().toString();

        // Acquire the exclusive lock for accessing session internals.
        // No other session should be holding the lock, so we log a
        // message to let the user know of such cases.
        if (!lock.tryLock()) {
            if (isWriteOperation
                    && operation instanceof SessionWriteOperation) {
                Exception trace = new Exception(
                        "Stack trace of concurrent access to " + session);
                log.warn("Attempt to perform " + operation
                        + " while another thread is concurrently writing"
                        + " to " + session + ". Blocking until the other"
                        + " thread is finished using this session. Please"
                        + " review your code to avoid concurrent use of"
                        + " a session.", trace);
            } else if (log.isDebugEnabled()) {
                Exception trace = new Exception(
                        "Stack trace of concurrent access to " + session);
                log.debug("Attempt to perform " + operation + " while"
                        + " another thread is concurrently reading from "
                        + session + ". Blocking until the other thread"
                        + " is finished using this session. Please"
                        + " review your code to avoid concurrent use of"
                        + " a session.", trace);
            }
            lock.lock();
        }

        boolean isOutermostWriteOperation = false;
        try {
            // Check that the session is still alive
            checkAlive();

            // Raise the isWriteOperation flag for write operations.
            // The flag is used to set the appropriate log level above.
            boolean wasWriteOperation = isWriteOperation;
            if (!wasWriteOperation
                    && operation instanceof SessionWriteOperation) {
                isWriteOperation = true;
                isOutermostWriteOperation = true;
            }

            try {
                // Perform the actual operation
                log.debug("Performing {}", operation);
                long start = System.nanoTime();
                try {
                    return operation.perform(context);
                } finally {
                    long time = System.nanoTime() - start;

                    // JCR-3040: Increment the operation counters
                    if (isWriteOperation) {
                        writeCounter.incrementAndGet();
                        writeDuration.addAndGet(time);
                    } else {
                        readCounter.incrementAndGet();
                        readDuration.addAndGet(time);
                    }

                    log.debug("Performed {} in {}ns", operation, time);
                }
            } finally {
                isWriteOperation = wasWriteOperation;
            }
        } finally {
            lock.unlock();

            // Delay return from a write operation if the observation queue
            // is being overloaded. This needs to be done after releasing
            // the (outermost) write locks to prevent potential deadlocks.
            // See https://issues.apache.org/jira/browse/JCR-2746
            if (isOutermostWriteOperation) {
                WorkspaceManager manager =
                    context.getRepositoryContext().getWorkspaceManager();
                ObservationDispatcher dispatcher =
                    manager.getObservationDispatcher(context.getWorkspace().getName());
                dispatcher.delayIfEventQueueOverloaded();
            }
        }
    }

    /**
     * Closes this session.  Special care is made to detect attempts to
     * access the session concurrently or to close it more than once, and to
     * log detailed warnings in such cases.
     *
     * @return <code>true</code> if the session was closed, or
     *         <code>false</code> if the session had already been closed
     */
    public boolean close() {
        String session = context.getSessionImpl().toString();

        if (!lock.tryLock()) {
            Exception trace = new Exception(
                    "Stack trace of concurrent access to " + session);
            log.warn("Attempt to close " + session + " while another"
                    + " thread is concurrently accessing this session."
                    + " Blocking until the other thread is finished"
                    + " using this session. Please review your code"
                    + " to avoid concurrent use of a session.", trace);
            lock.lock();
        }
        try {
            if (isAlive()) {
                sessionCount.decrementAndGet();
                closed = new Exception(
                        "Stack trace of  where " + session
                        + " was originally closed");
                return true;
            } else {
                Exception trace = new Exception(
                        "Stack trace of the duplicate attempt to close "
                        + session);
                log.warn("Attempt to close " + session + " after it has"
                        + " already been closed. Please review your code"
                        + " for proper session management.", trace);
                log.warn(session + " has already been closed. See the"
                        + " attached exception for a trace of where this"
                        + " session was closed.", closed);
                return false;
            }
        } finally {
            lock.unlock();
        }
    }

}
