org.jodconverter.office.OfficeProcessManagerPoolEntryITest.java Source code

Java tutorial

Introduction

Here is the source code for org.jodconverter.office.OfficeProcessManagerPoolEntryITest.java

Source

/*
 * Copyright 2004 - 2012 Mirko Nasato and contributors
 *           2016 - 2018 Simon Braconnier and contributors
 *
 * This file is part of JODConverter - Java OpenDocument Converter.
 *
 * Licensed 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.jodconverter.office;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

import java.io.File;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.Test;
import org.slf4j.LoggerFactory;

import com.sun.star.frame.TerminationVetoException;
import com.sun.star.frame.XTerminateListener;
import com.sun.star.lang.EventObject;

import org.jodconverter.process.PureJavaProcessManager;

public class OfficeProcessManagerPoolEntryITest {

    private static final org.slf4j.Logger LOGGER = LoggerFactory
            .getLogger(OfficeProcessManagerPoolEntryITest.class);

    private static final OfficeUrl CONNECT_URL = new OfficeUrl(2002);
    private static final long RESTART_INITIAL_WAIT = 1000; // 1 Second.
    private static final long RESTART_WAIT_TIMEOUT = 10000; // 10 Seconds.

    private static class VetoTerminateListener implements XTerminateListener {

        @Override
        public void disposing(final EventObject event) {
            // Nothing to do here
        }

        @Override
        public void queryTermination(final EventObject event) throws TerminationVetoException {

            // This will prevent a clean termination
            throw new TerminationVetoException();
        }

        @Override
        public void notifyTermination(final EventObject event) {
            // Nothing to do here
        }
    }

    private static OfficeProcess startOfficeProcess(final OfficeUrl officeUrl) throws Exception {

        // Starts an office process
        final OfficeProcess officeProcess = new OfficeProcess(officeUrl);
        officeProcess.start();
        Thread.sleep(1000); // NOSONAR
        final Integer exitCode = officeProcess.getExitCode();
        if (exitCode != null && exitCode.equals(Integer.valueOf(81))) {
            officeProcess.start(true);
            Thread.sleep(1000); // NOSONAR
        }
        return officeProcess;
    }

    private static void assertRestartedAndReconnected(final OfficeProcessManagerPoolEntry officeManager,
            final long initialWait, final long timeout) throws Exception {

        final long start = System.currentTimeMillis();

        if (initialWait > 0) {
            Thread.sleep(initialWait); // NOSONAR
        }

        final long limit = start + timeout;
        while (System.currentTimeMillis() < limit) {
            if (getOfficeProcess(officeManager).isRunning() && getConnection(officeManager).isConnected()) {
                return;
            }

            // Wait a sec
            Thread.sleep(1000); // NOSONAR
        }

        // Times out...
        assertThat(officeManager)
                .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                .containsExactly(true, true);
    }

    private static OfficeProcess getOfficeProcess(final OfficeProcessManagerPoolEntry manager)
            throws IllegalAccessException {

        final OfficeProcessManager processManager = (OfficeProcessManager) FieldUtils.readField(manager,
                "officeProcessManager", true);
        return (OfficeProcess) FieldUtils.readField(processManager, "process", true);
    }

    private static OfficeConnection getConnection(final OfficeProcessManagerPoolEntry manager)
            throws IllegalAccessException {

        final OfficeProcessManager processManager = (OfficeProcessManager) FieldUtils.readField(manager,
                "officeProcessManager", true);
        return (OfficeConnection) FieldUtils.readField(processManager, "connection", true);
    }

    /**
     * Tests the execution of a task.
     *
     * @throws Exception if an error occurs.
     */
    @Test
    public void executeTask() throws Exception {

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL);

        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            final MockOfficeTask task = new MockOfficeTask();
            officeManager.execute(task);
            assertThat(task.isCompleted()).isTrue();

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
        }
    }

    private class RestartAfterCrashTask implements Callable<Boolean> {

        private final OfficeProcessManagerPoolEntry officeManager;

        private RestartAfterCrashTask(final OfficeProcessManagerPoolEntry officeManager) {
            super();

            this.officeManager = officeManager;
        }

        @Override
        public Boolean call() throws Exception {
            final MockOfficeTask badTask = new MockOfficeTask(10 * 1000);

            officeManager.execute(badTask);
            return true;
        }
    }

    /**
     * Tests that an office process is restarted successfully after a crash.
     *
     * @throws Exception if an error occurs.
     */
    @Test
    public void execute_WhenOfficeProcessCrash_ShouldRestartAfterCrash() throws Exception {

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL);

        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            // Submit the task to an executor
            final ExecutorService pool = Executors.newFixedThreadPool(1);
            try {
                final Callable<Boolean> task = new RestartAfterCrashTask(officeManager);
                final Future<Boolean> future = pool.submit(task);

                Thread.sleep(500); // NOSONAR

                // Simulate crash
                final Process underlyingProcess = (Process) FieldUtils.readField(getOfficeProcess(officeManager),
                        "process", true);
                assertThat(underlyingProcess).isNotNull();
                LOGGER.debug("Simulating the crash");
                underlyingProcess.destroy();

                // Wait until the task is completed
                try {
                    future.get();
                    fail("Exception expected");
                } catch (ExecutionException ex) {
                    assertThat(ex.getCause()).isInstanceOf(OfficeException.class);
                    assertThat(ex.getCause().getCause()).isInstanceOf(CancellationException.class);
                }

            } finally {
                pool.shutdownNow();
            }

            assertRestartedAndReconnected(officeManager, RESTART_INITIAL_WAIT, RESTART_WAIT_TIMEOUT);

            final MockOfficeTask goodTask = new MockOfficeTask();
            officeManager.execute(goodTask);
            assertThat(goodTask.isCompleted()).isTrue();

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
        }
    }

    /**
     * Tests that an office process is restarted when the execution of a task times out.
     *
     * @throws Exception if an error occurs.
     */
    @Test
    public void execute_WhenTimeoutExceptionOccured_ShouldRestartAfterTaskTimeout() throws Exception {

        final OfficeProcessManagerPoolEntryConfig config = new OfficeProcessManagerPoolEntryConfig();
        config.setTaskExecutionTimeout(1500L);
        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL, config);

        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            final MockOfficeTask task = new MockOfficeTask(2000);
            try {
                officeManager.execute(task);
                fail("Exception expected");
            } catch (OfficeException officeEx) {
                assertThat(officeEx.getCause()).isInstanceOf(TimeoutException.class);
            }

            assertRestartedAndReconnected(officeManager, RESTART_INITIAL_WAIT, RESTART_WAIT_TIMEOUT);

            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            final MockOfficeTask goodTask = new MockOfficeTask();
            officeManager.execute(goodTask);
            assertThat(goodTask.isCompleted()).isTrue();

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
        }
    }

    /**
     * Tests that an office process is restarted when it reached the maximum number of executed tasks.
     *
     * @throws Exception if an error occurs.
     */
    @Test
    public void execute_WhenMaxTasksPerProcessReached_ShouldRestart() throws Exception {

        final OfficeProcessManagerPoolEntryConfig config = new OfficeProcessManagerPoolEntryConfig();
        config.setMaxTasksPerProcess(3);
        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL, config);

        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            for (int i = 0; i < 3; i++) {
                final MockOfficeTask task = new MockOfficeTask();
                officeManager.execute(task);
                assertThat(task.isCompleted()).isTrue();
                assertThat(officeManager).extracting("taskCount.value").containsExactly(i + 1);
            }

            final MockOfficeTask task = new MockOfficeTask();
            officeManager.execute(task);
            assertThat(task.isCompleted()).isTrue();
            assertThat(officeManager).extracting("taskCount.value").containsExactly(1);

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
        }
    }

    @Test
    public void start_WhenProcessAlreadyExistsAndKillExistingProcessOn_ShouldKillExistingProcess()
            throws Exception {

        // Starts an office process
        final OfficeProcess officeProcess = startOfficeProcess(CONNECT_URL);

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL);
        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);

            officeProcess.forciblyTerminate(1000L, 5000L);
            officeProcess.deleteInstanceProfileDir();
        }
    }

    @Test
    public void start_WhenProcessAlreadyExistsAndKillExistingProcessOff_ShouldKillExistingProcess()
            throws Exception {

        // Starts an office process
        final OfficeProcess officeProcess = startOfficeProcess(CONNECT_URL);

        final OfficeProcessManagerPoolEntryConfig config = new OfficeProcessManagerPoolEntryConfig();
        config.setKillExistingProcess(false);
        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL, config);
        try {
            officeManager.start();
            fail("Exception expected");

        } catch (Exception ex) {

            assertThat(ex).isExactlyInstanceOf(OfficeException.class)
                    .hasMessageContaining("A process with acceptString").hasMessageContaining("is already running");

        } finally {

            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);

            officeProcess.forciblyTerminate(1000L, 5000L);
            officeProcess.deleteInstanceProfileDir();
        }
    }

    @Test
    public void start_WithCustomProfileDir_ShouldCopyProfileDirToWorkingDir() throws Exception {

        final OfficeProcessManagerPoolEntryConfig config = new OfficeProcessManagerPoolEntryConfig();
        config.setOfficeHome(LocalOfficeUtils.getDefaultOfficeHome());
        config.setWorkingDir(new File(System.getProperty("java.io.tmpdir")));
        config.setOfficeHome(LocalOfficeUtils.getDefaultOfficeHome());
        config.setTemplateProfileDir(new File("src/integTest/resources/templateProfileDir"));

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL, config);
        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            // Check the profile dir existence
            final File instanceProfileDir = (File) FieldUtils.readField(getOfficeProcess(officeManager),
                    "instanceProfileDir", true);
            assertThat(new File(instanceProfileDir, "user/customFile")).isFile();

        } finally {

            officeManager.stop();
            assertThat(officeManager.isRunning()).isFalse();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(false, false);
            assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
        }
    }

    @Test
    public void stop_WhenCouldNotTerminateDueToRetryTimeout_ThrowsOfficeException() throws Exception {

        final OfficeProcessManagerPoolEntryConfig config = new OfficeProcessManagerPoolEntryConfig();
        config.setProcessManager(new PureJavaProcessManager() {

            private boolean firstAttempt = true;

            @Override
            public void kill(final Process process, final long pid) {
                if (firstAttempt) {
                    firstAttempt = false;
                    try {
                        Thread.sleep(500); // NOSONAR
                    } catch (InterruptedException e) {
                        // Swallow
                    }
                } else {
                    super.kill(process, pid);
                }
            }
        });

        final VetoTerminateListener terminateListener = new VetoTerminateListener();

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL, config);
        try {
            officeManager.start();
            assertThat(officeManager.isRunning()).isTrue();
            assertThat(officeManager)
                    .extracting("officeProcessManager.process.running", "officeProcessManager.connection.connected")
                    .containsExactly(true, true);

            // Listen to termination and change the timeout
            config.setProcessTimeout(1L);
            getConnection(officeManager).getDesktop().addTerminateListener(terminateListener);

        } finally {

            try {
                officeManager.stop();
                fail("Exception expected");

            } catch (Exception ex) {

                assertThat(ex).isExactlyInstanceOf(OfficeException.class)
                        .hasCauseExactlyInstanceOf(RetryTimeoutException.class);

            } finally {

                // Ensure that after the test, the office instance is terminated.
                config.setProcessTimeout(30000L);
                getConnection(officeManager).getDesktop().removeTerminateListener(terminateListener);

                officeManager.stop();
                assertThat(officeManager.isRunning()).isFalse();
                assertThat(officeManager).extracting("officeProcessManager.process.running",
                        "officeProcessManager.connection.connected").containsExactly(false, false);
                assertThat(getOfficeProcess(officeManager).getExitCode(0, 0)).isEqualTo(0);
            }
        }
    }

    @Test
    public void isRunning_WhenNotStarted_ReturnsFalse() throws Exception {

        final OfficeProcessManagerPoolEntry officeManager = new OfficeProcessManagerPoolEntry(CONNECT_URL);

        assertThat(officeManager.isRunning()).isFalse();
    }
}