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

import java.util.ArrayList;
import java.util.List;

import junit.framework.TestCase;

import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.state.ISMLocking.ReadLock;
import org.apache.jackrabbit.core.state.ISMLocking.WriteLock;
import org.apache.jackrabbit.spi.commons.name.NameConstants;

/**
 * <code>AbstractISMLockingTest</code> contains test cases for the ISMLocking requirements.
 */
public abstract class AbstractISMLockingTest extends TestCase {

    /**
     * The {@link ISMLocking} instance under test.
     */
    protected ISMLocking locking;

    /**
     * Test node state.
     */
    protected NodeState state;

    /**
     * Node references instance targeting {@link #state}.
     */
    protected NodeReferences refs;

    /**
     * List of change logs, each with a different modification for {@link #state}.
     */
    protected List<ChangeLog> logs;

    protected void setUp() throws Exception {
        super.setUp();
        locking = createISMLocking();
        NodeId id = NodeId.randomId();
        state = new NodeState(id, NameConstants.NT_BASE, null, ItemState.STATUS_EXISTING, true);
        refs = new NodeReferences(state.getNodeId());
        logs = new ArrayList<ChangeLog>();
        ChangeLog log = new ChangeLog();
        log.added(state);
        logs.add(log);
        log = new ChangeLog();
        log.deleted(state);
        logs.add(log);
        log = new ChangeLog();
        log.modified(state);
        logs.add(log);
        log = new ChangeLog();
        log.modified(refs);
        logs.add(log);
    }

    /**
     * @return an {@link ISMLocking} instance which is exercised by the tests
     */
    public abstract ISMLocking createISMLocking();

    /**
     * Checks the following requirement: <p/> <i>While a read lock is held for a given item with id
     * <code>I</code> an implementation must ensure that no write lock is issued for a change log that
     * contains a reference to an item with id <code>I</code>. </i>
     *
     * @throws InterruptedException on interruption; this will err the test
     */
    public void testReadBlocksWrite() throws InterruptedException {
        ReadLock rLock = locking.acquireReadLock(state.getId());
        for (ChangeLog changeLog : logs) {
            verifyBlocked(startWriterThread(locking, changeLog));
        }
        rLock.release();
    }

    /**
     * Checks the following requirement: <p/> <i>While a write lock is held for a given change log
     * <code>C</code> an implementation must ensure that no read lock is issued for an item that is contained
     * in <code>C</code>, unless the current thread is the owner of the write lock!</i> <p/> The "unless"
     * clause is tested by {@link #testWriteBlocksRead_notIfSameThread()} test.
     *
     * @throws InterruptedException on interruption; this will err the test
     */
    public void testWriteBlocksRead() throws InterruptedException {
        for (ChangeLog changeLog : logs) {
            WriteLock wLock = locking.acquireWriteLock(changeLog);
            verifyBlocked(startReaderThread(locking, state.getId()));
            wLock.release();
        }
    }

    public void testWriteBlocksRead_notIfSameThread() throws InterruptedException {
        for (final ChangeLog changeLog : logs) {
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        WriteLock wLock = locking.acquireWriteLock(changeLog);
                        locking.acquireReadLock(state.getId()).release();
                        wLock.release();
                    } catch (InterruptedException e) {
                    }
                }
            });
            t.start();
            verifyNotBlocked(t);
        }
    }

    /**
     * Checks the following requirement: <p/> While a write lock is held for a given change log <code>C</code>
     * an implementation must ensure that no write lock is issued for a change log <code>C'</code> that intersects
     * with <code>C</code>. That is both change logs contain a reference to the same item. Please note that an
     * implementation is free to block requests entirely for additional write lock while a write lock is
     * active. It is not a requirement to support concurrent write locks.
     *
     * @throws InterruptedException on interruption; this will err the test
     */
    public void testIntersectingWrites() throws InterruptedException {
        ChangeLog cl = new ChangeLog();
        cl.added(state);
        WriteLock wLock = locking.acquireWriteLock(cl);
        for (ChangeLog changeLog : logs) {
            verifyBlocked(startWriterThread(locking, changeLog));
        }
        wLock.release();
    }

    /**
     * Checks if a downgraded write lock allows other threads to read again.
     *
     * @throws InterruptedException on interruption; this will err the test
     */
    public void testDowngrade() throws InterruptedException {
        for (ChangeLog changeLog : logs) {
            WriteLock wLock = locking.acquireWriteLock(changeLog);
            verifyBlocked(startReaderThread(locking, state.getId()));
            ReadLock rLock = wLock.downgrade();
            verifyNotBlocked(startReaderThread(locking, state.getId()));
            rLock.release();
        }
    }

    // ------------------------------< utilities >-------------------------------

    /**
     * Creates and starts a thread that acquires and releases the write lock of the given
     * <code>ISMLocking</code> for the given changelog. The thread's interrupted status is set if it was
     * interrupted during the acquire-release sequence and could therefore not finish it.
     *
     * @param lock the <code>ISMLocking</code> to use
     * @param changeLog the <code>ChangeLog</code> to use
     * @return a thread that has been started
     */
    protected final Thread startWriterThread(final ISMLocking lock, final ChangeLog changeLog) {
        Thread t = new Thread(new Runnable() {

            public void run() {
                try {
                    lock.acquireWriteLock(changeLog).release();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        t.start();
        return t;
    }

    /**
     * Creates and starts an thread that acquires and releases the read lock of the given
     * <code>ISMLocking</code> for the given id. The thread's interrupted status is set if it was interrupted
     * during the acquire-release sequence and could therefore not finish it.
     *
     * @param lock the <code>ISMLocking</code> to use
     * @param id the id to use
     * @return a thread that has been started
     */
    protected final Thread startReaderThread(final ISMLocking lock, final ItemId id) {
        Thread t = new Thread(new Runnable() {

            public void run() {
                try {
                    lock.acquireReadLock(id).release();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        t.start();
        return t;
    }

    /**
     * Verifies that the given thread is blocked. Then it interrupts the thread and waits a certain amount of
     * time for it to complete. (If it doesn't complete within that time then the test that calls this method
     * fails).
     *
     * @param other a started thread
     * @throws InterruptedException on interruption
     */
    protected final void verifyBlocked(final Thread other) throws InterruptedException {
        Thread.sleep(100);
        assertTrue(other.isAlive());
        other.interrupt();
        other.join(100);
        assertFalse(other.isAlive());
    }

    /**
     * Verifies that the given thread is not blocked and runs to completion within a certain amount of time
     * and without interruption. (If it doesn't complete within that time without interruption then the test
     * that calls this method fails).
     *
     * @param other a started thread
     * @throws InterruptedException on interruption
     */
    protected final void verifyNotBlocked(final Thread other) throws InterruptedException {
        other.join(1000);
        assertFalse(other.isInterrupted());
        assertFalse(other.isAlive());
    }
}
