Java tutorial
/* * Copyright (c)2014 Florin T.Ptracu * * 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 ca.simplegames.micro; import ca.simplegames.micro.controllers.ControllerException; import ca.simplegames.micro.controllers.ControllerNotFoundException; import ca.simplegames.micro.helpers.HelperWrapper; import ca.simplegames.micro.repositories.Repository; import ca.simplegames.micro.repositories.RepositoryManager; import ca.simplegames.micro.utils.ClassUtils; import ca.simplegames.micro.utils.CollectionUtils; import ca.simplegames.micro.utils.ParamsFactory; import ca.simplegames.micro.utils.PathUtilities; import ca.simplegames.micro.viewers.TemplateEngineWrapper; import ca.simplegames.micro.viewers.ViewException; import org.apache.bsf.BSFManager; import org.apache.commons.lang3.StringUtils; import org.jrack.*; import org.jrack.context.MapContext; import org.jrack.utils.Mime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileNotFoundException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; /** * A micro MVC implementation for Java web applications * * @author <a href="mailto:florin.patrascu@gmail.com">Florin T.PATRASCU</a> * @since $Revision$ (created: 2013-01-01 4:22 PM) */ public class Micro { public static final String DOUBLE_SLASH = "//"; private Logger log = LoggerFactory.getLogger(getClass()); public static final String DOT = "."; public static final String SLASH = "/"; public static final String INDEX = "index"; public static final String HTML = "html"; public static final String HTML_EXTENSION = DOT + HTML; public static final String DEFAULT_CONTENT_TYPE = Mime.mimeType(HTML_EXTENSION); public static final String TOOLS = "Tools"; public static Context tools = new MicroContext().with("PathUtilities", new PathUtilities()).with("StringUtils", new StringUtils()); private SiteContext site; private String welcomeFile; public Micro(String path, ServletContext servletContext, String userClassPaths) throws Exception { final File applicationPath = new File(path); final File webInfPath = new File(applicationPath, "WEB-INF"); showBanner(); site = new SiteContext(new MapContext<String>().with(Globals.SERVLET_CONTEXT, servletContext) .with(Globals.SERVLET_PATH_NAME, path).with(Globals.SERVLET_PATH, applicationPath) .with(Globals.WEB_INF_PATH, webInfPath)); welcomeFile = site.getWelcomeFile(); //initialize the classpath StringBuilder cp = new StringBuilder(); if (new File(webInfPath, "/lib").exists()) { cp.append(webInfPath.toString()).append("/lib,"); } if (new File(webInfPath, "/classes").exists()) { cp.append(webInfPath.toString()).append("/classes,"); } if (StringUtils.isNotBlank(userClassPaths)) { cp.append(",").append(userClassPaths); } String resources = ClassUtils.configureClasspath(webInfPath.toString(), StringUtils.split(cp.toString(), "," + File.pathSeparatorChar)); if (log.isDebugEnabled()) { log.info("classpath: " + resources); } configureBSF(); site.loadApplication(webInfPath.getAbsolutePath() + "/config"); // done with the init phase //log.info("\n"); // Registers a new virtual-machine shutdown hook. // For more details, see: http://goo.gl/L9k1YT Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { shutdown(); } }); } public void shutdown() { if (site != null) { log.warn("Shutdown procedure started ..."); site.shutdown(); log.info("Shutdown completed."); } } public RackResponse call(Context<String> input) { MicroContext context = new MicroContext<String>(); //try { input.with(Globals.SITE, site); input.with(Rack.RACK_LOGGER, log); String pathInfo = input.get(Rack.PATH_INFO); context.with(Globals.RACK_INPUT, input).with(Globals.SITE, site).with(Rack.RACK_LOGGER, log) .with(Globals.LOG, log).with(Globals.REQUEST, context.getRequest()) .with(Globals.MICRO_ENV, site.getMicroEnv()) // .with(Globals.CONTEXT, context) <-- don't, please! .with(Globals.PARAMS, input.get(Rack.PARAMS)).with(Globals.PARAMS, ParamsFactory.capture(context)) //<- wrap them nicely .with(Globals.SITE, site).with(Globals.PATH_INFO, pathInfo).with(TOOLS, tools); input.with(Globals.CONTEXT, context); // mostly for helping the testing effort context.with(Globals.MICRO_TEMPLATE_ENGINES, new TemplateEngineWrapper(context)); for (Repository repository : site.getRepositoryManager().getRepositories()) { context.with(repository.getName(), repository.getRepositoryWrapper(context)); } RackResponse response = new RackResponse(RackResponseUtils.ReturnCode.OK).withContentType(null) .withContentLength(0); // a la Sinatra, they're doing it right context.setRackResponse(response); try { // inject the Helpers into the current context List<HelperWrapper> helpers = site.getHelperManager().getHelpers(); if (!helpers.isEmpty()) { for (HelperWrapper helper : helpers) { if (helper != null) { context.with(helper.getName(), helper.getInstance(context)); } } } if (site.getFilterManager() != null) { callFilters(site.getFilterManager().getBeforeFilters(), context); } if (!context.isHalt()) { String path = input.get(JRack.PATH_INFO); if (StringUtils.isBlank(path)) { path = input.get(Rack.SCRIPT_NAME); } if (site.getRouteManager() != null) { site.getRouteManager().call(path, context); } // Routes or filters providing their own Views will most probably ask a flow interruption, hence the // next check for isHalt() if (!context.isHalt()) { path = (String) context.get(Globals.PATH); if (path == null) { // user not deciding the PATH path = maybeAppendHtmlToPath(context); context.with(Globals.PATH, path.contains(DOUBLE_SLASH) ? path.replace(DOUBLE_SLASH, SLASH) : path); } final String pathBasedContentType = PathUtilities .extractType((String) context.get(Globals.PATH)); String templateName = StringUtils.defaultString(context.getTemplateName(), RepositoryManager.DEFAULT_TEMPLATE_NAME); Repository defaultRepository = site.getRepositoryManager().getDefaultRepository(); // verify if there is a default repository decided by 3rd party components; controllers, extensions, etc. if (context.getDefaultRepositoryName() != null) { defaultRepository = site.getRepositoryManager() .getRepository(context.getDefaultRepositoryName()); } // calculate the Template name View view = (View) context.get(Globals.VIEW); if (view != null && StringUtils.isNotBlank(view.getTemplate())) { templateName = view.getTemplate(); } else { view = defaultRepository.getView(path); if (view != null && view.getTemplate() != null) { templateName = view.getTemplate(); } } if (!site.isLegacy()) { // Execute the View Filters and Controllers (if any), render it and save it as the context 'yield' var context.put(Globals.YIELD, defaultRepository.getRepositoryWrapper(context).get(path)); } // Render the Default Template. The template will pull out the View via the Globals.YIELD, and the result being // sent out as the Template body merged with the View's own content. The Controllers are executed *before* // rendering the View, any context artifacts being available to the Template as well. Repository templatesRepository = site.getRepositoryManager().getTemplatesRepository(); if (context.getTemplatesRepositoryName() != null) { templatesRepository = site.getRepositoryManager() .getRepository(context.getTemplatesRepositoryName()); } if (templatesRepository != null) { String out = templatesRepository.getRepositoryWrapper(context) .get(templateName + pathBasedContentType); response.withContentLength(out.getBytes(Charset.forName(Globals.UTF8)).length) .withBody(out); } else { throw new FileNotFoundException( String.format("templates repository: %s", context.getTemplatesRepositoryName())); } } if (!context.isHalt()) { if (site.getFilterManager() != null) { callFilters(site.getFilterManager().getAfterFilters(), context); } } } return context.getRackResponse().withContentType(getContentType(context)); } catch (ControllerNotFoundException e) { context.with(Globals.ERROR, e); return badJuju(context, HttpServletResponse.SC_NO_CONTENT, e); } catch (ControllerException e) { context.with(Globals.ERROR, e); return badJuju(context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } catch (FileNotFoundException e) { context.with(Globals.ERROR, e); return badJuju(context, HttpServletResponse.SC_NOT_FOUND, e); } catch (ViewException e) { context.with(Globals.ERROR, e); return badJuju(context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } catch (RedirectException re) { return context.getRackResponse(); } catch (Exception e) { // must think more about this one :( context.with(Globals.ERROR, e); e.printStackTrace(); return badJuju(context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } // Experimental!!!!!! // } finally { // // this is an experimental trick that will save some processing time required by BSF to load // // various engines. // @SuppressWarnings("unchecked") // CloseableThreadLocal<BSFManager> closeableBsfManager = (CloseableThreadLocal<BSFManager>) // context.get(Globals.CLOSEABLE_BSF_MANAGER); // if(closeableBsfManager!=null){ // closeableBsfManager.close(); // } // } } /** * This method will do the following: * - extract the resource file extension for the given path * - check if there are any user defined mime types and use those otherwise * will use the default Mime detection mechanism {@see Mime.mimeType} * - if the response however has the Content-type defined already then the * resulting content type is the one in the response. * * @param context the current context * @return a String representing the content type value */ @SuppressWarnings("unchecked") private String getContentType(MicroContext context) { RackResponse response = context.getRackResponse(); String responseContentType = response.getHeaders().get(Globals.HEADERS_CONTENT_TYPE); final String ext = PathUtilities.extractType((String) context.get(Globals.PATH)); String contentType = Mime.mimeType(ext); if (responseContentType != null) { contentType = responseContentType; } else if (site.getUserMimeTypes() != null && site.getUserMimeTypes().containsKey(ext)) { contentType = site.getUserMimeTypes().get(ext); } // verify the charset Map<String, String[]> params = (Map<String, String[]>) context.get(Globals.PARAMS); if (!CollectionUtils.isEmpty(params) && params.get(Globals.CHARSET) != null && !contentType.contains(Globals.CHARSET)) { contentType = String.format("%s;%s", contentType, params.get(Globals.CHARSET)[0]); } return contentType; } private void configureBSF() { BSFManager.registerScriptingEngine("beanshell", "bsh.util.BeanShellBSFEngine", new String[] { "bsh" }); BSFManager.registerScriptingEngine("groovy", "org.codehaus.groovy.bsf.GroovyEngine", new String[] { "groovy", "gy" }); BSFManager.registerScriptingEngine("jruby19", "org.jruby.embed.bsf.JRubyEngine", new String[] { "ruby", "rb" }); } private void showBanner() { log.info(""); log.info(" _ __ ___ ( ) ___ _ __ ___ "); log.info("| '_ ` _ \\| |/ __| '__/ _ \\ "); log.info("| | | | | | | (__| | | (_) |"); log.info("|_| |_| |_|_|\\___|_| \\___/ (" + Globals.VERSION + ")"); log.info("= a modular micro MVC Java framework"); log.info(""); } public SiteContext getSite() { return site; } // todo improve me, por favor private RackResponse badJuju(MicroContext context, int status, Exception e) { context.with(Globals.ERROR, e); try { String baddie = site.getRepositoryManager().getTemplatesRepository().getRepositoryWrapper(context) .get(status + HTML_EXTENSION); return new RackResponse(status).withContentType(Mime.mimeType(HTML_EXTENSION)) .withContentLength(baddie.getBytes(Charset.forName(Globals.UTF8)).length).withBody(baddie); } catch (Exception e1) { return new RackResponse(status).withHeader("Content-Type", (Mime.mimeType(".html"))) .withBody(Globals.EMPTY_STRING).withContentLength(0); } } private String maybeAppendHtmlToPath(MicroContext context) { final Context rackInput = context.getRackInput(); String path = (String) rackInput.get(JRack.PATH_INFO); if (StringUtils.isBlank(path)) { path = (String) rackInput.get(Rack.SCRIPT_NAME); } if (welcomeFile.isEmpty() || path.contains(HTML)) { return path; } if (path.lastIndexOf(DOT) == -1) { if (!path.endsWith(SLASH)) { path = path + SLASH; } String welcomeFile = StringUtils.defaultString(site.getWelcomeFile(), INDEX + DOT + HTML); path = path + welcomeFile; } context.with(Globals.PATH_INFO, path); return path; } private void callFilters(List<Filter> filters, MicroContext context) { if (!filters.isEmpty()) { for (Filter filter : filters) { try { filter.call(context); if (context.isHalt()) { break; } } catch (Exception e) { e.printStackTrace(); log.error(String.format("Filter: %s, error: %s", filter, e.getMessage())); } } } } public boolean isFilterAddsWelcomeFile() { final String aTrue = "true"; return welcomeFile.equalsIgnoreCase(aTrue); } public static class PoweredBy { public String getName() { return Globals.FRAMEWORK_NAME; } public String getVersion() { return Globals.VERSION; } public String toString() { return String.format("%s version: %s", getName(), getVersion()); } } }