com.galeoconsulting.leonardinius.rest.service.ScriptRunner.java Source code

Java tutorial

Introduction

Here is the source code for com.galeoconsulting.leonardinius.rest.service.ScriptRunner.java

Source

/*
 * Copyright 2011 Leonid Maslov<leonidms@gmail.com>
 *
 * 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 com.galeoconsulting.leonardinius.rest.service;

import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.user.UserManager;
import com.galeoconsulting.leonardinius.api.LanguageUtils;
import com.galeoconsulting.leonardinius.api.ScriptService;
import com.galeoconsulting.leonardinius.api.ScriptSessionManager;
import com.galeoconsulting.leonardinius.rest.CacheControl;
import com.galeoconsulting.leonardinius.rest.ErrorCollection;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.sun.jersey.api.uri.UriBuilderImpl;
import org.apache.commons.io.input.NullReader;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;

import javax.annotation.Nullable;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.galeoconsulting.leonardinius.api.ScriptSessionManager.ScriptSession;
import static com.galeoconsulting.leonardinius.api.ScriptSessionManager.SessionId;
import static com.google.common.base.Preconditions.checkNotNull;

@Path("/")
public class ScriptRunner implements DisposableBean {
    // ------------------------------ FIELDS ------------------------------

    private static final Logger LOG = LoggerFactory.getLogger(ScriptRunner.class);

    private static final String LANGUAGE = "language";
    private static final String SESSION_ID = "sessionId";

    private static final String PERMISSION_DENIED_USER_DO_NOT_HAVE_SYSTEM_ADMINISTRATOR_RIGHTS = "Permission denied: user do not have system administrator rights!";

    private static final String CLASS_NAME = ScriptRunner.class.getName();

    private final ScriptService scriptService;
    private final ScriptSessionManager sessionManager;

    private final UserManager userManager;
    private final ApplicationProperties applicationProperties;

    // --------------------------- CONSTRUCTORS ---------------------------

    @SuppressWarnings({ "UnusedDeclaration" })
    public ScriptRunner(final ScriptService scriptService, final UserManager userManager,
            ScriptSessionManager sessionManager, ApplicationProperties applicationProperties) {
        this.scriptService = checkNotNull(scriptService, "scriptService");
        this.userManager = checkNotNull(userManager, "userManager");
        this.sessionManager = checkNotNull(sessionManager, "sessionManager");
        this.applicationProperties = checkNotNull(applicationProperties, "applicationProperties");
    }

    // ------------------------ INTERFACE METHODS ------------------------

    // --------------------- Interface DisposableBean ---------------------

    @Override
    public void destroy() throws Exception {
        final Map<SessionId, ScriptSession> idSessionMap = sessionManager.listAllSessions();
        if (idSessionMap != null && !idSessionMap.isEmpty()) {
            LOG.warn("Alive sessions are found and shall be destroyed: {}", Joiner.on(',').skipNulls()
                    .join(Iterables.transform(idSessionMap.keySet(), new Function<SessionId, Object>() {
                        @Override
                        public Object apply(@Nullable SessionId sessionId) {
                            if (sessionId != null) {
                                return sessionId.getSessionId();
                            }
                            return null;
                        }
                    })));
        }
        sessionManager.clear();
    }

    // -------------------------- OTHER METHODS --------------------------

    @SuppressWarnings({ "UnusedDeclaration" })
    public URI buildSelfLink(String query) {
        URI base = URI.create(applicationProperties.getBaseUrl()).normalize();
        return new UriBuilderImpl().path(base.getPath()).path("/rest/rest-scripting/1.0").path(ScriptRunner.class)
                .build(query);
    }

    @POST
    @Path("/sessions/{" + SESSION_ID + "}")
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    public Response cli_eval(@PathParam(SESSION_ID) final String sessionId, EvalScript evalScript) {
        if (!isAdministrator()) {
            return responseForbidden();
        }

        ScriptSessionManager.ScriptSession scriptSession;
        try {
            scriptSession = checkNotNull(sessionManager.getSession(SessionId.valueOf(sessionId)),
                    "Session instance");
        } catch (IllegalArgumentException e) {
            return responseInternalError(Arrays.asList((e.getMessage())));
        }

        ConsoleOutputBean consoleOutputBean = new ConsoleOutputBean();
        try {
            return responseEvalOk(eval(scriptSession.getScriptEngine(), evalScript.getScript(),
                    evalScript.getBindings(), consoleOutputBean));
        } catch (ScriptException e) {
            //LOG.error("Script exception", e);
            return responseScriptError(e, consoleOutputBean.getOutAsString(), consoleOutputBean.getErrAsString());
        }
    }

    private boolean isAdministrator() {
        return userManager.getRemoteUsername() != null
                && userManager.isSystemAdmin(userManager.getRemoteUsername());
    }

    private Response responseForbidden() {
        return Response.serverError()
                .entity(createErrorCollection(
                        ImmutableList.of(PERMISSION_DENIED_USER_DO_NOT_HAVE_SYSTEM_ADMINISTRATOR_RIGHTS)))
                .cacheControl(CacheControl.NO_CACHE)

                .build();
    }

    private Response responseInternalError(List<String> errorMessages) {
        return responseError(createErrorCollection(errorMessages));
    }

    private <Entity> Response responseError(Entity entity) {
        return Response.serverError().entity(entity).cacheControl(CacheControl.NO_CACHE)

                .build();
    }

    private ErrorCollection createErrorCollection(final Iterable<String> errorMessages) {
        ErrorCollection.Builder builder = ErrorCollection.builder();
        for (String message : errorMessages) {
            builder = builder.addErrorMessage(message);
        }
        return builder.build();
    }

    private Response responseEvalOk(final ConsoleOutputBean output) {
        return responseOk(new ConsoleOutputBeanWrapper(output));
    }

    private <Entity> Response responseOk(Entity entity) {
        return Response.ok(entity).cacheControl(CacheControl.NO_CACHE).build();
    }

    private ConsoleOutputBean eval(ScriptEngine engine, String evalScript, Map<String, ?> bindings,
            final ConsoleOutputBean consoleOutputBean) throws ScriptException {
        updateBindings(engine, ScriptContext.ENGINE_SCOPE, new HashMap<String, Object>() {
            {
                put("out", new PrintWriter(consoleOutputBean.getOut(), true));
                put("err", new PrintWriter(consoleOutputBean.getErr(), true));
            }
        });

        if (bindings != null && !bindings.isEmpty()) {
            updateBindings(engine, ScriptContext.ENGINE_SCOPE, bindings);
        }

        engine.getContext().setWriter(consoleOutputBean.getOut());
        engine.getContext().setErrorWriter(consoleOutputBean.getErr());
        engine.getContext().setReader(new NullReader(0));

        consoleOutputBean.setEvalResult(engine.eval(evalScript, engine.getContext()));

        return consoleOutputBean;
    }

    @SuppressWarnings({ "SameParameterValue" })
    private void updateBindings(ScriptEngine engine, int scope, Map<String, ?> mergeValues) {
        Bindings bindings = engine.getContext().getBindings(scope);
        if (bindings == null) {
            bindings = engine.createBindings();
            engine.getContext().setBindings(bindings, scope);
        }
        bindings.putAll(mergeValues);
    }

    private Response responseScriptError(final Throwable th, String out, String err) {
        return responseError(
                new ScriptErrors(createErrorCollection(ImmutableList.<String>of(getStackTrace(th))), out, err));
    }

    private String getStackTrace(Throwable th) {
        if (th == null) {
            return "";
        }

        List<StackTraceElement> elements = Lists.newArrayList();
        for (StackTraceElement st : th.getStackTrace()) {
            if (st.getClassName().equals(CLASS_NAME))
                break;
            elements.add(st);
        }

        return new StringBuilder(ExceptionUtils.getMessage(th)).append(" at ")
                .append(Joiner.on("\n ").skipNulls().join(elements)).toString();
    }

    @DELETE
    @Path("/sessions/{" + SESSION_ID + "}")
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    public Response deleteSession(@PathParam(SESSION_ID) String sessionId) {
        if (!isAdministrator()) {
            return responseForbidden();
        }

        if (sessionManager.removeSession(SessionId.valueOf(sessionId)) == null) {
            return Response.noContent().cacheControl(CacheControl.NO_CACHE).build();
        }

        return Response.ok().cacheControl(CacheControl.NO_CACHE).build();
    }

    @POST
    @Path("/execute/{" + LANGUAGE + "}")
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    public Response execute(@PathParam(LANGUAGE) final String scriptLanguage, Script script) {
        if (!isAdministrator()) {
            return responseForbidden();
        }

        ScriptEngine engine;
        try {
            engine = createScriptEngine(scriptLanguage, script);
        } catch (IllegalArgumentException e) {
            return responseInternalError(Arrays.asList((e.getMessage())));
        }

        ConsoleOutputBean consoleOutputBean = new ConsoleOutputBean();
        try {
            return responseEvalOk(eval(engine, script.getScript(), script.getBindings(), consoleOutputBean));
        } catch (ScriptException e) {
            //LOG.error("Script exception", e);
            return responseScriptError(e, consoleOutputBean.getOutAsString(), consoleOutputBean.getErrAsString());
        }
    }

    private ScriptEngine createScriptEngine(String scriptLanguage, Script script) {
        ScriptEngine engine = engineByLanguage(scriptLanguage);
        if (engine == null) {
            throw new IllegalStateException(
                    String.format("Language '%s' script engine could not be found", scriptLanguage));
        }
        updateBindings(engine, ScriptContext.ENGINE_SCOPE, new HashMap<String, Object>() {
            {
                put("log", LOG);
                put("selfScriptRunner", ScriptRunner.this);
            }
        });

        engine.getContext().setAttribute(ScriptEngine.FILENAME, scriptName(script.getFilename()),
                ScriptContext.ENGINE_SCOPE);
        engine.getContext().setAttribute(ScriptEngine.ARGV, getArgvs(script.getArgv()), ScriptContext.ENGINE_SCOPE);

        return engine;
    }

    private ScriptEngine engineByLanguage(String language) {
        return scriptService.getEngineByLanguage(language);
    }

    private String scriptName(String filename) {
        return StringUtils.defaultIfEmpty(filename, "<unnamed script>");
    }

    private String[] getArgvs(List<String> argv) {
        return argv == null ? new String[0] : argv.toArray(new String[argv.size()]);
    }

    @GET
    @Path("/sessions")
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    public Response listSessions(@QueryParam(LANGUAGE) @DefaultValue("") String language) {
        if (!isAdministrator()) {
            return responseForbidden();
        }

        SessionIdCollectionWrapper ids = new SessionIdCollectionWrapper(Lists.<SessionIdWrapper>newArrayList());
        for (Map.Entry<SessionId, ScriptSessionManager.ScriptSession> entry : sessionManager.listAllSessions()
                .entrySet()) {
            String languageName = LanguageUtils.getLanguageName(entry.getValue().getScriptEngine().getFactory());
            if (StringUtils.isBlank(language) || StringUtils.equalsIgnoreCase(language, languageName)) {
                String sessionId = entry.getKey().getSessionId();
                String versionString = LanguageUtils
                        .getVersionString(entry.getValue().getScriptEngine().getFactory());

                ids.addSession(new SessionIdWrapper(sessionId, languageName, versionString));
            }
        }

        return responseOk(ids);
    }

    @PUT
    @Path("/sessions")
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_ATOM_XML })
    public Response newSession(Language language) {
        if (!isAdministrator()) {
            return responseForbidden();
        }

        ScriptEngine engine;
        try {
            engine = createScriptEngine(language.getLanguage(),
                    new Script("", "cli", ImmutableList.<String>of(), ImmutableMap.<String, String>of()));
        } catch (IllegalArgumentException e) {
            return responseInternalError(Arrays.asList((e.getMessage())));
        }

        SessionId sessionId = sessionManager
                .putSession(ScriptSession.newInstance(getActorName(userManager.getRemoteUsername()), engine));

        return Response
                .ok(new SessionIdWrapper(sessionId.getSessionId(),
                        LanguageUtils.getLanguageName(engine.getFactory()),
                        LanguageUtils.getVersionString(engine.getFactory())))
                .cacheControl(CacheControl.NO_CACHE).build();
    }

    private String getActorName(String username) {
        return checkNotNull(username);
    }

    // -------------------------- INNER CLASSES --------------------------

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class Language {
        public Language() {
        }

        public Language(String language) {
            this.language = language;
        }

        public String getLanguage() {
            return language;
        }

        public void setLanguage(String language) {
            this.language = language;
        }

        @XmlElement
        private String language;
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class ScriptErrors {
        @XmlElement(name = "errors")
        private ErrorCollection errorCollection;

        @XmlElement
        private String out;

        @XmlElement
        private String err;

        public ErrorCollection getErrorCollection() {
            return errorCollection;
        }

        public void setErrorCollection(ErrorCollection errorCollection) {
            this.errorCollection = errorCollection;
        }

        public String getOut() {
            return out;
        }

        public void setOut(String out) {
            this.out = out;
        }

        public String getErr() {
            return err;
        }

        public void setErr(String err) {
            this.err = err;
        }

        public ScriptErrors() {
        }

        public ScriptErrors(ErrorCollection errorCollection, String out, String err) {
            this.errorCollection = errorCollection;
            this.out = out;
            this.err = err;
        }
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class Script {
        public Script() {
        }

        public Script(String script, String filename, List<String> argv, Map<String, String> bindings) {
            this.script = script;
            this.filename = filename;
            this.argv = argv;
            this.bindings = bindings;
        }

        @XmlElement
        private String script;

        @XmlElement
        private String filename;

        @XmlElement
        private List<String> argv;

        @XmlElement
        private Map<String, String> bindings;

        public Map<String, String> getBindings() {
            return bindings;
        }

        public void setBindings(Map<String, String> bindings) {
            this.bindings = bindings;
        }

        public String getScript() {
            return script;
        }

        public void setScript(String script) {
            this.script = script;
        }

        public String getFilename() {
            return filename;
        }

        public void setFilename(String filename) {
            this.filename = filename;
        }

        public List<String> getArgv() {
            return argv;
        }

        public void setArgv(List<String> argv) {
            this.argv = argv;
        }
    }

    public static class ConsoleOutputBean {
        private StringWriter out;

        private StringWriter err;

        private Object evalResult;

        @SuppressWarnings({ "UnusedDeclaration" })
        public ConsoleOutputBean(StringWriter out, StringWriter err) {
            this.out = out;
            this.err = err;
            this.evalResult = null;
        }

        @SuppressWarnings({ "UnusedDeclaration" })
        public ConsoleOutputBean() {
            this(new StringWriter(), new StringWriter());
        }

        public Object getEvalResult() {
            return evalResult;
        }

        private String asString(StringWriter sw) {
            return checkNotNull(sw, "sw").getBuffer().toString();
        }

        String getOutAsString() {
            return asString(getOut());
        }

        String getErrAsString() {
            return asString(getErr());
        }

        public void setEvalResult(Object evalResult) {
            this.evalResult = evalResult;
        }

        public StringWriter getOut() {
            return out;
        }

        @SuppressWarnings({ "UnusedDeclaration" })
        public void setOut(StringWriter out) {
            this.out = out;
        }

        public StringWriter getErr() {
            return err;
        }

        @SuppressWarnings({ "UnusedDeclaration" })
        public void setErr(StringWriter err) {
            this.err = err;
        }
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class SessionIdWrapper {
        @XmlElement
        private String sessionId;

        @XmlElement
        private String languageName;

        @XmlElement
        private String languageVersion;

        public SessionIdWrapper(String sessionId, String languageName, String languageVersion) {
            this.sessionId = sessionId;
            this.languageName = languageName;
            this.languageVersion = languageVersion;
        }

        public String getLanguageName() {
            return languageName;
        }

        public void setLanguageName(String languageName) {
            this.languageName = languageName;
        }

        public String getSessionId() {
            return sessionId;
        }

        public void setSessionId(String sessionId) {
            this.sessionId = sessionId;
        }
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class SessionIdCollectionWrapper {
        @XmlElement
        private List<SessionIdWrapper> sessions;

        public SessionIdCollectionWrapper(List<SessionIdWrapper> sessions) {
            this.sessions = sessions;
        }

        public List<SessionIdWrapper> getSessions() {
            return sessions;
        }

        public void setSessions(List<SessionIdWrapper> sessions) {
            this.sessions = sessions;
        }

        public SessionIdCollectionWrapper addSession(SessionIdWrapper id) {
            sessions.add(id);
            return this;
        }

        public SessionIdCollectionWrapper addSession(Iterable<SessionIdWrapper> ids) {
            Iterables.addAll(sessions, ids);
            return this;
        }
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class ConsoleOutputBeanWrapper {
        @XmlElement
        private String out;

        @XmlElement
        private String err;

        @XmlElement
        private String evalResult;

        public ConsoleOutputBeanWrapper(String evalResult, String out, String err) {
            this.out = out;
            this.err = err;
            this.evalResult = evalResult;
        }

        public ConsoleOutputBeanWrapper(ConsoleOutputBean bean) {
            this(String.valueOf(bean.getEvalResult()), bean.getOutAsString(), bean.getErrAsString());
        }

        public String getEvalResult() {
            return evalResult;
        }

        public void setEvalResult(String evalResult) {
            this.evalResult = evalResult;
        }

        public String getOut() {
            return out;
        }

        public void setOut(String out) {
            this.out = out;
        }

        public String getErr() {
            return err;
        }

        public void setErr(String err) {
            this.err = err;
        }
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    @XmlRootElement
    public static class EvalScript {
        @XmlElement
        private String script;

        @XmlElement
        private Map<String, String> bindings;

        public void setBindings(Map<String, String> bindings) {
            this.bindings = bindings;
        }

        public String getScript() {
            return script;
        }

        public void setScript(String script) {
            this.script = script;
        }

        public Map<String, String> getBindings() {
            return bindings;
        }
    }
}