/*
* (C) Copyright 2006-2008 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* bstefanescu
*
* $Id$
*/
package org.nuxeo.ecm.webengine.model.impl;
import static org.nuxeo.ecm.webengine.WebEngine.SKIN_PATH_PREFIX_KEY;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.net.SocketException;
import java.security.Principal;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import javax.script.ScriptException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.ExceptionUtils;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.repository.RepositoryManager;
import org.nuxeo.ecm.platform.rendering.api.RenderingException;
import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider;
import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
import org.nuxeo.ecm.webengine.WebEngine;
import org.nuxeo.ecm.webengine.WebException;
import org.nuxeo.ecm.webengine.forms.FormData;
import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
import org.nuxeo.ecm.webengine.login.WebEngineFormAuthenticator;
import org.nuxeo.ecm.webengine.model.AdapterResource;
import org.nuxeo.ecm.webengine.model.Messages;
import org.nuxeo.ecm.webengine.model.Module;
import org.nuxeo.ecm.webengine.model.ModuleResource;
import org.nuxeo.ecm.webengine.model.Resource;
import org.nuxeo.ecm.webengine.model.ResourceType;
import org.nuxeo.ecm.webengine.model.WebContext;
import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException;
import org.nuxeo.ecm.webengine.scripting.ScriptFile;
import org.nuxeo.ecm.webengine.security.PermissionService;
import org.nuxeo.ecm.webengine.session.UserSession;
import org.nuxeo.runtime.api.Framework;
/**
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public abstract class AbstractWebContext implements WebContext {
private static final Log log = LogFactory.getLog(WebContext.class);
// TODO: this should be made configurable through an extension point
public static Locale DEFAULT_LOCALE = Locale.ENGLISH;
public static final String LOCALE_SESSION_KEY = "webengine_locale";
private static boolean isRepositoryDisabled = false;
protected final WebEngine engine;
private UserSession us;
protected final LinkedList<File> scriptExecutionStack;
protected final HttpServletRequest request;
protected final HttpServletResponse response;
protected final Map<String, Object> vars;
protected Resource head;
protected Resource tail;
protected Resource root;
protected Module module;
protected FormData form;
protected String basePath;
private String repoName;
protected AbstractWebContext(HttpServletRequest request, HttpServletResponse response) {
engine = Framework.getLocalService(WebEngine.class);
scriptExecutionStack = new LinkedList<File>();
this.request = request;
this.response = response;
vars = new HashMap<String, Object>();
}
// public abstract HttpServletRequest getHttpServletRequest();
// public abstract HttpServletResponse getHttpServletResponse();
public void setModule(Module module) {
this.module = module;
}
@Override
public Resource getRoot() {
return root;
}
@Override
public void setRoot(Resource root) {
this.root = root;
}
@Override
public <T> T getAdapter(Class<T> adapter) {
if (CoreSession.class == adapter) {
return adapter.cast(getCoreSession());
} else if (Principal.class == adapter) {
return adapter.cast(getPrincipal());
} else if (Resource.class == adapter) {
return adapter.cast(tail());
} else if (WebContext.class == adapter) {
return adapter.cast(this);
} else if (Module.class == adapter) {
return adapter.cast(module);
} else if (WebEngine.class == adapter) {
return adapter.cast(engine);
}
return null;
}
@Override
public Module getModule() {
return module;
}
@Override
public WebEngine getEngine() {
return engine;
}
@Override
public UserSession getUserSession() {
if (us == null) {
us = UserSession.getCurrentSession(request);
}
return us;
}
@Override
public CoreSession getCoreSession() {
if (StringUtils.isNotBlank(repoName)) {
return SessionFactory.getSession(request, repoName);
} else {
return SessionFactory.getSession(request);
}
}
@Override
public Principal getPrincipal() {
return request.getUserPrincipal();
}
@Override
public HttpServletRequest getRequest() {
return request;
}
public HttpServletResponse getResponse() {
return response;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public String getModulePath() {
return head.getPath();
}
@Override
public String getMessage(String key) {
Messages messages = module.getMessages();
try {
return messages.getString(key, getLocale().getLanguage());
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public String getMessage(String key, Object... args) {
Messages messages = module.getMessages();
try {
String msg = messages.getString(key, getLocale().getLanguage());
if (args != null && args.length > 0) {
// format the string using given args
msg = MessageFormat.format(msg, args);
}
return msg;
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public String getMessage(String key, List<Object> args) {
Messages messages = module.getMessages();
try {
String msg = messages.getString(key, getLocale().getLanguage());
if (args != null && args.size() > 0) {
// format the string using given args
msg = MessageFormat.format(msg, args.toArray());
}
return msg;
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public String getMessageL(String key, String language) {
Messages messages = module.getMessages();
try {
return messages.getString(key, language);
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public String getMessageL(String key, String locale, Object... args) {
Messages messages = module.getMessages();
try {
String msg = messages.getString(key, locale);
if (args != null && args.length > 0) {
// format the string using given args
msg = MessageFormat.format(msg, args);
}
return msg;
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public String getMessageL(String key, String locale, List<Object> args) {
Messages messages = module.getMessages();
try {
String msg = messages.getString(key, locale);
if (args != null && !args.isEmpty()) {
// format the string using given args
msg = MessageFormat.format(msg, args.toArray());
}
return msg;
} catch (MissingResourceException e) {
return '!' + key + '!';
}
}
@Override
public Locale getLocale() {
LocaleProvider localeProvider = Framework.getLocalService(LocaleProvider.class);
if (localeProvider != null && request.getUserPrincipal() != null) {
Locale userPrefLocale = localeProvider.getLocale(getCoreSession());
if (userPrefLocale != null) {
return userPrefLocale;
}
}
UserSession us = getUserSession();
if (us != null) {
Object locale = us.get(LOCALE_SESSION_KEY);
if (locale instanceof Locale) {
return (Locale) locale;
}
}
// take the one on request
Locale locale = request.getLocale();
return locale == null ? DEFAULT_LOCALE : locale;
}
@Override
public void setLocale(Locale locale) {
UserSession us = getUserSession();
if (us != null) {
us.put(LOCALE_SESSION_KEY, locale);
}
}
@Override
public Resource newObject(String typeName, Object... args) {
ResourceType type = module.getType(typeName);
if (type == null) {
throw new WebResourceNotFoundException("No Such Object Type: " + typeName);
}
return newObject(type, args);
}
@Override
public Resource newObject(ResourceType type, Object... args) {
Resource obj = type.newInstance(type.getResourceClass(), this);
try {
obj.initialize(this, type, args);
} finally {
// we must be sure the object is pushed even if an error occurred
// otherwise we may end up with an empty object stack and we will
// not be able to
// handle errors based on objects handleError() method
push(obj);
}
return obj;
}
@Override
public AdapterResource newAdapter(Resource ctx, String serviceName, Object... args) {
return (AdapterResource)newObject(module.getAdapter(ctx, serviceName), args);
}
@Override
public void setProperty(String key, Object value) {
vars.put(key, value);
}
// TODO: use FormData to get query params?
@Override
public Object getProperty(String key) {
Object value = getUriInfo().getPathParameters().getFirst(key);
if (value == null) {
value = request.getParameter(key);
if (value == null) {
value = vars.get(key);
}
}
return value;
}
@Override
public Object getProperty(String key, Object defaultValue) {
Object value = getProperty(key);
return value == null ? defaultValue : value;
}
@Override
public String getCookie(String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
@Override
public String getCookie(String name, String defaultValue) {
String value = getCookie(name);
return value == null ? defaultValue : value;
}
@Override
public FormData getForm() {
if (form == null) {
form = new FormData(request);
}
return form;
}
@Override
public String getBasePath() {
if (basePath == null) {
String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH);
if (",".equals(webenginePath)) {
// when the parameter is empty, request.getHeader return ',' on
// apache server.
webenginePath = "";
}
basePath = webenginePath != null ? webenginePath : getDefaultBasePath();
}
return basePath;
}
private String getDefaultBasePath() {
StringBuilder buf = new StringBuilder(request.getRequestURI().length());
String path = request.getContextPath();
if (path == null) {
path = "/nuxeo/site"; // for testing
}
buf.append(path).append(request.getServletPath());
if ("/".equals(path)) {
return "";
}
int len = buf.length();
if (len > 0 && buf.charAt(len - 1) == '/') {
buf.setLength(len - 1);
}
return buf.toString();
}
@Override
public String getBaseURL() {
StringBuffer sb = request.getRequestURL();
int p = sb.indexOf(getBasePath());
if (p > -1) {
return sb.substring(0, p);
}
return sb.toString();
}
@Override
public StringBuilder getServerURL() {
StringBuilder url = new StringBuilder(VirtualHostHelper.getServerURL(request));
if (url.toString().endsWith("/")) {
url.deleteCharAt(url.length() - 1);
}
return url;
}
@Override
public String getURI() {
return request.getRequestURI();
}
@Override
public String getURL() {
StringBuffer sb = request.getRequestURL();
if (sb.charAt(sb.length() - 1) == '/') {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
public StringBuilder getUrlPathBuffer() {
StringBuilder buf = new StringBuilder(getBasePath());
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
buf.append(pathInfo);
}
return buf;
}
@Override
public String getUrlPath() {
return getUrlPathBuffer().toString();
}
@Override
public String getLoginPath() {
StringBuilder buf = getUrlPathBuffer();
int len = buf.length();
if (len > 0 && buf.charAt(len - 1) == '/') { // remove trailing /
buf.setLength(len - 1);
}
buf.append(WebEngineFormAuthenticator.LOGIN_KEY);
return buf.toString();
}
/**
* This method is working only for root objects that implement {@link ModuleResource}
*/
@Override
public String getUrlPath(DocumentModel document) {
return ((ModuleResource) head).getLink(document);
}
@Override
public Log getLog() {
return log;
}
/* object stack API */
@Override
public Resource push(Resource rs) {
rs.setPrevious(tail);
if (tail != null) {
tail.setNext(rs);
tail = rs;
} else {
head = tail = rs;
}
return rs;
}
@Override
public Resource pop() {
if (tail == null) {
return null;
}
Resource rs = tail;
if (tail == head) {
head = tail = null;
} else {
tail = rs.getPrevious();
tail.setNext(null);
}
rs.dispose();
return rs;
}
@Override
public Resource tail() {
return tail;
}
@Override
public Resource head() {
return head;
}
/** template and script resolver */
@Override
public ScriptFile getFile(String path) {
if (path == null || path.length() == 0) {
return null;
}
char c = path.charAt(0);
if (c == '.') { // local path - use the path stack to resolve it
File file = getCurrentScriptDirectory();
if (file != null) {
try {
// get the file local path - TODO this should be done in
// ScriptFile?
file = new File(file, path).getCanonicalFile();
if (file.isFile()) {
return new ScriptFile(file);
}
} catch (IOException e) {
throw WebException.wrap(e);
}
// try using stacked roots
String rootPath = engine.getRootDirectory().getAbsolutePath();
String filePath = file.getAbsolutePath();
path = filePath.substring(rootPath.length());
} else {
log.warn("Relative path used but there is any running script");
path = new Path(path).makeAbsolute().toString();
}
}
return module.getFile(path);
}
public void pushScriptFile(File file) {
if (scriptExecutionStack.size() > 64) { // stack limit
throw new IllegalStateException("Script execution stack overflowed. More than 64 calls between scripts");
}
if (file == null) {
throw new IllegalArgumentException("Cannot push a null file");
}
scriptExecutionStack.add(file);
}
public File popScriptFile() {
int size = scriptExecutionStack.size();
if (size == 0) {
throw new IllegalStateException("Script execution stack underflowed. No script path to pop");
}
return scriptExecutionStack.remove(size - 1);
}
public File getCurrentScriptFile() {
int size = scriptExecutionStack.size();
if (size == 0) {
return null;
}
return scriptExecutionStack.get(size - 1);
}
public File getCurrentScriptDirectory() {
int size = scriptExecutionStack.size();
if (size == 0) {
return null;
}
return scriptExecutionStack.get(size - 1).getParentFile();
}
/* running scripts and rendering templates */
@Override
public void render(String template, Writer writer) {
render(template, null, writer);
}
@Override
public void render(String template, Object ctx, Writer writer) {
ScriptFile script = getFile(template);
if (script != null) {
render(script, ctx, writer);
} else {
throw new WebResourceNotFoundException("Template not found: " + template);
}
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void render(ScriptFile script, Object ctx, Writer writer) {
Map map = null;
if (ctx instanceof Map) {
map = (Map) ctx;
}
try {
String template = script.getURL();
Map<String, Object> bindings = createBindings(map);
if (log.isDebugEnabled()) {
log.debug("## Rendering: " + template);
}
pushScriptFile(script.getFile());
engine.getRendering().render(template, bindings, writer);
} catch (IOException | RenderingException e) {
Throwable cause = ExceptionUtils.getRootCause(e);
if (cause instanceof SocketException) {
log.debug("Output socket closed: failed to write response", e);
return;
}
throw WebException.wrap("Failed to render template: "
+ (script == null ? script : script.getAbsolutePath()), e);
} finally {
if (!scriptExecutionStack.isEmpty()) {
popScriptFile();
}
}
}
@Override
public Object runScript(String script) {
return runScript(script, null);
}
@Override
public Object runScript(String script, Map<String, Object> args) {
ScriptFile sf = getFile(script);
if (sf != null) {
return runScript(sf, args);
} else {
throw new WebResourceNotFoundException("Script not found: " + script);
}
}
@Override
public Object runScript(ScriptFile script, Map<String, Object> args) {
try {
pushScriptFile(script.getFile());
return engine.getScripting().runScript(script, createBindings(args));
} catch (WebException e) {
throw e;
} catch (ScriptException e) {
throw WebException.wrap("Failed to run script " + script, e);
} finally {
if (!scriptExecutionStack.isEmpty()) {
popScriptFile();
}
}
}
@Override
public boolean checkGuard(String guard) throws ParseException {
return PermissionService.parse(guard).check(this);
}
public Map<String, Object> createBindings(Map<String, Object> vars) {
Map<String, Object> bindings = new HashMap<String, Object>();
if (vars != null) {
bindings.putAll(vars);
}
initializeBindings(bindings);
return bindings;
}
@Override
public Resource getTargetObject() {
Resource t = tail;
while (t != null) {
if (!t.isAdapter()) {
return t;
}
t = t.getPrevious();
}
return null;
}
@Override
public AdapterResource getTargetAdapter() {
Resource t = tail;
while (t != null) {
if (t.isAdapter()) {
return (AdapterResource) t;
}
t = t.getPrevious();
}
return null;
}
protected void initializeBindings(Map<String, Object> bindings) {
Resource obj = getTargetObject();
bindings.put("Context", this);
bindings.put("Module", module);
bindings.put("Engine", engine);
bindings.put("Runtime", Framework.getRuntime());
bindings.put("basePath", getBasePath());
bindings.put("skinPath", getSkinPathPrefix());
bindings.put("contextPath", VirtualHostHelper.getContextPathProperty());
bindings.put("Root", root);
if (obj != null) {
bindings.put("This", obj);
DocumentModel doc = obj.getAdapter(DocumentModel.class);
if (doc != null) {
bindings.put("Document", doc);
}
Resource adapter = getTargetAdapter();
if (adapter != null) {
bindings.put("Adapter", adapter);
}
}
if (!isRepositoryDisabled && getPrincipal() != null) {
bindings.put("Session", getCoreSession());
}
}
private String getSkinPathPrefix() {
if (Framework.getProperty(SKIN_PATH_PREFIX_KEY) != null) {
return module.getSkinPathPrefix();
}
String webenginePath = request.getHeader(NUXEO_WEBENGINE_BASE_PATH);
if (webenginePath == null) {
return module.getSkinPathPrefix();
} else {
return getBasePath() + "/" + module.getName() + "/skin";
}
}
public static boolean isRepositorySupportDisabled() {
return isRepositoryDisabled;
}
/**
* Can be used by the application to disable injecting repository sessions in scripting context. If the application
* is not deploying a repository injecting a repository session will throw exceptions each time rendering is used.
*
* @param isRepositoryDisabled true to disable repository session injection, false otherwise
*/
public static void setIsRepositorySupportDisabled(boolean isRepositoryDisabled) {
AbstractWebContext.isRepositoryDisabled = isRepositoryDisabled;
}
@Override
public void setRepositoryName(String repoName) {
RepositoryManager rm = Framework.getLocalService(RepositoryManager.class);
if (rm.getRepository(repoName) != null) {
this.repoName = repoName;
} else {
throw new IllegalArgumentException("Repository " + repoName + " not found");
}
}
}