org.taverna.server.localworker.impl.LocalWorker.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.localworker.impl.LocalWorker.java

Source

/*
 * Copyright (C) 2010-2012 The University of Manchester
 * 
 * See the file "LICENSE.txt" for license terms.
 */
package org.taverna.server.localworker.impl;

import static java.lang.Runtime.getRuntime;
import static java.lang.System.getProperty;
import static java.lang.System.out;
import static java.nio.charset.Charset.defaultCharset;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.UUID.randomUUID;
import static org.apache.commons.io.FileUtils.forceDelete;
import static org.apache.commons.io.FileUtils.forceMkdir;
import static org.apache.commons.io.FileUtils.writeByteArrayToFile;
import static org.apache.commons.io.FileUtils.writeLines;
import static org.taverna.server.localworker.impl.SecurityConstants.HELIO_TOKEN_NAME;
import static org.taverna.server.localworker.impl.SecurityConstants.KEYSTORE_FILE;
import static org.taverna.server.localworker.impl.SecurityConstants.SECURITY_DIR_NAME;
import static org.taverna.server.localworker.impl.SecurityConstants.TRUSTSTORE_FILE;
import static org.taverna.server.localworker.impl.utils.FilenameVerifier.getValidatedFile;
import static org.taverna.server.localworker.remote.RemoteStatus.Finished;
import static org.taverna.server.localworker.remote.RemoteStatus.Initialized;
import static org.taverna.server.localworker.remote.RemoteStatus.Operating;
import static org.taverna.server.localworker.remote.RemoteStatus.Stopped;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

import org.taverna.server.localworker.remote.IllegalStateTransitionException;
import org.taverna.server.localworker.remote.ImplementationException;
import org.taverna.server.localworker.remote.RemoteDirectory;
import org.taverna.server.localworker.remote.RemoteInput;
import org.taverna.server.localworker.remote.RemoteListener;
import org.taverna.server.localworker.remote.RemoteSecurityContext;
import org.taverna.server.localworker.remote.RemoteSingleRun;
import org.taverna.server.localworker.remote.RemoteStatus;
import org.taverna.server.localworker.server.UsageRecordReceiver;

import edu.umd.cs.findbugs.annotations.SuppressWarnings;

/**
 * This class implements one side of the connection between the Taverna Server
 * master server and this process. It delegates to a {@link Worker} instance the
 * handling of actually running a workflow.
 * 
 * @author Donal Fellows
 * @see DirectoryDelegate
 * @see FileDelegate
 * @see WorkerCore
 */
@SuppressWarnings({ "SE_BAD_FIELD", "SE_NO_SERIALVERSIONID" })
public class LocalWorker extends UnicastRemoteObject implements RemoteSingleRun {
    // ----------------------- CONSTANTS -----------------------

    /**
     * Subdirectories of the working directory to create by default.
     */
    private static final String[] dirstomake = { "conf", "externaltool", "lib", "logs", "plugins", "repository",
            "t2-database", "var" };

    /** The name of the default encoding for characters on this machine. */
    public static final String SYSTEM_ENCODING = defaultCharset().name();

    /** Handle to the directory containing the security info. */
    static final File SECURITY_DIR;
    static {
        File home = new File(getProperty("user.home"));
        // If we can't write to $HOME (i.e., we're in an odd deployment) use
        // the official version of /tmp/$PID as a fallback.
        if (!home.canWrite())
            home = new File(getProperty("java.io.tmpdir"), ManagementFactory.getRuntimeMXBean().getName());
        SECURITY_DIR = new File(home, SECURITY_DIR_NAME);
    }

    // ----------------------- VARIABLES -----------------------

    /**
     * Magic flag used to turn off problematic code when testing inside CI
     * environment.
     */
    static boolean DO_MKDIR = true;

    /** What to use to run a workflow engine. */
    private final String executeWorkflowCommand;
    /** What workflow to run. */
    private final String workflow;
    /** The remote access object for the working directory. */
    private final DirectoryDelegate baseDir;
    /** What inputs to pass as files. */
    final Map<String, String> inputFiles;
    /** What inputs to pass as files (as file refs). */
    final Map<String, File> inputRealFiles;
    /** What inputs to pass as direct values. */
    final Map<String, String> inputValues;
    /** The interface to the workflow engine subprocess. */
    private final Worker core;
    /** Our descriptor token (UUID). */
    private final String masterToken;
    /**
     * The root working directory for a workflow run, or <tt>null</tt> if it has
     * been deleted.
     */
    private File base;
    /**
     * When did this workflow start running, or <tt>null</tt> for
     * "never/not yet".
     */
    private Date start;
    /**
     * When did this workflow finish running, or <tt>null</tt> for
     * "never/not yet".
     */
    private Date finish;
    /** The cached status of the workflow run. */
    RemoteStatus status;
    /**
     * The name of the input Baclava document, or <tt>null</tt> to not do it
     * that way.
     */
    String inputBaclava;
    /**
     * The name of the output Baclava document, or <tt>null</tt> to not do it
     * that way.
     */
    String outputBaclava;
    /**
     * The file containing the input Baclava document, or <tt>null</tt> to not
     * do it that way.
     */
    private File inputBaclavaFile;
    /**
     * The file containing the output Baclava document, or <tt>null</tt> to not
     * do it that way.
     */
    private File outputBaclavaFile;
    /**
     * Registered shutdown hook so that we clean up when this process is killed
     * off, or <tt>null</tt> if that is no longer necessary.
     */
    Thread shutdownHook;
    /** Location for security information to be written to. */
    File securityDirectory;
    /**
     * Password to use to encrypt security information. This default is <7 chars
     * to work even without Unlimited Strength JCE.
     */
    char[] keystorePassword = new char[] { 'c', 'h', 'a', 'n', 'g', 'e' };
    /** Additional server-specified environment settings. */
    Map<String, String> environment = new HashMap<String, String>();

    // ----------------------- METHODS -----------------------

    /**
     * @param executeWorkflowCommand
     *            The script used to execute workflows.
     * @param workflow
     *            The workflow to execute.
     * @param workerClass
     *            The class to instantiate as our local representative of the
     *            run.
     * @param urReceiver
     *            The remote class to report the generated usage record(s) to.
     * @param id
     *            The UUID to use, or <tt>null</tt> if we are to invent one.
     * @throws RemoteException
     *             If registration of the worker fails.
     * @throws ImplementationException
     *             If something goes wrong during local setup.
     */
    protected LocalWorker(String executeWorkflowCommand, String workflow, Class<? extends Worker> workerClass,
            UsageRecordReceiver urReceiver, UUID id) throws RemoteException, ImplementationException {
        super();
        if (id == null)
            id = randomUUID();
        masterToken = id.toString();
        this.workflow = workflow;
        this.executeWorkflowCommand = executeWorkflowCommand;
        base = new File(getProperty("java.io.tmpdir"), masterToken);
        out.println("about to create " + base);
        try {
            forceMkdir(base);
            for (String subdir : dirstomake) {
                new File(base, subdir).mkdir();
            }
        } catch (IOException e) {
            throw new ImplementationException("problem creating run working directory", e);
        }
        baseDir = new DirectoryDelegate(base, null);
        inputFiles = new HashMap<String, String>();
        inputRealFiles = new HashMap<String, File>();
        inputValues = new HashMap<String, String>();
        try {
            core = workerClass.newInstance();
        } catch (Exception e) {
            out.println("problem when creating core worker implementation");
            e.printStackTrace(out);
            throw new ImplementationException("problem when creating core worker implementation", e);
        }
        core.setURReceiver(urReceiver);
        Thread t = new Thread(new Runnable() {
            /**
             * Kill off the worker launched by the core.
             */
            @Override
            public void run() {
                try {
                    shutdownHook = null;
                    destroy();
                } catch (ImplementationException e) {
                } catch (RemoteException e) {
                }
            }
        });
        getRuntime().addShutdownHook(t);
        shutdownHook = t;
        status = Initialized;
    }

    @Override
    public void destroy() throws RemoteException, ImplementationException {
        if (status != Finished && status != Initialized)
            try {
                core.killWorker();
                if (finish == null)
                    finish = new Date();
            } catch (Exception e) {
                out.println("problem when killing worker");
                e.printStackTrace(out);
            }
        try {
            if (shutdownHook != null)
                getRuntime().removeShutdownHook(shutdownHook);
        } catch (RuntimeException e) {
            throw new ImplementationException("problem removing shutdownHook", e);
        } finally {
            shutdownHook = null;
        }
        // Is this it?
        try {
            if (base != null)
                forceDelete(base);
        } catch (IOException e) {
            out.println("problem deleting working directory");
            e.printStackTrace(out);
            throw new ImplementationException("problem deleting working directory", e);
        } finally {
            base = null;
        }
        try {
            if (securityDirectory != null)
                forceDelete(securityDirectory);
        } catch (IOException e) {
            out.println("problem deleting security directory");
            e.printStackTrace(out);
            throw new ImplementationException("problem deleting security directory", e);
        } finally {
            securityDirectory = null;
        }
    }

    @Override
    public void addListener(RemoteListener listener) throws RemoteException, ImplementationException {
        throw new ImplementationException("not implemented");
    }

    @Override
    public String getInputBaclavaFile() {
        return inputBaclava;
    }

    @Override
    public List<RemoteInput> getInputs() throws RemoteException {
        ArrayList<RemoteInput> result = new ArrayList<RemoteInput>();
        for (String name : inputFiles.keySet())
            result.add(new InputDelegate(name));
        return result;
    }

    @Override
    public List<String> getListenerTypes() {
        return emptyList();
    }

    @Override
    public List<RemoteListener> getListeners() {
        return singletonList(core.getDefaultListener());
    }

    @Override
    public String getOutputBaclavaFile() {
        return outputBaclava;
    }

    @SuppressWarnings("SE_INNER_CLASS")
    class SecurityDelegate extends UnicastRemoteObject implements RemoteSecurityContext {
        private void setPrivatePerms(File dir) {
            if (!dir.setReadable(false, false) || !dir.setReadable(true, true) || !dir.setExecutable(false, false)
                    || !dir.setExecutable(true, true) || !dir.setWritable(false, false)
                    || !dir.setWritable(true, true)) {
                out.println("warning: " + "failed to set permissions on security context directory");
            }
        }

        protected SecurityDelegate(String token) throws IOException {
            super();
            if (DO_MKDIR) {
                securityDirectory = new File(SECURITY_DIR, token);
                forceMkdir(securityDirectory);
                setPrivatePerms(securityDirectory);
            }
        }

        /**
         * Write some data to a given file in the context directory.
         * 
         * @param name
         *            The name of the file to write.
         * @param data
         *            The bytes to put in the file.
         * @throws RemoteException
         *             If anything goes wrong.
         * @throws ImplementationException
         */
        protected void write(String name, byte[] data) throws RemoteException, ImplementationException {
            try {
                File f = new File(securityDirectory, name);
                writeByteArrayToFile(f, data);
            } catch (IOException e) {
                throw new ImplementationException("problem writing " + name, e);
            }
        }

        /**
         * Write some data to a given file in the context directory.
         * 
         * @param name
         *            The name of the file to write.
         * @param data
         *            The lines to put in the file. The
         *            {@linkplain LocalWorker#SYSTEM_ENCODING system encoding}
         *            will be used to do the writing.
         * @throws RemoteException
         *             If anything goes wrong.
         * @throws ImplementationException
         */
        protected void write(String name, Collection<String> data) throws RemoteException, ImplementationException {
            try {
                File f = new File(securityDirectory, name);
                writeLines(f, SYSTEM_ENCODING, data);
            } catch (IOException e) {
                throw new ImplementationException("problem writing " + name, e);
            }
        }

        /**
         * Write some data to a given file in the context directory.
         * 
         * @param name
         *            The name of the file to write.
         * @param data
         *            The line to put in the file. The
         *            {@linkplain LocalWorker#SYSTEM_ENCODING system encoding}
         *            will be used to do the writing.
         * @throws RemoteException
         *             If anything goes wrong.
         * @throws ImplementationException
         */
        protected void write(String name, char[] data) throws RemoteException, ImplementationException {
            try {
                File f = new File(securityDirectory, name);
                writeLines(f, SYSTEM_ENCODING, asList(new String(data)));
            } catch (IOException e) {
                throw new ImplementationException("problem writing " + name, e);
            }
        }

        @Override
        public void setKeystore(byte[] keystore) throws RemoteException, ImplementationException {
            if (status != Initialized)
                throw new RemoteException("not initializing");
            if (keystore == null)
                throw new IllegalArgumentException("keystore may not be null");
            write(KEYSTORE_FILE, keystore);
        }

        @Override
        public void setPassword(char[] password) throws RemoteException {
            if (status != Initialized)
                throw new RemoteException("not initializing");
            if (password == null)
                throw new IllegalArgumentException("password may not be null");
            keystorePassword = password.clone();
        }

        @Override
        public void setTruststore(byte[] truststore) throws RemoteException, ImplementationException {
            if (status != Initialized)
                throw new RemoteException("not initializing");
            if (truststore == null)
                throw new IllegalArgumentException("truststore may not be null");
            write(TRUSTSTORE_FILE, truststore);
        }

        @Override
        public void setUriToAliasMap(HashMap<URI, String> uriToAliasMap) throws RemoteException {
            if (status != Initialized)
                throw new RemoteException("not initializing");
            if (uriToAliasMap == null)
                return;
            ArrayList<String> lines = new ArrayList<String>();
            for (Entry<URI, String> site : uriToAliasMap.entrySet())
                lines.add(site.getKey().toASCIIString() + " " + site.getValue());
            // write(URI_ALIAS_MAP, lines);
        }

        @Override
        public void setHelioToken(String helioToken) throws RemoteException {
            if (status != Initialized)
                throw new RemoteException("not initializing");
            out.println("registering HELIO CIS token for export");
            environment.put(HELIO_TOKEN_NAME, helioToken);
        }
    }

    @Override
    public RemoteSecurityContext getSecurityContext() throws RemoteException, ImplementationException {
        try {
            return new SecurityDelegate(masterToken);
        } catch (RemoteException e) {
            if (e.getCause() != null)
                throw new ImplementationException("problem initializing security context", e.getCause());
            throw e;
        } catch (IOException e) {
            throw new ImplementationException("problem initializing security context", e);
        }
    }

    @Override
    public RemoteStatus getStatus() {
        // only state that can spontaneously change to another
        if (status == Operating) {
            status = core.getWorkerStatus();
            if (status == Finished && finish == null)
                finish = new Date();
        }
        return status;
    }

    @Override
    public RemoteDirectory getWorkingDirectory() {
        return baseDir;
    }

    File validateFilename(String filename) throws RemoteException {
        if (filename == null)
            throw new IllegalArgumentException("filename must be non-null");
        try {
            return getValidatedFile(base, filename.split("/"));
        } catch (IOException e) {
            throw new IllegalArgumentException("failed to validate filename", e);
        }
    }

    @SuppressWarnings("SE_INNER_CLASS")
    class InputDelegate extends UnicastRemoteObject implements RemoteInput {
        private String name;

        InputDelegate(String name) throws RemoteException {
            super();
            this.name = name;
            if (!inputFiles.containsKey(name)) {
                if (status != Initialized)
                    throw new IllegalStateException("not initializing");
                inputFiles.put(name, null);
                inputRealFiles.put(name, null);
                inputValues.put(name, null);
            }
        }

        @Override
        public String getFile() {
            return inputFiles.get(name);
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getValue() {
            return inputValues.get(name);
        }

        @Override
        public void setFile(String file) throws RemoteException {
            if (status != Initialized)
                throw new IllegalStateException("not initializing");
            inputRealFiles.put(name, validateFilename(file));
            inputValues.put(name, null);
            inputFiles.put(name, file);
            inputBaclava = null;
        }

        @Override
        public void setValue(String value) throws RemoteException {
            if (status != Initialized)
                throw new IllegalStateException("not initializing");
            inputValues.put(name, value);
            inputFiles.put(name, null);
            inputRealFiles.put(name, null);
            inputBaclava = null;
        }
    }

    @Override
    public RemoteInput makeInput(String name) throws RemoteException {
        return new InputDelegate(name);
    }

    @Override
    public RemoteListener makeListener(String type, String configuration) throws RemoteException {
        throw new RemoteException("listener manufacturing unsupported");
    }

    @Override
    public void setInputBaclavaFile(String filename) throws RemoteException {
        if (status != Initialized)
            throw new IllegalStateException("not initializing");
        inputBaclavaFile = validateFilename(filename);
        for (String input : inputFiles.keySet()) {
            inputFiles.put(input, null);
            inputRealFiles.put(input, null);
            inputValues.put(input, null);
        }
        inputBaclava = filename;
    }

    @Override
    public void setOutputBaclavaFile(String filename) throws RemoteException {
        if (status != Initialized)
            throw new IllegalStateException("not initializing");
        if (filename != null)
            outputBaclavaFile = validateFilename(filename);
        else
            outputBaclavaFile = null;
        outputBaclava = filename;
    }

    @Override
    public void setStatus(RemoteStatus newStatus)
            throws IllegalStateTransitionException, RemoteException, ImplementationException {
        if (status == newStatus)
            return;

        switch (newStatus) {
        case Initialized:
            throw new IllegalStateTransitionException("may not move back to start");
        case Operating:
            switch (status) {
            case Initialized:
                try {
                    start = new Date();
                    core.initWorker(executeWorkflowCommand, workflow, base, inputBaclavaFile, inputRealFiles,
                            inputValues, outputBaclavaFile, securityDirectory, keystorePassword, environment,
                            masterToken);
                    /*
                     * Do not clear the keystorePassword array here; its
                     * ownership is *transferred* to the worker core which
                     * doesn't copy it but *does* clear it after use.
                     */
                    keystorePassword = null;
                } catch (Exception e) {
                    throw new ImplementationException("problem creating executing workflow", e);
                }
                break;
            case Stopped:
                try {
                    core.startWorker();
                } catch (Exception e) {
                    throw new ImplementationException("problem starting workflow run", e);
                }
                break;
            case Finished:
                throw new IllegalStateTransitionException("already finished");
            }
            status = Operating;
            break;
        case Stopped:
            switch (status) {
            case Initialized:
                throw new IllegalStateTransitionException("may only stop from Operating");
            case Operating:
                try {
                    core.stopWorker();
                } catch (Exception e) {
                    throw new ImplementationException("problem stopping workflow run", e);
                }
                break;
            case Finished:
                throw new IllegalStateTransitionException("already finished");
            }
            status = Stopped;
            break;
        case Finished:
            switch (status) {
            case Operating:
            case Stopped:
                try {
                    core.killWorker();
                    if (finish == null)
                        finish = new Date();
                } catch (Exception e) {
                    throw new ImplementationException("problem killing workflow run", e);
                }
                break;
            }
            status = Finished;
            break;
        }
    }

    @Override
    public Date getFinishTimestamp() {
        return finish == null ? null : new Date(finish.getTime());
    }

    @Override
    public Date getStartTimestamp() {
        return start == null ? null : new Date(start.getTime());
    }
}