/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2015 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.tests.e2e;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;

import javax.inject.Inject;
import javax.inject.Named;

import org.glassfish.jersey.client.ClientAsyncExecutor;
import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
import org.glassfish.jersey.server.ManagedAsync;
import org.glassfish.jersey.server.ManagedAsyncExecutor;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.spi.ExecutorServiceProvider;
import org.glassfish.jersey.test.JerseyTest;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

import jersey.repackaged.com.google.common.collect.Sets;
import jersey.repackaged.com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * {@link org.glassfish.jersey.spi.ExecutorServiceProvider} E2E tests.
 *
 * @author Marek Potociar (marek.potociar at oracle.com)
 */
public class ExecutorServiceProviderTest extends JerseyTest {

    @Path("resource")
    @Produces("text/plain")
    public static class TestResourceA {

        @GET
        public String getSync() {
            return "resource";
        }

        @GET
        @Path("async")
        @ManagedAsync
        public String getAsync() {
            return "async-resource-" + ExecutorServiceProviderTest.getResponseOnThread();
        }
    }

    // A separate resource class for the custom-async sub-path to ensure that
    // the named ExecutorService injection is only performed for the "/resource/custom-async" path.
    @Path("resource")
    @Produces("text/plain")
    public static class TestResourceB {

        @Inject
        @Named("custom")
        ExecutorService executorService;

        @GET
        @Path("custom-async")
        public void getCustomAsync(@Suspended final AsyncResponse asyncResponse) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    asyncResponse.resume("custom-async-resource-" + ExecutorServiceProviderTest.getResponseOnThread());
                }
            });
        }
    }

    static String getResponseOnThread() {
        final String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("async-request-")) {
            return "passed";
        } else {
            return "error - unexpected custom thread name: " + threadName;
        }
    }

    @ClientAsyncExecutor
    @ManagedAsyncExecutor
    @Named("custom")
    public static class CustomExecutorProvider implements ExecutorServiceProvider {

        private final Set<ExecutorService> executors = Sets.newIdentityHashSet();
        private volatile int executorCreationCount = 0;
        private volatile int executorReleaseCount = 0;

        public void reset() {
            for (ExecutorService executor : executors) {
                executor.shutdownNow();
            }

            executors.clear();
            executorCreationCount = 0;
            executorReleaseCount = 0;
        }

        @Override
        public ExecutorService getExecutorService() {
            return new CustomExecutorService();
        }

        @Override
        public void dispose(final ExecutorService executorService) {
            executorService.shutdownNow();
        }

        private class CustomExecutorService implements ExecutorService {

            private final ExecutorService delegate;
            private final AtomicBoolean isCleanedUp;

            public CustomExecutorService() {
                this.isCleanedUp = new AtomicBoolean(false);
                this.delegate = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
                        .setNameFormat("async-request-%d")
                        .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
                        .build());

                executorCreationCount++;
                executors.add(this);
            }

            @Override
            public void shutdown() {
                tryCleanUp();
                delegate.shutdown();
            }

            @Override
            public List<Runnable> shutdownNow() {
                tryCleanUp();
                return delegate.shutdownNow();
            }

            @Override
            public boolean isShutdown() {
                return delegate.isShutdown();
            }

            @Override
            public boolean isTerminated() {
                return delegate.isTerminated();
            }

            @Override
            public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
                return delegate.awaitTermination(timeout, unit);
            }

            @Override
            public <T> Future<T> submit(Callable<T> task) {
                return delegate.submit(task);
            }

            @Override
            public <T> Future<T> submit(Runnable task, T result) {
                return delegate.submit(task, result);
            }

            @Override
            public Future<?> submit(Runnable task) {
                return delegate.submit(task);
            }

            @Override
            public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
                return delegate.invokeAll(tasks);
            }

            @Override
            public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
                    throws InterruptedException {
                return delegate.invokeAll(tasks, timeout, unit);
            }

            @Override
            public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
                return delegate.invokeAny(tasks);
            }

            @Override
            public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
                    throws InterruptedException, ExecutionException, TimeoutException {
                return delegate.invokeAny(tasks, timeout, unit);
            }

            private void tryCleanUp() {
                if (isCleanedUp.compareAndSet(false, true)) {
                    executors.remove(this);
                    executorReleaseCount++;
                }
            }

            @Override
            public void execute(Runnable command) {
                delegate.execute(command);
            }
        }
    }

    private static final CustomExecutorProvider serverExecutorProvider = new CustomExecutorProvider();

    @Override
    protected Application configure() {
        // enable(TestProperties.LOG_TRAFFIC);
        // enable(TestProperties.DUMP_ENTITY);
        return new ResourceConfig(TestResourceA.class, TestResourceB.class).register(serverExecutorProvider);
    }

    /**
     * Reproducer for JERSEY-2205 (client-side).
     *
     * @throws Exception in case of a test error.
     */
    @Test
    public void testCustomClientExecutorsInjectionAndReleasing() throws Exception {
        final CustomExecutorProvider provider = new CustomExecutorProvider();
        Client client = ClientBuilder.newClient().register(provider);

        Response response = client.target(getBaseUri()).path("resource").request().get();

        assertEquals(200, response.getStatus());
        assertEquals("resource", response.readEntity(String.class));

        // no executors should be created or released at this point yet
        assertEquals("Unexpected number of created client executors", 0, provider.executorCreationCount);
        assertEquals("Unexpected number of released client executors", 0, provider.executorReleaseCount);
        assertEquals("Unexpected number of client executors stored in the set.",
                0, provider.executors.size());

        Future<Response> fr = client.target(getBaseUri()).path("resource").request().async().get();
        response = fr.get();

        assertEquals(200, response.getStatus());
        assertEquals("resource", response.readEntity(String.class));

        // single executor should be created but not released at this point yet
        assertEquals("Unexpected number of created client executors", 1, provider.executorCreationCount);
        assertEquals("Unexpected number of released client executors", 0, provider.executorReleaseCount);
        assertEquals("Unexpected number of client executors stored in the set.",
                1, provider.executors.size());

        client.close();

        // the created executor needs to be released by now; no more executors should be created
        assertEquals("Unexpected number of created client executors", 1, provider.executorCreationCount);
        assertEquals("Unexpected number of released client executors", 1, provider.executorReleaseCount);
        assertEquals("Unexpected number of client executors stored in the set.",
                0, provider.executors.size());
    }

    /**
     * Reproducer for JERSEY-2205 (server-side).
     *
     * @throws Exception in case of a test error.
     */
    @Test
    public void testCustomServerExecutorsInjectionAndReleasing() throws Exception {
        // reset server executor statistics to avoid data pollution from other test methods
        serverExecutorProvider.reset();

        Response response = target("resource").request().get();

        assertEquals(200, response.getStatus());
        assertEquals("resource", response.readEntity(String.class));

        // no executors should be created or released at this point yet
        assertEquals("Unexpected number of created server executors", 0, serverExecutorProvider.executorCreationCount);
        assertEquals("Unexpected number of released server executors", 0, serverExecutorProvider.executorReleaseCount);
        assertEquals("Unexpected number of server executors stored in the set.",
                0, serverExecutorProvider.executors.size());

        response = target("resource/async").request().get();

        assertEquals(200, response.getStatus());
        assertEquals("async-resource-passed", response.readEntity(String.class));

        // single executor should be created but not released at this point yet
        assertEquals("Unexpected number of created server executors", 1, serverExecutorProvider.executorCreationCount);
        assertEquals("Unexpected number of released server executors", 0, serverExecutorProvider.executorReleaseCount);
        assertEquals("Unexpected number of server executors stored in the set.",
                1, serverExecutorProvider.executors.size());

        tearDown(); // stopping test container

        // the created executor needs to be released by now; no more executors should be created
        assertEquals("Unexpected number of created server executors", 1, serverExecutorProvider.executorCreationCount);
        assertEquals("Unexpected number of released server executors", 1, serverExecutorProvider.executorReleaseCount);
        assertEquals("Unexpected number of server executors stored in the set.",
                0, serverExecutorProvider.executors.size());

        setUp(); // re-starting test container to ensure proper post-test tearDown.
    }

    /**
     * Test named custom executor injection and release mechanism.
     *
     * @throws Exception in case of a test error.
     */
    @Test
    public void testCustomNamedServerExecutorsInjectionAndReleasing() throws Exception {
        serverExecutorProvider.reset();

        Response response = target("resource/custom-async").request().get();

        assertEquals(200, response.getStatus());
        assertEquals("custom-async-resource-passed", response.readEntity(String.class));

        // single executor should be created but not released at this point yet
        assertEquals("Unexpected number of created server executors", 1, serverExecutorProvider.executorCreationCount);
        assertEquals("Unexpected number of released server executors", 0, serverExecutorProvider.executorReleaseCount);
        assertEquals("Unexpected number of server executors stored in the set.",
                1, serverExecutorProvider.executors.size());

        tearDown(); // stopping test container

        // the created executor needs to be released by now; no more executors should be created
        assertEquals("Unexpected number of created server executors", 1, serverExecutorProvider.executorCreationCount);
        assertEquals("Unexpected number of released server executors", 1, serverExecutorProvider.executorReleaseCount);
        assertEquals("Unexpected number of server executors stored in the set.",
                0, serverExecutorProvider.executors.size());

        setUp(); // re-starting test container to ensure proper post-test tearDown.
    }
}
