de.zib.gndms.kit.monitor.GroovyMonitor.java Source code

Java tutorial

Introduction

Here is the source code for de.zib.gndms.kit.monitor.GroovyMonitor.java

Source

package de.zib.gndms.kit.monitor;

/*
 * Copyright 2008-2011 Zuse Institute Berlin (ZIB)
 *
 * 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.
 */

import org.apache.commons.codec.binary.Base64InputStream;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.servlet.http.*;
import java.io.*;
import java.security.Principal;
import java.util.List;

/**
 * Instances represent a multi-way connection point between an output stream (get-request) and
 * input-streams (post-request) of a GroovyMonitorServlet which can be used to execute server-side
 * groovy script code in a stateful manner.
 * <br />
 * 
 * Thread safe.
 *
 * @see GroovyMoniServlet
 *
 * @author  try ste fan pla nti kow zib
 * @version $Id$
 *
 *          User: stepn Date: 19.07.2008 Time: 13:08:00
 */
@SuppressWarnings({ "HardcodedLineSeparator" })
final class GroovyMonitor implements HttpSessionBindingListener, HttpSessionActivationListener {

    /**
     * Describes how code is to be executed by a GroovyMoniSession instance
     *
     */
    enum RunMode {
        REPL(true, true), BATCH(false, true), SCRIPT(false, false), EVAL_SCRIPT(true, false),
        // reserved to denote end of execution
        CLOSE(false, false);

        /**
         * Determined if the result of calls to script.run() be printed on out
         */
        private final boolean evaled;

        /**
         * Determines if a two-way connection is closed after the first post-request or if
         * it is kept-open for multiple requests
         */
        private final boolean looping;

        RunMode(boolean eval, boolean loop) {
            evaled = eval;
            looping = loop;
        }

        boolean isEvaled() {
            return evaled;
        }

        boolean isLooping() {
            return looping;
        }
    }

    @SuppressWarnings({ "FieldCanBeLocal" })
    private static final String INIT_SCRIPT;

    private static final int INIT_SCRIPT_SIZE_BOUND = 1024;

    static {
        StringBuilder builder = new StringBuilder(INIT_SCRIPT_SIZE_BOUND);
        builder.append("ExpandoMetaClass.enableGlobally()\n");
        builder.append("Object.metaClass.out=out\n");
        builder.append("Object.metaClass.err=out\n");
        INIT_SCRIPT = builder.toString();
    }

    /**
     *  Timeout used in out-stream flushing wait-loop
     */
    private static final long WAIT_LOOP_DELAY = 2300L;

    /**
     * The console object that runs the servlet engine that hosts the servlet that uses this
     */
    @Nullable
    private GroovyMoniServer moniServer;

    /**
     * Principal of the get-request that created this instance
     */
    @Nullable
    private Principal principal;

    /**
     * Session-unique token that identifies this instance
     */
    @NotNull
    private final String token;

    /**
     * GroovyShell object that executes code received via incoming post-requests
     */
    @Nullable
    private final GroovyShell shell;

    @NotNull
    private final PrintWriter outWriter;

    @NotNull
    private RunMode runMode;

    /**
     * True iff at least one post-request was executed by this monitor.  It is not relevant
     * whether that execution was succesfull or not.
     *
     */
    private boolean doneOnce;

    @SuppressWarnings({ "FieldHasSetterButNoGetter" })
    @Nullable
    private HttpSession session;

    /**
     *
     * @param theMoniServer the responsible monitor server for this instances' servlet
     * @param thePrincipal that created this monitor
     * @param theToken that is used to identify this monitor in principal's session
     * @param theMode @see RunMode
     * @param args a plain argument string passend on to the GroovyBindingFactory
     * @param theOutWriter output stream of the get-requests's connection 
     * @throws IOException if something is afoul with the stream
     */
    @SuppressWarnings({ "IOResourceOpenedButNotSafelyClosed" })
    GroovyMonitor(@NotNull GroovyMoniServer theMoniServer, @NotNull Principal thePrincipal,
            @NotNull String theToken, @NotNull RunMode theMode, @NotNull String args,
            @NotNull PrintWriter theOutWriter) throws IOException {
        token = theToken;
        moniServer = theMoniServer;
        principal = thePrincipal;
        outWriter = theOutWriter;
        runMode = theMode;
        shell = createShell(args);
    }

    /**
     * @return a newly created shell that writes to outStream, uses bindings created by
     * createBinding() and has already processed the contents of getInitStream()
     *
     * @throws IOException
     * @param args
     */
    @NotNull
    private GroovyShell createShell(@NotNull String args) throws IOException {
        CompilerConfiguration config = new CompilerConfiguration();
        config.setOutput(outWriter);

        final GroovyBindingFactory bindingFactory = getMoniServer().getBindingFactory();
        Binding binding = createBinding(args, bindingFactory);

        GroovyShell theShell = new GroovyShell(binding, config);
        theShell.initializeBinding();

        runInitStream(theShell);
        bindingFactory.initShell(theShell, binding);

        return theShell;
    }

    /**
     * Calls BindingFactory and adds points "out" and "err" properties to outWriter
     *
     * @return new binding
     * @param args
     * @param bindingFactory
     */
    @NotNull
    private synchronized Binding createBinding(@NotNull String args, @NotNull GroovyBindingFactory bindingFactory) {
        Binding binding = bindingFactory.createBinding(getMoniServer(), getPrincipal(), args);
        binding.setVariable("out", outWriter);
        binding.setVariable("err", outWriter);
        binding.setProperty("out", outWriter);
        binding.setProperty("err", outWriter);
        return binding;
    }

    private void runInitStream(@NotNull GroovyShell theShell) throws IOException {
        InputStream theInitStream = null;
        try {
            theInitStream = getInitStream();
            runStream(theShell, "", theInitStream, false);
        } finally {
            if (theInitStream != null)
                theInitStream.close();
        }
    }

    /**
     * The initStream is evaluated by the shell prior to the processing of post-requests.
     *
     * Currently adds "out" and "err" properties to Object.metaClass for easy output from
     * within classes.
     *
     * @return a new init stream
     */
    @NotNull
    @SuppressWarnings({ "HardcodedLineSeparator" })
    private static InputStream getInitStream() {
        try {
            return new ByteArrayInputStream(INIT_SCRIPT.getBytes("utf8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
    * Explicitely sets "out" and "err" properties to outWriter before running the script
    * read from inStream.
    *
    * @param theShell
    * @param args  to the stream/script, bound to "args" property
    * @param inStream
    * @param printEvaled if true, the result object is printed to outWriter via script.println()
    * @return the result of evaluating inStream
    */
    private Object runStream(@NotNull GroovyShell theShell, String args, @NotNull InputStream inStream,
            boolean printEvaled) throws IOException {
        final InputStreamReader streamReader = new InputStreamReader(inStream, "utf8");
        final StringBuilder builder = new StringBuilder(INIT_SCRIPT.length());
        final BufferedReader bufReader = new BufferedReader(streamReader);
        try {
            String line = bufReader.readLine();
            while (line != null) {
                builder.append(line);
                builder.append('\n');
                line = bufReader.readLine();
            }
        } finally {
            bufReader.close();
        }

        final Script script = theShell.parse(builder.toString());

        script.setProperty("out", outWriter);
        script.setProperty("err", outWriter);
        script.setProperty("args", args);

        outWriter.flush();
        final Object result = script.run();
        if (printEvaled)
            script.println(result);
        outWriter.flush();
        return result;
    }

    /**
     * Main wait loop on the output stream. Wakes up every WAIT_LOOP_DELY ms
     * to flush the stream or on notify().
     *
     */
    synchronized void waitLoop() {

        do {
            try {
                wait(WAIT_LOOP_DELAY);
            } catch (InterruptedException e) {
                /* ignored */ }
            outWriter.flush();
        }

        while (isRemainingOpen());
    }

    /**
     * outWriter closing condition: Not in CLOSE mode and either looping or no post-request
     * processed so far
     *
     * @return true, if processing should continue and outWriter may not yet be closed down
     */
    private synchronized boolean isRemainingOpen() {
        return !RunMode.CLOSE.equals(runMode) && (runMode.isLooping() || !isPostDone());
    }

    /**
     * Evaluate the multiparts of a http multipart request as groovy shell script code.
     *
     * @param servletRequest
     * @param args to the script/command (plain string)
     *@param b64 if true, the content is asumed to be encoded in base64 @throws IOException
     */
    synchronized void evalParts(@NotNull HttpServletRequest servletRequest, @NotNull String args, boolean b64)
            throws IOException {
        try {
            if (isRemainingOpen()) {
                boolean isMultipart = ServletFileUpload.isMultipartContent(servletRequest);
                if (isMultipart) {
                    FileItemFactory factory = new DiskFileItemFactory();
                    ServletFileUpload upload = new ServletFileUpload(factory);
                    try {
                        List<FileItem> items = upload.parseRequest(servletRequest);
                        for (FileItem fi : items)
                            handlePart(b64, args, fi);
                    } catch (FileUploadException fue) {
                        throw new RuntimeException(fue);
                    }
                } else
                    throw new RuntimeException("Non-multipart content received");
            } else {
                throw new RuntimeException("Processing of multiple requests on a monitor in " + " mode " + runMode
                        + " is either invalid " + " or monitor was closed down in the middle of processing");
            }
        } finally {
            postDone();
            notifyAll();
        }
    }

    /**
     * Processing of single parts.
     * <br />
     *
     * Converts non-file parts into string streams and handles base64 decoding.
     *
      * @param b64 if true, the content is asumed to be encoded in base64
      * @param args args to the part/script
      * @param part @throws IOException
      */
    private synchronized void handlePart(boolean b64, @NotNull String args, @NotNull FileItem part)
            throws IOException {
        if (!part.isFormField()) {
            InputStream in = part.getInputStream();
            handleStream(b64, args, in);
        } else {
            final String val = part.getString();
            final InputStream valStream = new ByteArrayInputStream(val.getBytes("utf8"));
            try {
                handleStream(b64, args, valStream);
            } finally {
                valStream.close();
            }
        }
    }

    @SuppressWarnings({ "SynchronizationOnLocalVariableOrMethodParameter" })
    private synchronized void handleStream(boolean b64, final @NotNull String args, final @NotNull InputStream val)
            throws IOException {
        final @NotNull GroovyShell theShell = getShell();
        synchronized (theShell) {
            final InputStream decodedStream = getDecodedValStream(b64, val);
            try {
                runStream(theShell, args, decodedStream, runMode.isEvaled());
            } finally {
                decodedStream.close();
                if (val != decodedStream)
                    val.close();
            }
        }
    }

    private static @NotNull InputStream getDecodedValStream(boolean b64, final @NotNull InputStream val)
            throws IOException {
        final @NotNull InputStream val1;
        if (b64) {
            val1 = new Base64InputStream(val);
        } else
            val1 = val;
        return val1;
    }

    synchronized void verifyPrincipal(@NotNull Principal thePrincipal) {
        if (!getPrincipal().equals(thePrincipal))
            throw new SecurityException("Principal mismatch");
    }

    public synchronized void valueBound(HttpSessionBindingEvent event) {
        setSession(event.getSession());
    }

    private synchronized void setSession(@NotNull HttpSession httpSession) {
        if (session != null)
            throw new RuntimeException("Attempt to overwrite monitor session");
        session = httpSession;
    }

    public synchronized void valueUnbound(HttpSessionBindingEvent event) {
        if (event.getSession() == session) {
            session = null;
            final GroovyMoniServer theSerer = getMoniServer();
            theSerer.getBindingFactory().destroyBinding(theSerer, getShell().getContext());
            runMode = RunMode.CLOSE;
            notifyAll();
        }
    }

    public void sessionDidActivate(HttpSessionEvent event) {
        // That may be
    }

    public synchronized void sessionWillPassivate(HttpSessionEvent event) {
        final HttpSession evSession = event.getSession();
        if (evSession != null) {
            evSession.removeAttribute(getToken());
        }
    }

    synchronized boolean isPostDone() {
        return doneOnce;
    }

    synchronized void postDone() {
        doneOnce = true;
    }

    @NotNull
    String getToken() {
        return token;
    }

    @NotNull
    private synchronized Principal getPrincipal() {
        if (principal == null)
            throw new IllegalStateException("null Principal encountered where not allowed");
        return principal;
    }

    @NotNull
    synchronized GroovyShell getShell() {
        if (shell == null)
            throw new IllegalStateException("null shell encountered where not allowed");
        return shell;
    }

    @NotNull
    synchronized RunMode getRunMode() {
        return runMode;
    }

    synchronized void setRunMode(@NotNull RunMode theMode) {
        runMode = theMode;
    }

    @NotNull
    private synchronized GroovyMoniServer getMoniServer() {
        if (moniServer == null)
            throw new IllegalStateException("null moniServer encountered where not allowed");
        return moniServer;
    }

}