BeanshellController.java
/*
* 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.genesys.server.mvc.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 org.genesys.filerepository.service.RepositoryService;
import org.genesys.glis.v1.api.GenesysApi;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.GenesysFilterService;
import org.genesys.server.service.GenesysService;
import org.genesys.server.service.worker.AccessionProcessor;
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
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(required = false)
private GenesysApi glisGenesys;
@Autowired
private AccessionProcessor accessionProcessor;
@Autowired
private RepositoryService repositoryService;
@Autowired
private AccessionService accessionService;
@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("accessionService", accessionService);
this.exposedBeans.put("accessionProcessor", accessionProcessor);
this.exposedBeans.put("entityManager", entityManager);
this.exposedBeans.put("repositoryService", repositoryService);
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);
}
/**
* Execute script in transaction.
*
* @param model
* @param script
* @return
*/
@Transactional(readOnly = false, rollbackFor = Exception.class)
@RequestMapping(method = RequestMethod.POST, params = { "transactional", "script" })
public String bshExecute(RedirectAttributes redirectAttributes, @RequestParam(name = "script") String script) {
LOG.debug("Executing script in transaction 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.genesys.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;
}
}
}