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;
		}
	}
}