/*
* BeanShell Web
* Copyright (C) 2012 Stefano Fornari
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY Stefano Fornari, Stefano Fornari
* DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*/
package ste.web.beanshell.jetty;
import java.io.*;
import java.util.logging.Logger;
import javax.servlet.*;
import javax.servlet.http.*;
import bsh.*;
import java.util.Enumeration;
import java.util.logging.Level;
import static ste.web.beanshell.Constants.*;
/**
* Executes the bsh script specified by the URL. The script is the controller
* and shall set the view that should be rendered after the execution of the
* script in the bsh variable <i>view</i>.
*
* The lookup of the scripts and views are controlled by the following pattern:
* <pre>
* {context}{controllers-prefix}/{script-pathname}
* {context}{views-prefix}/{jsp-pathname}
* </pre>
* For example, if context=/myapp, controllers-prefix=c and views-prefix=v,
* the URL http://myserver:8080/myapp/mycontroller.bsh will read the script
* {CONTAINER_HOME}/myapp/c/mycontroller.bsh. If the controller sets <i>view</i>
* to myview.jsp, the request is forwarded to http://myserver:8080/myapp/v/myview.jsp
*
* <i>controllers-prefix</i> and <i>views-prefix</i> can be set as context
* parameters in web.xml:
*
* <context-param>
* <param-name><b>controllers-prefix</b></param-name>
* <param-value><b>controllers</b></param-value>
* </context-param>
* <context-param>
* <param-name><b>views-prefix</b></param-name>
* <param-value><b>views</b></param-value>
* </context-param>
*
* <i>controllers-prefix</i> and <i>views-prefix</i> defauult to "".
*
* In addition to scripts Beanshell can be extended with commands, which must be
* located somewhere under the classpath. BeanShellServlet instructs Beanshell
* to search for commands under <pre>WEB-INF/commands</pre>.
*
* @author ste
*/
public class BeanShellServlet extends HttpServlet {
// ---------------------------------------------------------- Protected data
protected static final Logger log = Logger.getLogger(LOG_NAME);
protected String contextRealPath = null;
protected String controllersPrefix = "controllers";
protected String viewsPrefix = "views";
// ---------------------------------------------------------- Public methods
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
ServletContext context = getServletContext();
controllersPrefix = context.getInitParameter(PARAM_CONTROLLERS);
if (controllersPrefix == null) {
controllersPrefix = DEFAULT_CONTROLLERS_PREFIX;
} else {
//
// let's fix a common mistake :)
//
if (!controllersPrefix.startsWith("/")) {
controllersPrefix = '/' + controllersPrefix;
}
}
viewsPrefix = context.getInitParameter(PARAM_VIEWS);
if (viewsPrefix == null) {
viewsPrefix = DEFAULT_VIEWS_PREFIX;
} else {
//
// let's fix a common mistake :)
//
if (!viewsPrefix.endsWith("/")) {
viewsPrefix = viewsPrefix + '/';
}
}
contextRealPath = context.getRealPath("");
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "controllers-prefix: {0}", controllersPrefix);
log.log(Level.FINE, "views-prefix: {0}", viewsPrefix);
log.log(Level.FINE, "context path: {0}", contextRealPath);
}
}
@Override
public void doGet(final HttpServletRequest request ,
final HttpServletResponse response)
throws ServletException, IOException {
doWork(request, response);
}
@Override
public void doPost(final HttpServletRequest request ,
final HttpServletResponse response)
throws ServletException, IOException {
doWork(request, response);
}
// --------------------------------------------------------- Private methods
/**
* This methods does the real work regardless the HTTP protocol used.
* It grabs the pathname of the script to be executed by the URI used to
* invoke.
* Note that if the request comes from an internal redirect, getRequestURI
* returns the external URI (i.e. the one typed in the browser); in this
* case the servlet engine sets the redirected URI properties in the
* following attributes:
* <pre>
* javax.servlet.forward.request_uri
* javax.servlet.forward.context_path
* javax.servlet.forward.servlet_path
* javax.servlet.forward.path_info
* javax.servlet.forward.query_string
* </pre>
*
* Therefore we first check if the attribute <code>javax.servlet.forward.request_uri</code>
* is set; if set we directly use it, if not, we get the servlet URI calling
* <code>request.getRequestURI()</code>
*
* @param request the request
* @param response the response
* @throws ServletException in case of engine exceptions
* @throws IOException in case of IO errors
*/
private void doWork(final HttpServletRequest request ,
final HttpServletResponse response)
throws ServletException, IOException {
if (log.isLoggable(Level.FINE)) {
Enumeration<String> names = request.getAttributeNames();
while(names.hasMoreElements()) {
String name = (String)names.nextElement();
if (name.startsWith("javax.servlet")) {
log.log(Level.FINE, ">> {0}: {1}", new Object[]{name, request.getAttribute(name)});
}
}
}
String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
if (uri == null) {
uri = request.getServletPath();
} else {
uri = uri.substring(((String)request.getAttribute("javax.servlet.include.context_path")).length());
}
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Serving {0}", uri);
}
try {
Interpreter interpreter = new Interpreter();
BeanShellUtils.setup(interpreter, request, response);
interpreter.set("log", log);
//
// Add commands path
//
interpreter.eval("addClassPath(\"" + contextRealPath + "\"); importCommands(\"/WEB-INF/commands\")");
String s = getServletContext().getRealPath(uri);
interpreter.eval(getScript(s));
BeanShellUtils.setVariablesAttributes(interpreter, request);
String nextView = (String)interpreter.get("view");
if ((nextView != null) && (nextView instanceof String)) {
nextView = request.getServletPath() + "/.." + viewsPrefix + nextView + ".jsp";
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Forwarding to {0}", nextView);
}
request.getRequestDispatcher(nextView).include(request, response);
}
} catch (Exception e) {
handleError(request, response, e);
}
}
/**
* Handles errors conditions returning an appropriate content to the client.
*
* @param request the request object
* @param response the response object
* @t a throwable object
*
*/
private void handleError(final HttpServletRequest request,
final HttpServletResponse response,
final Throwable t) {
String msg = t.getMessage();
if (log.isLoggable(Level.SEVERE)) {
log.log(Level.SEVERE, "Error message: {0}", msg);
log.throwing(getClass().getName(), "handleError", t);
}
try {
if (t instanceof FileNotFoundException) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
} catch (IOException e) {
if (log.isLoggable(Level.SEVERE)) {
log.severe(e.getMessage());
log.throwing(getClass().getName(), "handleError", e);
}
}
}
/**
* Returns the script to be invoked. @see doWork() for more information on
* how local redirects are handled.
*
* @param script the script name to start to build the real pathname
*
* @return the beanshell script to be invoked
*
* @throws IOException if there are issues reading the script
*/
private String getScript(final String script) throws IOException {
File scriptFile = new File(script);
String controllerPath = scriptFile.getParent() + controllersPrefix;
File controllerFile = new File(controllerPath, scriptFile.getName());
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "script: {0}", script);
log.log(Level.FINE, "controllerFile: {0}", controllerFile);
}
return BeanShellUtils.getScript(controllerFile);
}
}