Commit 1dea75f5 authored by Matija Obreza's avatar Matija Obreza

Beanshell admin tool

parent cfd309b6
......@@ -537,6 +537,11 @@
<artifactId>glis-client-resttemplate</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache-extras.beanshell</groupId>
<artifactId>bsh</artifactId>
<version>2.0b6</version>
</dependency>
</dependencies>
<build>
......
/*
* Copyright 2017 Global Crop Diversity Trust
*
* 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 org.genesys2.server.servlet.controller.admin;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.genesys.glis.v1.api.GenesysApi;
import org.genesys2.server.service.GenesysFilterService;
import org.genesys2.server.service.GenesysService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import bsh.ConsoleInterface;
import bsh.EvalError;
import bsh.Interpreter;
/**
* Beanshell
*
* @author Matija Obreza
*/
@Controller
@RequestMapping("/admin/bsh")
@PreAuthorize("hasRole('ADMINISTRATOR')")
public class BeanshellController implements InitializingBean {
public static final Logger LOG = LoggerFactory.getLogger(BeanshellController.class);
@Autowired
private GenesysService genesysService;
@Autowired
private GenesysFilterService filterService;
@Autowired
private GenesysApi glisGenesys;
@PersistenceContext
private EntityManager entityManager;
@Autowired
private JdbcTemplate jdbcTemplate;
private ObjectMapper mapper;
private Map<String, Object> exposedBeans;
@Override
public void afterPropertiesSet() throws Exception {
mapper = new ObjectMapper();
mapper.disable(SerializationFeature.EAGER_SERIALIZER_FETCH);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
this.exposedBeans = new HashMap<>();
this.exposedBeans.put("json", mapper);
this.exposedBeans.put("genesysService", genesysService);
this.exposedBeans.put("filterService", filterService);
this.exposedBeans.put("glisGenesys", glisGenesys);
this.exposedBeans.put("entityManager", entityManager);
this.exposedBeans.put("jdbc", jdbcTemplate);
}
/**
* Render the form.
*/
@RequestMapping(method = RequestMethod.GET)
public ModelAndView defaultView(ModelMap model) {
model.addAttribute("beans", this.exposedBeans);
return new ModelAndView("/admin/beanshell", model);
}
/**
* Execute script in read-only transaction.
*
* @param model
* @param script
* @return
*/
@Transactional(readOnly = true, noRollbackFor = Exception.class)
@RequestMapping(method = RequestMethod.POST, params = { "readonly", "script" })
public String bshExecReadonly(RedirectAttributes redirectAttributes, @RequestParam(name = "script") String script) {
LOG.debug("Executing script in readonly mode\n{}", script);
return executeScript(redirectAttributes, script);
}
private String executeScript(RedirectAttributes redirectAttributes, String script) {
redirectAttributes.addFlashAttribute("script", script);
try {
ConsoleInterface console = new MemoryConsole();
Interpreter i = initializeInterpreter(console);
Object result = i.eval(script);
redirectAttributes.addFlashAttribute("result", result);
try {
redirectAttributes.addFlashAttribute("resultJson", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result));
} catch (Throwable e) {
redirectAttributes.addFlashAttribute("resultJson", "Could not convert to JSON: " + e.getMessage());
}
redirectAttributes.addFlashAttribute("console", console.toString());
} catch (final EvalError e) {
LOG.error(e.getMessage());
redirectAttributes.addFlashAttribute("error", new BeanshellError(e));
} catch (final Throwable e) {
LOG.error(e.getMessage(), e);
redirectAttributes.addFlashAttribute("exception", new BeanshellError(e));
}
return "redirect:/admin/bsh";
}
protected Interpreter initializeInterpreter(ConsoleInterface console) throws EvalError {
final Interpreter i = new Interpreter(console);
// beans
exposedBeans.entrySet().stream().forEach(entry -> {
try {
i.set(entry.getKey(), entry.getValue());
} catch (EvalError e) {
LOG.error("Trouble initializing bsh interpreter with key={}: {}", entry.getKey(), e.getMessage(), e);
}
});
// imports
i.eval("import org.genesys2.server.model.genesys.*;");
return i;
}
/**
* Byte array backed Beanshell console
*/
public static class MemoryConsole implements ConsoleInterface {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * 1024);
private final PrintStream stdout = new PrintStream(baos, true);
private final PrintStream stderr = new PrintStream(baos, true);
@Override
public Reader getIn() {
return null;
}
@Override
public PrintStream getOut() {
return stdout;
}
@Override
public PrintStream getErr() {
return stderr;
}
@Override
public void println(Object o) {
stdout.println(o);
}
@Override
public void print(Object o) {
stdout.print(o);
}
@Override
public void error(Object o) {
stderr.println(o);
}
@Override
public String toString() {
return baos.toString();
}
}
/**
* Serializable exception wrapper for flash attributes
*/
public static class BeanshellError implements Serializable {
private static final long serialVersionUID = 6436834633021529446L;
private String message;
private StackTraceElement[] stackTrace;
private int errorLineNumber;
private String errorText;
private String cause;
private String scriptStackTrace;
private String errorClass;
public BeanshellError(Throwable e) {
this.errorClass = e.getClass().getName();
this.message = e.getMessage();
this.stackTrace = e.getStackTrace();
if (e instanceof EvalError) {
EvalError ee = (EvalError) e;
this.errorLineNumber = ee.getErrorLineNumber();
this.errorText = ee.getErrorText();
this.scriptStackTrace = ee.getScriptStackTrace();
if (ee.getCause() != null)
this.cause = ee.getCause().getMessage();
}
}
public String getMessage() {
return message;
}
public StackTraceElement[] getStackTrace() {
return stackTrace;
}
public String getCause() {
return cause;
}
public int getErrorLineNumber() {
return errorLineNumber;
}
public String getErrorText() {
return errorText;
}
public String getScriptStackTrace() {
return scriptStackTrace;
}
public String getErrorClass() {
return errorClass;
}
}
}
......@@ -749,6 +749,7 @@ menu.admin.repository=Repository
menu.admin.repository.files=Repository file manager
menu.admin.repository.galleries=Image galleries
menu.admin.hazelcast=Hazelcast
menu.admin.beanshell=Beanshell
news.content.page.all.title=News list
news.archive.title=News Archive
......
<!DOCTYPE html>
<%@ include file="/WEB-INF/jsp/init.jsp" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
<title><spring:message code="menu.admin.beanshell"/></title>
</head>
<body>
<div class="row">
<!-- List exposed beans -->
<c:if test="${beans ne null}">
<div class="col-sm-12 col-md-3 pull-right">
<h4>Exposed beans</h4>
<ul>
<c:forEach items="${beans.keySet()}" var="item">
<li>
<b>${item}</b>
<%-- &rarr; ${beans[item].getClass().toString()} --%>
</li>
</c:forEach>
</ul>
</div>
</c:if>
<div class="col-sm-12 col-md-9">
<!-- Script -->
<form method="post" action="<c:url value='/admin/bsh' />">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<textarea style="min-height: 300px; font-family: Consolas, monospace;" name="script" class="form-control"><c:out value="${script}"/></textarea>
<button type="submit" name="readonly" class="btn btn-primary">Execute in readonly mode</button>
<%-- <button type="submit" name="transactional" class="btn btn-default">Transactional</button> --%>
</form>
</div>
</div>
<!-- Outcome -->
<c:if test="${error ne null}">
<div class="row">
<div class="col-xs-12">
<h4>Script error</h4>
<div class="alert alert-danger">
<b>In line <c:out value="${error.errorLineNumber}" />:
<c:out value="${error.errorText}" /></b>
<c:out value="${error.cause}" />
</div>
<pre style="white-space: pre-wrap;">
<c:out value="${error.scriptStackTrace}"/>
</pre>
</div>
</div>
</c:if>
<c:if test="${exception ne null}">
<div class="row">
<div class="col-xs-12">
<h4>Exception</h4>
<div class="alert alert-danger">
<c:out value="${exception.message}" />
</div>
<pre style="white-space: pre-wrap;">
<c:forEach var="stackTraceElem" items="${exception.stackTrace}"><c:out value="${stackTraceElem}"/>
</c:forEach>
</pre>
</div>
</div>
</c:if>
<div class="row">
<div class="col-xs-12 col-md-6">
<h4>Result as JSON</h2>
<pre style="white-space: pre-wrap;"><c:out value="${resultJson}" /></pre>
<h4>Result</h4>
<pre style="white-space: pre-wrap;"><c:out value="${result}" /></pre>
</div>
<div class="col-xs-12 col-md-6">
<h4>Console</h4>
<pre style="white-space: pre-wrap;"><c:out value="${console}" /></pre>
</div>
</div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment