/*******************************************************************************
* Copyright 2013 Ivan Shubin http://mindengine.net
*
* 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 net.mindengine.blogix;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import javax.servlet.ServletOutputStream;
import net.mindengine.blogix.compiler.BlogixClassLoader;
import net.mindengine.blogix.config.BlogixConfig;
import net.mindengine.blogix.markup.DummyMarkup;
import net.mindengine.blogix.markup.Markup;
import net.mindengine.blogix.utils.BlogixFileUtils;
import net.mindengine.blogix.web.DefaultViewResolver;
import net.mindengine.blogix.web.RouteInvoker;
import net.mindengine.blogix.web.ViewResolver;
import net.mindengine.blogix.web.routes.Route;
import net.mindengine.blogix.web.routes.RouteURL;
import net.mindengine.blogix.web.routes.RoutesContainer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ClassUtils;
public class Blogix {
private static final String PUBLIC = "/public/";
private static final ClassLoader[] CLASS_LOADERS = new ClassLoader[]{Blogix.class.getClassLoader(), new BlogixClassLoader("src")};
private static final HashMap<String, String> EMPTY_CONTROLLER_ARGS = new HashMap<String, String>();
public static final String VIEW_MAIN_PATH = "view/";
private BlogixConfig config = BlogixConfig.getConfig();
private ViewResolver viewResolver = new DefaultViewResolver(this, defaultClassLoaders());
private RoutesContainer routesContainer = new RoutesContainer(defaultClassLoaders());
private RouteInvoker routeInvoker = new RouteInvoker();
private Markup markup;
public Blogix() throws IOException, URISyntaxException {
try {
routesContainer.load( BlogixFileUtils.findFile( "conf/routes" ), config.getDefaultControllerPackages(), config.getDefaultProviderPackages() );
}
catch (Exception e) {
throw new RuntimeException("Could not load properties", e);
}
}
public static ClassLoader[] defaultClassLoaders() {
return CLASS_LOADERS;
}
public RoutesContainer getRoutesContainer() {
return routesContainer;
}
public void setRoutesContainer(RoutesContainer routesContainer) {
this.routesContainer = routesContainer;
}
public RouteInvoker getRouteInvoker() {
return routeInvoker;
}
public void setRouteInvoker(RouteInvoker routeInvoker) {
this.routeInvoker = routeInvoker;
}
public void processUri(String uri, ServletOutputStream outputStream) throws Exception {
if (!isPublicResource(uri)) {
processRoute(uri, outputStream);
}
else {
processPublicResource(uri, outputStream);
}
}
private void processPublicResource(String uri, ServletOutputStream outputStream) throws FileNotFoundException, IOException {
//removing '/public/' part
uri = uri.substring(PUBLIC.length());
File publicFile = findPublicResource(uri);
IOUtils.copy(new FileInputStream((File)publicFile), outputStream);
}
private File findPublicResource(String uri) throws FileNotFoundException {
String publicDirPath = config.getPublicPath();
String fullPath = publicDirPath + File.separator + uri;
return BlogixFileUtils.findFile(fullPath);
}
private boolean isPublicResource(String uri) {
if (uri.startsWith(PUBLIC)) {
return true;
}
else return false;
}
private void processRoute(String uri, ServletOutputStream outputStream) throws Exception {
Route route = findRouteMatchingUri(uri);
if ( route != null ) {
Map<String, String> controllerArgs = createParametersMap(route.getUrl(), uri);
processRoute(uri, route, controllerArgs, outputStream);
}
else throw new IllegalArgumentException("Cannot find route for uri: " + uri);
}
public void processRoute(String uri, Route route, Map<String,String> controllerArgs, OutputStream outputStream) throws Exception {
Object objectModel = null;
if ( route.getController() != null ) {
objectModel = routeInvoker.invokeRoute(route, controllerArgs);
}
String view = route.getView();
if (view != null) {
Map<String, Object> routeModel = createRouteModel(route, uri);
viewResolver.resolveViewAndRender(routeModel, objectModel, view, outputStream);
}
else resolveViewless(objectModel, outputStream);
}
/**
* Resolves routes which do not have views. Can only resolve controllers which return file type.
* @param objectModel
* @param outputStream
* @throws IOException
* @throws FileNotFoundException
*/
private void resolveViewless(Object objectModel, OutputStream outputStream) throws FileNotFoundException, IOException {
if (objectModel != null && objectModel instanceof File) {
File file = (File) objectModel;
IOUtils.copy(new FileInputStream(file), outputStream);
}
else {
throw new IllegalArgumentException("Can't render viewless route. Controller did not return a file");
}
}
public void processRoute(String uri, Route route, OutputStream outputStream) throws Exception {
processRoute(uri, route, EMPTY_CONTROLLER_ARGS, outputStream);
}
@SuppressWarnings("unchecked")
public Map<String, Object> convertObjectToMapModel(Map<String, Object> routeModel, Object model) {
Map<String, Object> modelMap = new HashMap<String, Object>();
if (routeModel != null) {
modelMap.putAll(routeModel);
}
if ( model != null ) {
if ( model instanceof Map ) {
Map<Object, Object> map = (Map<Object, Object>)model;
for ( Map.Entry<Object, Object> entry : map.entrySet() ) {
modelMap.put(entry.getKey().toString(), entry.getValue());
}
}
else {
modelMap.put("model", model);
}
}
addMarkupToModel(modelMap);
addUserCustomVariables(modelMap);
return modelMap;
}
private void addMarkupToModel(Map<String, Object> modelMap) {
if (!modelMap.containsKey("markup")) {
modelMap.put("markup", getMarkup());
}
}
private static void addUserCustomVariables(Map<String, Object> modelMap) {
Map<String, String> properties = BlogixConfig.getConfig().getUserCustomProperties();
modelMap.putAll(properties);
}
public Markup getMarkup() {
if (markup == null) {
try {
markup = createMarkupFromProperties();
} catch (Exception e) {
throw new RuntimeException("Cannot create markup", e);
}
}
return markup;
}
private Markup createMarkupFromProperties() throws ClassNotFoundException, IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
String markupClassPath = config.getMarkupClass();
if (noMarkupDefined(markupClassPath)) {
return new DummyMarkup();
}
else {
return createMarkupFromClassPath(markupClassPath);
}
}
private Markup createMarkupFromClassPath(String markupClassPath) throws ClassNotFoundException, IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Class<?> configuredMarkupClass = Class.forName(markupClassPath);
if (!implementsInterface(configuredMarkupClass, Markup.class)) {
throw new RuntimeException("Class " + configuredMarkupClass.getName() + " does not implement " + Markup.class.getName());
}
return (Markup) configuredMarkupClass.getConstructor().newInstance();
}
private boolean noMarkupDefined(String markupClassPath) {
return markupClassPath == null || markupClassPath.trim().isEmpty();
}
private boolean implementsInterface(Class<?> clazz, Class<Markup> interfaceClass) {
return ClassUtils.getAllInterfaces(clazz).contains(interfaceClass);
}
private Map<String, String> createParametersMap(RouteURL url, String uri) {
if ( !uri.endsWith("/") ) {
uri = uri + "/";
}
if ( url.getParameters() != null && !url.getParameters().isEmpty() ) {
Matcher matcher = url.asRegexPattern().matcher(uri);
if ( matcher.find() ) {
if ( matcher.groupCount() >= url.getParameters().size()) {
Map<String, String> parametersMap = new HashMap<String, String>();
int i = 0;
for (String parameter : url.getParameters() ) {
i++;
parametersMap.put(parameter, matcher.group(i));
}
return parametersMap;
}
}
throw new IllegalArgumentException("Can't extract controller arguments from uri: " + uri);
}
return Collections.emptyMap();
}
private Map<String, Object> createRouteModel(Route route, String uri) {
BlogixData data = new BlogixData();
data.setCurrentUri(uri);
data.setWayToRoot(findWayToRootFromUri(uri));
Map<String, Object> routeModel = route.getModel();
if (routeModel == null) {
routeModel = new HashMap<String, Object>();
}
routeModel.put("blogix", data);
return routeModel;
}
private String findWayToRootFromUri(String uri) {
int amountOfSlashes = countAmountOfSlashesInUri(uri);
if (amountOfSlashes > 1) {
StringBuffer wayToRoot = new StringBuffer("..");
for (int i = 1; i < amountOfSlashes - 1; i++) {
wayToRoot.append("/..");
}
return wayToRoot.toString();
}
else return ".";
}
private int countAmountOfSlashesInUri(String uri) {
int amountOfSlashes = 0;
for (int i = 0; i < uri.length(); i++) {
if (uri.charAt(i) == '/') {
amountOfSlashes++;
}
}
return amountOfSlashes;
}
private Route findRouteMatchingUri(String uri) {
return routesContainer.findRouteMatchingUri(uri);
}
}