ca.simplegames.micro.Micro.java Source code

Java tutorial

Introduction

Here is the source code for ca.simplegames.micro.Micro.java

Source

/*
 * 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());
        }
    }
}