dk.ange.octave.exec.OctaveExec.java Source code

Java tutorial

Introduction

Here is the source code for dk.ange.octave.exec.OctaveExec.java

Source

/*
 * Copyright 2007, 2008 Ange Optimization ApS
 *
 * 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.
 */
/**
 * @author Kim Hansen
 */
package dk.ange.octave.exec;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Random;
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 dk.ange.octave.exception.OctaveException;
import dk.ange.octave.exception.OctaveIOException;
import dk.ange.octave.util.NamedThreadFactory;
import dk.ange.octave.util.NoCloseWriter;
import dk.ange.octave.util.ReaderWriterPipeThread;
import dk.ange.octave.util.TeeWriter;

/**
 * The object connecting to the octave process
 */
public final class OctaveExec {

    /**
     * System property where the executable is found
     */
    public static final String PROPERTY_EXECUTABLE = "dk.ange.octave.executable";

    private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
            .getLog(OctaveExec.class);

    private static final String[] CMD_ARRAY = { null, "--no-history", "--no-init-file", "--no-line-editing",
            "--no-site-file", "--silent" };

    private final Process process;

    private final Writer processWriter;

    private final BufferedReader processReader;

    private final ExecutorService executor = Executors.newFixedThreadPool(2,
            new NamedThreadFactory(OctaveExec.class.getSimpleName()));

    private final ReaderWriterPipeThread errorStreamThread;

    private boolean destroyed = false;

    /**
     * Will start the octave process.
     * 
     * @param stdinLog
     *            This writer will capture all that is written to the octave process via stdin, if null the data will
     *            not be captured.
     * @param stderrLog
     *            This writer will capture all that is written from the octave process on stderr, if null the data will
     *            not be captured.
     * @param octaveProgram
     *            This is the path to the octave program, if it is null the program will be found using the system
     *            property 'dk.ange.octave.executable' and if that is not set 'octave' will be assumed to be in the
     *            PATH.
     * @param environment
     *            The environment for the octave process, if null the process will inherit the environment for the
     *            virtual machine.
     * @param workingDir
     *            This will be the working dir for the octave process, if null the process will inherit the working dir
     *            of the current process.
     */
    public OctaveExec(final Writer stdinLog, final Writer stderrLog, final File octaveProgram,
            final String[] environment, final File workingDir) {
        final String[] cmdArray = CMD_ARRAY.clone();
        if (octaveProgram != null) {
            cmdArray[0] = octaveProgram.getPath();
        } else {
            cmdArray[0] = System.getProperty(PROPERTY_EXECUTABLE, "octave");
        }
        try {
            process = Runtime.getRuntime().exec(cmdArray, environment, workingDir);
        } catch (final IOException e) {
            throw new OctaveIOException(e);
        }
        // Connect stderr
        errorStreamThread = ReaderWriterPipeThread.instantiate(new InputStreamReader(process.getErrorStream()),
                stderrLog);
        // Connect stdout
        processReader = new BufferedReader(
                new InputStreamReader(process.getInputStream(), Charset.forName("Latin1")));
        // Connect stdin
        if (stdinLog == null) {
            processWriter = new OutputStreamWriter(process.getOutputStream());
        } else {
            processWriter = new TeeWriter(new NoCloseWriter(stdinLog),
                    new OutputStreamWriter(process.getOutputStream()));
        }
    }

    private final Random random = new Random();

    private String generateSpacer() {
        return "-=+X+=- Octave.java spacer -=+X+=- " + random.nextLong() + " -=+X+=-";
    }

    /**
     * @param input
     * @param output
     */
    public void eval(final WriteFunctor input, final ReadFunctor output) {
        final String spacer = generateSpacer();
        final Future<Void> writerFuture = executor.submit(new OctaveWriterCallable(processWriter, input, spacer));
        final Future<Void> readerFuture = executor.submit(new OctaveReaderCallable(processReader, output, spacer));
        final RuntimeException writerException = getFromFuture(writerFuture);
        if (writerException instanceof CancellationException) {
            log.error("Did not expect writer to be canceled", writerException);
        }
        if (writerException != null) {
            if (writerException instanceof CancellationException) {
                log.error("Did not expect writer to be canceled", writerException);
            }
            readerFuture.cancel(true);
        }
        final RuntimeException readerException = getFromFuture(readerFuture);
        if (writerException != null) {
            throw writerException;
        }
        if (readerException != null) {
            // Only gets here when writerException==null, and in that case we don't expect the reader to be cancelled
            if (readerException instanceof CancellationException) {
                log.error("Did not expect reader to be canceled", writerException);
            }
            throw readerException;
        }
    }

    private RuntimeException getFromFuture(final Future<Void> future) {
        try {
            future.get();
        } catch (final InterruptedException e) {
            final String message = "InterruptedException should not happen";
            log.error(message, e);
            return new RuntimeException(message, e);
        } catch (final ExecutionException e) {
            if (e.getCause() instanceof OctaveException) {
                final OctaveException oe = (OctaveException) e.getCause();
                return reInstantiateException(oe);
            }
            // Can happen when there is an error in a OctaveWriter
            final String message = "ExecutionException should not happen";
            log.error(message, e);
            return new RuntimeException(message, e);
        } catch (final CancellationException e) {
            return e;
        } catch (final RuntimeException e) {
            final String message = "RuntimeException should not happen";
            log.error(message, e);
            return new RuntimeException(message, e);
        }
        return null;
    }

    private OctaveException reInstantiateException(final OctaveException inException) {
        final OctaveException outException;
        try {
            outException = inException.getClass().getConstructor(String.class, Throwable.class)
                    .newInstance(inException.getMessage(), inException);
        } catch (final Exception e) {
            throw new IllegalStateException("Exception should not happen", e);
        }
        if (isDestroyed()) {
            outException.setDestroyed(true);
        }
        return outException;
    }

    private synchronized void setDestroyed(final boolean destroyed) {
        this.destroyed = destroyed;
    }

    private synchronized boolean isDestroyed() {
        return destroyed;
    }

    /**
     * Kill the octave process without remorse
     */
    public void destroy() {
        setDestroyed(true);
        executor.shutdownNow();
        process.destroy();
        errorStreamThread.close();
        try {
            processWriter.close();
        } catch (final IOException e) {
            log.debug("Ignored error from processWriter.close() in OctaveExec.destroy()", e);
        }
    }

    /**
     * Close the octave process in an orderly fashion.
     */
    public void close() {
        try {
            // it is not worth it to rewrite this to use eval() and some specialised Functors
            processWriter.write("exit\n");
            processWriter.close();
            final String read1 = processReader.readLine();
            // Allow a single blank line, exit in octave 3.2 returns that:
            if (read1 != null && !"".equals(read1)) {
                throw new OctaveIOException("Expected a blank line, read '" + read1 + "'");
            }
            final String read2 = processReader.readLine();
            if (read2 != null) {
                throw new OctaveIOException("Expected reader to be at end of stream, read '" + read2 + "'");
            }
            processReader.close();
            errorStreamThread.close();
            final int exitValue;
            try {
                exitValue = process.waitFor();
            } catch (final InterruptedException e) {
                throw new OctaveIOException("Interrupted when waiting for octave process to terminate", e);
            }
            if (exitValue != 0) {
                throw new OctaveIOException("octave process terminated abnormaly, exitValue=" + exitValue);
            }
        } catch (final IOException e) {
            final OctaveIOException octaveException = new OctaveIOException("reader error", e);
            if (isDestroyed()) {
                octaveException.setDestroyed(true);
            }
            throw octaveException;
        } finally {
            executor.shutdown();
        }
    }

    /**
     * @param writer
     *            the new writer to write the error output to
     */
    public void setErrorWriter(final Writer writer) {
        errorStreamThread.setWriter(writer);
    }

}