Java tutorial
/* * 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; } } }