/*
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/* @test
 * @bug 6979009
 * @summary Ensure ClosedByInterruptException is thrown when I/O operation
 *     interrupted by Thread.interrupt
 */

import java.io.*;
import java.util.Random;
import java.nio.ByteBuffer;
import java.nio.channels.*;

public class ClosedByInterrupt {

    static final int K = 1024;
    static final Random rand = new Random();

    static volatile boolean failed;

    public static void main(String[] args) throws Exception {
        File f = File.createTempFile("blah", null);
        f.deleteOnExit();

        // create 1MB file.
        byte[] b = new byte[K*K];
        rand.nextBytes(b);
        ByteBuffer bb = ByteBuffer.wrap(b);
        try (FileChannel fc = new FileOutputStream(f).getChannel()) {
            while (bb.hasRemaining())
                fc.write(bb);
        }

        // test with 1-16 concurrent threads
        for (int i=1; i<=16; i++) {
            System.out.format("%d thread(s)%n", i);
            test(f, i);
            if (failed)
                break;
        }

        if (failed)
            throw new RuntimeException("Test failed");
    }

    /**
     * Starts "nThreads" that do I/O on the given file concurrently. Continuously
     * interrupts one of the threads to cause the file to be closed and
     * ClosedByInterruptException to be thrown. The other threads should "fail" with
     * ClosedChannelException (or the more specific AsynchronousCloseException).
     */
    static void test(File f, int nThreads) throws Exception {
        try (FileChannel fc = new RandomAccessFile(f, "rwd").getChannel()) {
            Thread[] threads = new Thread[nThreads];

            // start threads
            for (int i=0; i<nThreads; i++) {
                boolean interruptible = (i==0);
                ReaderWriter task = new ReaderWriter(fc, interruptible);
                Thread t = new Thread(task);
                t.start();
                threads[i] = t;
            }

            // give time for threads to start
            Thread.sleep(500 + rand.nextInt(1000));

            // interrupt thread until channel is closed
            while (fc.isOpen()) {
                threads[0].interrupt();
                Thread.sleep(rand.nextInt(50));
            }

            // wait for test to finish
            for (int i=0; i<nThreads; i++) {
                threads[i].join();
            }
        }
    }

    /**
     * A task that continuously reads or writes to random areas of a file
     * until the channel is closed. An "interruptible" task expects the
     * channel to be closed by an interupt, a "non-interruptible" thread
     * does not.
     */
    static class ReaderWriter implements Runnable {
        final FileChannel fc;
        final boolean interruptible;
        final boolean writer;

        ReaderWriter(FileChannel fc, boolean interruptible) {
            this.fc = fc;
            this.interruptible = interruptible;
            this.writer = rand.nextBoolean();
        }

        public void run() {
            ByteBuffer bb = ByteBuffer.allocate(K);
            if (writer)
                rand.nextBytes(bb.array());

            try {
                for (;;) {
                    long position = rand.nextInt(K*K - bb.capacity());
                    if (writer) {
                        bb.position(0).limit(bb.capacity());
                        fc.write(bb, position);
                    } else {
                        bb.clear();
                        fc.read(bb, position);
                    }
                    if (!interruptible) {
                        // give the interruptible thread a chance
                        try {
                            Thread.sleep(rand.nextInt(50));
                        } catch (InterruptedException e) {
                            unexpected(e);
                        }
                    }
                }
            } catch (ClosedByInterruptException e) {
                if (interruptible) {
                    if (Thread.interrupted()) {
                        expected(e + " thrown and interrupt status set");
                    } else {
                        unexpected(e + " thrown but interrupt status not set");
                    }
                } else {
                    unexpected(e);
                }
            } catch (ClosedChannelException e) {
                if (interruptible) {
                    unexpected(e);
                } else {
                    expected(e);
                }
            } catch (Exception e) {
                unexpected(e);
            }
        }
    }

    static void expected(Exception e) {
        System.out.format("%s (expected)%n", e);
    }

    static void expected(String msg) {
        System.out.format("%s (expected)%n", msg);
    }

    static void unexpected(Exception e) {
        System.err.format("%s (not expected)%n", e);
        failed = true;
    }

    static void unexpected(String msg) {
        System.err.println(msg);
        failed = true;
    }
}
