/* * Sewing: a Simple framework for Embedded-OSGi Web Development * Copyright (C) 2009 Bug Labs * Email: bballantine@buglabs.net * Site: http://www.buglabs.net * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ package com.buglabs.osgi.sewing.pub; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Writer; import java.net.URL; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.osgi.framework.BundleContext; import org.osgi.service.http.HttpContext; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; import org.osgi.service.log.LogService; import com.buglabs.osgi.sewing.LogManager; import com.buglabs.osgi.sewing.TemplateIncludesCache; import com.buglabs.osgi.sewing.pub.util.ControllerMap; import com.buglabs.osgi.sewing.pub.util.RequestHelper; import com.buglabs.osgi.sewing.pub.util.RequestParameters; import freemarker.template.InputSource; import freemarker.template.SimpleHash; import freemarker.template.SimpleScalar; import freemarker.template.Template; import freemarker.template.TemplateModelRoot; /** * This is the main Sewing framework servlet class. clients are expected to * extend this abstract class and implement the <code>getControllerMap()</code> * method. This class Servlet is then registered with the OSGi runtime using * ISewingService. * * Then, for each page a client would like to publish, the client must create * SewingController classes (typically as inner classes of the client's * SewingHttpServlet implementation). The url mapping (url -> controller) is * done in the getControllerMap() method. * * @author brian * * UPDATE: 2010-07-14 akweon: added beforeGet() and beforePost() in * processRequest * */ public abstract class SewingHttpServlet extends HttpServlet { private static final long serialVersionUID = 1L; public static final int GET = 0; public static final int POST = 1; private static final String ERROR_404 = "The resource you requested was not found."; private static final String ERROR_500 = "There was an application error."; private static final String ERROR_REDIRECT = "There was a redirect error."; private static final String TEMPLATE_EXT = ".fml"; private static final String TEMPLATES_ALIAS = "templates"; private static final String IMAGES_ALIAS = "images"; private static final String STYLESHEET_ALIAS = "stylesheets"; private static final String JAVASCRIPT_ALIAS = "javascripts"; private static final String INCLUDES_ALIAS = "includes"; private static final String ASSET_ROOT_KEY = "_assetRoot"; private static final String IMAGE_ROOT_KEY = "_imageRoot"; private static final String STYLESHEET_ROOT_KEY = "_stylesheetRoot"; private static final String JAVASCRIPT_ROOT_KEY = "_javascriptRoot"; private BundleContext bundle_context = null; private volatile String servlet_alias = null; private ControllerMap controller_map = null; /** * This is called during setup. Your implementation needs to map the inner * class controllers to a string name which is what shows up on the URL. * * @return */ public abstract ControllerMap getControllerMap(); /** * This is called by the Service framework when the servlet is registered * this sets up the servlet resources and stuff * * @param bundleContext * @param httpService * @param servletAlias */ public final void setup(BundleContext bundleContext, HttpService httpService, String servletAlias) { bundle_context = bundleContext; servlet_alias = servletAlias; controller_map = getControllerMap(); try { // register servlet httpService.registerServlet(servletAlias, this, null, null); // Register images httpService.registerResources(servlet_alias + "." + IMAGES_ALIAS, "", new ImagesHttpContext(bundle_context)); // Register stylesheets httpService.registerResources(servlet_alias + "." + STYLESHEET_ALIAS, "", new StylesheetsHttpContext(bundle_context)); // Register javascripts httpService.registerResources(servlet_alias + "." + JAVASCRIPT_ALIAS, "", new JavascriptsHttpContext(bundle_context)); } catch (ServletException e) { LogManager.log(LogService.LOG_ERROR, "Servlet Failed.", e); } catch (NamespaceException e) { LogManager.log(LogService.LOG_ERROR, "Unable to register resources.", e); } } /** * called by the service framework to unregiser everything * * @param httpService */ public final void teardown(HttpService httpService) { if (servlet_alias == null) return; try { httpService.unregister(servlet_alias); } catch (IllegalArgumentException e) { } if (!servlet_alias.endsWith("/")) { try { httpService.unregister(servlet_alias + "/"); } catch (IllegalArgumentException e) { } } try { httpService.unregister(servlet_alias + "." + IMAGES_ALIAS); } catch (IllegalArgumentException e) { } try { httpService.unregister(servlet_alias + "." + STYLESHEET_ALIAS); } catch (IllegalArgumentException e) { } try { httpService.unregister(servlet_alias + "." + JAVASCRIPT_ALIAS); } catch (IllegalArgumentException e) { } } /** * Handles the GET request for SewingHttpServlet Do not override if you hope * to use the framework as it was designed */ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { processRequest(GET, req, resp); } catch (Exception e) { LogManager.log(LogService.LOG_ERROR, "Unable to process GET request.", e); renderError(500, resp); return; } } /** * Handles the POST request for SewingHttpServlet Do not override if you * hope to use the framework as it was designed */ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { processRequest(POST, req, resp); } catch (Exception e) { LogManager.log(LogService.LOG_ERROR, "Unable to process POST request.", e); renderError(500, resp); return; } } /** * This one does all the work, called by both doGet and doPost * * @param type * @param req * @param resp * @throws IOException */ private void processRequest(int type, HttpServletRequest req, HttpServletResponse resp) throws IOException { String controllerPath = req.getPathInfo(); // set up controllerPath to be index if no controller specified in url if (controllerPath == null) controllerPath = "/index"; String controllerName = controllerPath.substring(1, controllerPath.length()); RequestParameters params = getRequestParams(req); processRequest(type, controllerName, params, req, resp); } /** * Broke down processRequest into a function that takes all of it's moving * parts explicitly. * * @param type * @param controllerName * @param params * @param req * @param resp * @throws IOException */ private void processRequest(int type, String controllerName, RequestParameters params, HttpServletRequest req, HttpServletResponse resp) throws IOException { SewingController controller = getController(controllerName); if (controller == null) { renderError(404, resp); return; } TemplateModelRoot root = null; // synchronize this section on controller // this is because one controller instance can be shared across requests // and, though the servlet container on BUG is currently // single-threaded, // in the future, there could be synchronization problems, especially // with re-direct synchronized (controller) { try { if (type == GET) { controller.beforeGet(params, req, resp); if (!controller.getSkipAction()) { root = controller.get(params, req, resp); } } else if (type == POST) { controller.beforePost(params, req, resp); if (!controller.getSkipAction()) { root = controller.post(params, req, resp); } } // check if the controller asked for a redirect and do it if // redirect, root is ignored if (controller.doRedirect() && controller.getRedirectInfo() != null) { handleRedirect(controller, resp); return; } } finally { controller.clearRedirect(); } } if (root == null) root = new SimpleHash(); setDefaultValues(root); Template t = getTemplate(controller, controllerName); t.setCache(new TemplateIncludesCache(bundle_context, INCLUDES_ALIAS)); resp.setContentType("text/html"); Writer out = resp.getWriter(); t.process(root, out); } /** * helper for doing a processRequest with a RedirectInfo object * * @param redirectInfo * @throws IOException */ private void processRequest(RedirectInfo redirectInfo) throws IOException { processRequest(redirectInfo.getRequestType(), redirectInfo.getControllerName(), redirectInfo.getParams(), redirectInfo.getRequestObject(), redirectInfo.getResponseObject()); } private void handleRedirect(SewingController requestController, HttpServletResponse resp) throws IOException { // make sure we're not trying to redirect to self if (requestController.equals(getController(requestController.getRedirectInfo().getControllerName()))) { resp.sendError(500, ERROR_REDIRECT + " A controller cannot redirect to itself"); return; } if (requestController.getRedirectInfo().getUrl() != null) { resp.setContentType("text/html"); Writer out = resp.getWriter(); out.write(requestController.getRedirectInfo().getRedirectHtml()); } else { processRequest(requestController.getRedirectInfo()); } requestController.clearRedirect(); } private RequestParameters getRequestParams(HttpServletRequest req) { if (RequestHelper.isMultipart(req)) return RequestHelper.parseMultipart(req); else return RequestHelper.parseParams(req); } /** * Sets up some defaults in the templates that implementers can use to * access images, javascripts, etc. * * @param root */ private void setDefaultValues(TemplateModelRoot root) { root.put(ASSET_ROOT_KEY, new SimpleScalar(servlet_alias + ".")); root.put(IMAGE_ROOT_KEY, new SimpleScalar(servlet_alias + "." + IMAGES_ALIAS)); root.put(STYLESHEET_ROOT_KEY, new SimpleScalar(servlet_alias + "." + STYLESHEET_ALIAS)); root.put(JAVASCRIPT_ROOT_KEY, new SimpleScalar(servlet_alias + "." + JAVASCRIPT_ALIAS)); } private SewingController getController(String controllerName) { if (controller_map == null) controller_map = getControllerMap(); if (controller_map == null) return null; return controller_map.get(controllerName); } private Template getTemplate(SewingController controller, String controllerName) throws IOException { String templateName = controller.getTemplateName(); if (templateName == null) templateName = controllerName + TEMPLATE_EXT; URL templateUrl = bundle_context.getBundle().getResource("/" + TEMPLATES_ALIAS + "/" + templateName); InputStream stream = templateUrl.openStream(); if (stream == null) throw new IOException("Unable to open stream on " + templateUrl.toExternalForm()); InputSource inputSource = new InputSource(new InputStreamReader(stream)); return new Template(inputSource); } /** * call this to render an http error page in the browser * * @param errorNum * @param req * @param resp */ private void renderError(int errorNum, HttpServletResponse resp) { String message = ERROR_500; if (errorNum == 404) message = ERROR_404; try { resp.sendError(errorNum, message); } catch (IOException e) { LogManager.log(LogService.LOG_ERROR, "Unable to process error response.", e); } } /** * images, javascripts, and stylesheets are assets this HttpContext is * implemented for each of the asset types to properly server up the * resource type from the right spot * * @author brian * */ private abstract class AssetsHttpContext implements HttpContext { protected BundleContext context; public AssetsHttpContext(BundleContext context) { this.context = context; } public boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) { return true; } public String getMimeType(String name) { return null; } public URL getResource(String path) { // get first slash after first character // (which is probably also a slash we don't want) int slash = path.indexOf('/', 1); String file = path; if (slash > -1) { file = path.substring(slash); } return context.getBundle().getResource(getResourcePath() + "/" + file); } protected abstract String getResourcePath(); } private class ImagesHttpContext extends AssetsHttpContext { public ImagesHttpContext(BundleContext context) { super(context); } protected String getResourcePath() { return "/" + IMAGES_ALIAS; } } private class StylesheetsHttpContext extends AssetsHttpContext { public StylesheetsHttpContext(BundleContext context) { super(context); } protected String getResourcePath() { return "/" + STYLESHEET_ALIAS; } } private class JavascriptsHttpContext extends AssetsHttpContext { public JavascriptsHttpContext(BundleContext context) { super(context); } protected String getResourcePath() { return "/" + JAVASCRIPT_ALIAS; } } }