/*
* Copyright (c)2012. Florin T.PATRASCU
*
* 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.cache.MicroCacheManager;
import ca.simplegames.micro.controllers.ControllerException;
import ca.simplegames.micro.controllers.ControllerManager;
import ca.simplegames.micro.controllers.ControllerNotFoundException;
import ca.simplegames.micro.extensions.ExtensionsManager;
import ca.simplegames.micro.filters.FilterManager;
import ca.simplegames.micro.helpers.HelperManager;
import ca.simplegames.micro.helpers.HelperWrapper;
import ca.simplegames.micro.repositories.RepositoryManager;
import ca.simplegames.micro.route.RouteManager;
import ca.simplegames.micro.templates.TemplateEnginesManager;
import ca.simplegames.micro.utils.CloseableThreadLocal;
import ca.simplegames.micro.utils.PathUtilities;
import ca.simplegames.micro.utils.StringUtils;
import org.apache.bsf.BSFEngine;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.jrack.Context;
import org.jrack.context.MapContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import javax.servlet.ServletContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.util.List;
import java.util.Map;
/**
* This class contains configuration information for a particular Micro
* site.
*
* @author <a href="mailto:florin.patrascu@gmail.com">Florin T.PATRASCU</a>
* @since $Revision$ (created: 2012-12-19 11:13 AM)
*/
public class SiteContext extends MapContext {
private Logger log = LoggerFactory.getLogger(getClass());
private MicroCacheManager cacheManager;
private RepositoryManager repositoryManager;
private ControllerManager controllerManager;
private FilterManager filterManager = new FilterManager();
private HelperManager helperManager;
private Map appConfig;
private File webInfPath;
private RouteManager routeManager;
private ExtensionsManager extensionsManager;
private String microEnv;
private TemplateEnginesManager templateEnginesManager;
private Map<String, String> userMimeTypes = null;
private String welcomeFile = "index.html";
private File applicationConfigPath;
public SiteContext(Context<String> env) {
for (Map.Entry<String, Object> entry : env) {
with(entry.getKey(), entry.getValue());
}
}
public ServletContext getServletContext() {
return (ServletContext) map.get(Globals.SERVLET_CONTEXT);
}
/**
* load the application config and execute the application script if present
*
* @param configPath the absolute path to "WEB-INF/config/micro-config.yml"
* @return itself
*/
@SuppressWarnings("unchecked")
public SiteContext loadApplication(String configPath) throws Exception {
File config = new File(configPath, "micro-config.yml");
applicationConfigPath = new File(configPath);
webInfPath = (File) get(Globals.WEB_INF_PATH);
if (config.exists()) {
with(Globals.MICRO_CONFIG_PATH, config);
try {
appConfig = (Map) new Yaml().load(new FileInputStream(config));
with(Globals.MICRO_CACHE_CONFIG, appConfig.get("cache"));
microEnv = StringUtils.defaultString(appConfig.get(Globals.MICRO_ENV), Globals.DEVELOPMENT);
// and just check if System env vars overwrite our settings; useful when running @Heroku, etc.
microEnv = StringUtils.defaultString(System.getenv(Globals.MICRO_ENV), microEnv);
// or if there is a -DMICRO_ENV=... in the VM ARGS
microEnv = StringUtils.defaultString(System.getProperty(Globals.MICRO_ENV), microEnv);
// - Cache
cacheManager = new MicroCacheManager(this);
log.info(String.format("Application name: %s", StringUtils.defaultString(appConfig.get("name"), "")));
log.info(String.format(" description: %s", StringUtils.defaultString(appConfig.get("description"), "")));
log.info("Template engines:");
templateEnginesManager = new TemplateEnginesManager(this, appConfig);
log.info("Repositories:");
// - Repositories
repositoryManager = new RepositoryManager(this);
// - Controllers
controllerManager = new ControllerManager(this, (Map<String, Object>) appConfig.get("controllers"));
// - Filters
// log.info("Filters:");
File filtersConfig = new File(configPath, "filters.yml");
if (filtersConfig.exists()) {
filterManager = new FilterManager(this,
(List<Map<String, Object>>) new Yaml().load(new FileInputStream(filtersConfig)));
}
// The strategy used for loading Helpers and Extensions will eventually be just one, currently
// exploring different methods for managing them, hence the redundancy, sorry for that.
// - Loading the Extensions
File extensionsDirectory = new File(configPath, "extensions");
if (extensionsDirectory.exists() && extensionsDirectory.isDirectory()) {
// load extensions
extensionsManager = new ExtensionsManager(this, files(extensionsDirectory, ".yml"));
}
// - Helpers
//log.info("Helpers:");
File helpersDirectory = new File(configPath, "helpers");
helperManager = new HelperManager();
if (helpersDirectory.exists() && helpersDirectory.isDirectory()) {
for (File file : files(helpersDirectory, ".yml")) {
Map<String, Object> yaml = (Map<String, Object>) new Yaml().load(new FileInputStream(file));
HelperWrapper helper = helperManager.addHelper(PathUtilities.extractName(file), yaml);
//log.info(String.format(" %s: %s", helper.getName(), file.getAbsolutePath()));
}
}
// - Routes
File routesConfig = new File(configPath, "routes.yml");
if (routesConfig.exists()) {
//routeManager = new RouteManager(this,
// (List<Map<String, Object>>) new Yaml().load(new FileInputStream(routesConfig)));
routeManager = new RouteManager(this, routesConfig);
}
MicroContext context = new MicroContext();
context.with(Globals.SITE, this)
.with(Globals.WEB_APP_NAME, StringUtils.defaultString(appConfig.get("name"), "<name your app>"))
.with(Globals.WEB_APP_DESCRIPTION, StringUtils.defaultString(appConfig.get("description"),
"<describe your app>"));
// load the user mime types, if any
userMimeTypes = (Map<String, String>) appConfig.get("mime_types");
//add anything else to the context? If no, then execute the app' startup controller:
controllerManager.execute(findApplicationStartupScript(configPath), context, appConfig);
if (context.get(Globals.CLOSEABLE_BSF_MANAGER) != null) {
((CloseableThreadLocal) context.get(Globals.CLOSEABLE_BSF_MANAGER)).close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (appConfig.get("welcome_file") != null) {
welcomeFile = (String) appConfig.get("welcome_file");
}
log.info(String.format("Welcome file is: '%s'", welcomeFile));
log.info(String.format("Application running in: '%s' mode", microEnv));
} else {
log.error("Application running in an unknown mode, missing configuration.");
}
return this;
}
public File[] files(File dir, final String withExtension) {
return dir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String filename) {
return filename.endsWith(withExtension);
}
});
}
private String findApplicationStartupScript(String configPath) {
return findSpecialApplicationScripts(configPath, "application");
}
private String findApplicationShutdownScript(String configPath) {
return findSpecialApplicationScripts(configPath, "shutdown");
}
private String findSpecialApplicationScripts(String configPath, String scriptName) {
String actionPath = Globals.EMPTY_STRING;
for (String ext : new String[]{"bsh", "rb", "js"}) {
File applicationScript = new File(configPath, String.format("%s.%s", scriptName, ext));
if (applicationScript.exists()) {
actionPath = applicationScript.getAbsolutePath();
break;
}
}
return actionPath;
}
public Logger getLog() {
return log;
}
public MicroCacheManager getCacheManager() {
return cacheManager;
}
public ControllerManager getControllerManager() {
return controllerManager;
}
public Map getAppConfig() {
return appConfig;
}
public FilterManager getFilterManager() {
return filterManager;
}
public RepositoryManager getRepositoryManager() {
return repositoryManager;
}
public File getWebInfPath() {
return webInfPath != null ? webInfPath : new File(Globals.EMPTY_STRING);
}
public RouteManager getRouteManager() {
return routeManager;
}
/**
* @return one of these running modes: development, production or test
*/
public String getMicroEnv() {
return StringUtils.defaultString(microEnv, Globals.DEVELOPMENT).trim();
}
// true if Micro runs in development mode
public boolean isDevelopment() {
return getMicroEnv().equalsIgnoreCase(Globals.DEVELOPMENT);
}
// true if Micro runs in production mode
public boolean isProduction() {
return getMicroEnv().equalsIgnoreCase(Globals.PRODUCTION);
}
// true if Micro runs in test mode
public boolean isTest() {
return getMicroEnv().equalsIgnoreCase(Globals.TEST);
}
/**
* a simple method that can be used as an ad-hoc scripting engine.
* <p/>
* Example:
* engine = site.getBSFEngine("beanshell", context, Collections.singletonMap("foo", "bar"));
* engine.exec("complexCalculus", 0, 0, "one = 1 * 1;"); // :P
* <p/>
* Check this discussion: http://goo.gl/D8m9g, about the execution scope and if the BSFEngine can be
* reused.
*
* @param language a valid BSF language, example: 'beanshell'
* @param context a MicroContext that can be used to transmit parameters
* @param configuration an optional Map containing configuration elements
* @param log a logger that can be used by the client code
* @return a new BSF Engine
* @throws BSFException if the Engine cannot be created
*/
public BSFEngine getBSFEngine(String language, MicroContext context, Map configuration, Logger log) throws BSFException {
//@SuppressWarnings("unchecked")
//CloseableThreadLocal<BSFManager> bsfManagerTL = (CloseableThreadLocal<BSFManager>)
// context.get(Globals.CLOSEABLE_BSF_MANAGER);
BSFManager bsfManager;
// if (bsfManagerTL == null) {
// bsfManagerTL = new CloseableThreadLocal<BSFManager>();
// bsfManager = new BSFManager();
// bsfManagerTL.set(bsfManager);
// bsfManager.setClassLoader(this.getClass().getClassLoader());
// bsfManager.declareBean(Globals.SITE, this, SiteContext.class);
// context.with(Globals.CLOSEABLE_BSF_MANAGER, bsfManagerTL);
// } else {
// bsfManager = bsfManagerTL.get();
// }
bsfManager = new BSFManager();
bsfManager.setClassLoader(this.getClass().getClassLoader());
bsfManager.declareBean(Globals.SITE, this, SiteContext.class);
bsfManager.declareBean(Globals.LOG, log, Logger.class);
if (configuration != null) {
bsfManager.declareBean(Globals.CONFIGURATION, configuration, Map.class);
}
bsfManager.declareBean(Globals.CONTEXT, context, MicroContext.class);
// pre-load the engine to make sure we were called right
org.apache.bsf.BSFEngine bsfEngine = null;
bsfEngine = bsfManager.loadScriptingEngine(language);
return bsfEngine;
}
/**
* a simple method that can be used as an ad-hoc scripting engine. This varian is using the logger
* provided by the site.
*
* @param language a valid BSF language, example: 'beanshell'
* @param context a MicroContext that can be used to transmit parameters
* @param configuration an optional Map containing configuration elements
* @return a new BSF Engine
* @throws Exception if the Engine cannot be created
*/
public BSFEngine getBSFEngine(String language, MicroContext context, Map configuration) throws Exception {
return getBSFEngine(language, context, configuration, log);
}
public TemplateEnginesManager getTemplateEnginesManager() {
return templateEnginesManager;
}
/**
* It is about the user defined mime types. If not null, Micro will check first against this model every time it
* has to decide about the response Content-Type
*
* @return A map containing user defined mime types
*/
public Map<String, String> getUserMimeTypes() {
return userMimeTypes;
}
/**
* @return a user defined welcome file or "index.html"
*/
public String getWelcomeFile() {
return welcomeFile;
}
public HelperManager getHelperManager() {
return helperManager;
}
public ExtensionsManager getExtensionsManager() {
return extensionsManager;
}
public File getApplicationPath() {
return (File) get(Globals.SERVLET_PATH);
}
public static String getMicroVersion() {
return Globals.VERSION; // dynamic!
}
public File getApplicationConfigPath() {
return applicationConfigPath;
}
// convenient group of setters that can be used when testing or for
// other specific requirements.
public void setCacheManager(MicroCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setRepositoryManager(RepositoryManager repositoryManager) {
this.repositoryManager = repositoryManager;
}
public void setControllerManager(ControllerManager controllerManager) {
this.controllerManager = controllerManager;
}
public void setFilterManager(FilterManager filterManager) {
this.filterManager = filterManager;
}
public void setHelperManager(HelperManager helperManager) {
this.helperManager = helperManager;
}
public void setExtensionsManager(ExtensionsManager extensionsManager) {
this.extensionsManager = extensionsManager;
}
public void setMicroEnv(String microEnv) {
this.microEnv = microEnv;
}
/**
* this method notifies the appropriate Micro managers to shutdown
*/
public void shutdown() {
MicroContext context = new MicroContext();
context.with(Globals.SITE, this);
try {
controllerManager.execute(
findApplicationShutdownScript(applicationConfigPath.getAbsolutePath()), context, appConfig);
} catch (ControllerException e) {
e.printStackTrace();
} catch (ControllerNotFoundException e) {
// no problem, the shutdown controller is optional
}
extensionsManager.shutdown();
}
/**
* a convenient wrapper if you're used with the JPublish site object ;)
*
* @param attributeName the attribute name
* @param attributeValue the value associated with this attribute
*/
public SiteContext put(String attributeName, Object attributeValue) {
with(attributeName, attributeValue);
return this;
}
}