org.wings.session.SessionServlet Maven / Gradle / Ivy
/*
* Copyright 2000,2005 wingS development team.
*
* This file is part of wingS (http://wingsframework.org).
*
* wingS is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* Please see COPYING for the complete licence.
*/
package org.wings.session;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.StringTokenizer;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wings.ReloadManager;
import org.wings.RequestURL;
import org.wings.Resource;
import org.wings.SForm;
import org.wings.SFrame;
import org.wings.event.ExitVetoException;
import org.wings.event.SRequestEvent;
import org.wings.externalizer.ExternalizeManager;
import org.wings.externalizer.ExternalizedResource;
import org.wings.io.Device;
import org.wings.io.DeviceFactory;
import org.wings.io.ServletDevice;
import org.wings.io.StringBuilderDevice;
import org.wings.resource.ReloadResource;
import org.wings.resource.StringResource;
import org.wings.resource.UpdateResource;
import org.wings.script.JavaScriptListener;
import org.wings.script.ScriptListener;
/**
* The servlet engine creates for each user a new HttpSession. This
* HttpSession can be accessed by all Serlvets running in the engine. A
* WingServlet creates one wings SessionServlet per HTTPSession and stores
* it in its context.
* As the SessionServlets acts as Wrapper for the WingsServlet, you can
* access from there as used the ServletContext and the HttpSession.
* Additionally the SessionServlet containts also the wingS-Session with
* all important services and the superordinated SFrame. To this SFrame all
* wings-Components and hence the complete application state is attached.
* The developer can access from any place via the SessionManager a
* reference to the wingS-Session. Additionally the SessionServlet
* provides access to the all containing HttpSession.
*
* @author Armin Haaf
*/
public final class SessionServlet
extends HttpServlet
implements HttpSessionBindingListener {
private final transient static Logger log = LoggerFactory.getLogger(SessionServlet.class);
/**
* The parent {@link WingServlet}
*/
protected transient HttpServlet parent = this;
/**
* The session.
*/
private transient /* --- ATTENTION! This disable session serialization! */ Session session;
private boolean firstRequest = true;
/** Refer to comment in {@link #doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} */
private String exitSessionWorkaround;
/**
* Default constructor.
*/
protected SessionServlet() {
}
/**
* Sets the parent servlet contianint this wings session
* servlet (WingsServlet, delegating its requests to the SessionServlet).
*/
protected final void setParent(HttpServlet p) {
if (p != null) parent = p;
}
public final Session getSession() {
return session;
}
/**
* Overrides the session set for setLocaleFromHeader by a request parameter.
* Hence you can force the wings session to adopt the clients Locale.
*/
public final void setLocaleFromHeader(String... args) {
if (args == null)
return;
for (String arg : args) {
try {
session.setLocaleFromHeader(Boolean.valueOf(arg));
} catch (Exception e) {
log.error("setLocaleFromHeader", e);
}
}
}
/**
* The Locale of the current wings session servlet is determined by
* the locale transmitted by the browser. The request parameter
*
LocaleFromHeader
can override the behaviour
* of a wings session servlet to adopt the clients browsers Locale.
*
* @param req The request to determine the local from.
*/
protected final void handleLocale(HttpServletRequest req) {
setLocaleFromHeader(req.getParameterValues("LocaleFromHeader"));
if (session.getLocaleFromHeader()) {
session.determineLocale();
}
}
// jetzt kommen alle Servlet Methoden, die an den parent deligiert
// werden
@Override
public ServletContext getServletContext() {
if (parent != this)
return parent.getServletContext();
else
return super.getServletContext();
}
@Override
public String getInitParameter(String name) {
if (parent != this)
return parent.getInitParameter(name);
else
return super.getInitParameter(name);
}
@Override
public Enumeration getInitParameterNames() {
if (parent != this)
return parent.getInitParameterNames();
else
return super.getInitParameterNames();
}
/**
* Delegates log messages to the according WingsServlet or alternativly
* to the HttpServlet logger.
*
* @param msg The logmessage
*/
@Override
public void log(String msg) {
if (parent != this)
parent.log(msg);
else
super.log(msg);
}
@Override
public String getServletInfo() {
if (parent != this)
return parent.getServletInfo();
else
return super.getServletInfo();
}
@Override
public ServletConfig getServletConfig() {
if (parent != this)
return parent.getServletConfig();
else
return super.getServletConfig();
}
// bis hierhin
/**
* init
*/
public final void init(ServletConfig config,
HttpServletRequest request,
HttpServletResponse response) throws ServletException {
try {
session = new Session(parent);
SessionManager.setSession(session);
// set request.url in session, if used in constructor of wings main classs
//if (request.isRequestedSessionIdValid()) {
// this will fire an event, if the encoding has changed ..
session.setProperty("request.url", new RequestURL("", getSessionEncoding(response)));
//}
session.init(config, request, response);
try {
String mainClassName = config.getInitParameter("wings.mainclass");
Class mainClass = null;
try {
mainClass = Class.forName(mainClassName, true,
Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
// fallback, in case the servlet container fails to set the
// context class loader.
mainClass = Class.forName(mainClassName);
}
mainClass.newInstance();
} catch (Exception ex) {
log.error("could not load wings.mainclass: " +
config.getInitParameter("wings.mainclass"), ex);
throw new ServletException(ex);
}
} finally {
// The session was set by the constructor. After init we
// expect that only doPost/doGet is called, which set the
// session also. So remove it here.
SessionManager.removeSession();
}
}
/**
* this method references to
* {@link #doGet(HttpServletRequest, HttpServletResponse)}
*/
@Override
public final void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
//value chosen to limit denial of service
if (req.getContentLength() > session.getMaxContentLength() * 1024) {
res.setContentType("text/html");
ServletOutputStream out = res.getOutputStream();
out.println("Too big ");
out.println("Error - content length > " +
session.getMaxContentLength() + 'k');
out.println("
");
} else {
doGet(req, res);
}
// sollte man den obigen Block nicht durch folgende Zeile ersetzen?
//throw new RuntimeException("this method must never be called!");
// bsc: Wieso?
}
/**
* Verarbeitet Informationen vom Browser:
*
* - setzt Locale
*
- Dispatch Get Parameter
*
- feuert Form Events
*
* Ist synchronized, damit nur ein Frame gleichzeitig bearbeitet
* werden kann.
*/
@Override
public final synchronized void doGet(HttpServletRequest req,
HttpServletResponse response) {
// Special case: You double clicked i.e. a "logout button"
// First request arrives, second is on hold. First invalidates session and sends redirect as response,
// but browser ignores and expects response in second request. But second request has longer a valid session.
if (session == null) {
try {
response.sendRedirect(exitSessionWorkaround != null ? exitSessionWorkaround : "");
return;
} catch (IOException e) {
log.info("Session exit workaround failed to to IOException (triple click?)");
}
}
SessionManager.setSession(session);
session.setServletRequest(req);
session.setServletResponse(response);
session.fireRequestEvent(SRequestEvent.REQUEST_START);
// in case, the previous thread did not clean up.
SForm.clearArmedComponents();
Device outputDevice = null;
ReloadManager reloadManager = session.getReloadManager();
try {
/*
* The tomcat 3.x has a bug, in that it does not encode the URL
* sometimes. It does so, when there is a cookie, containing some
* tomcat sessionid but that is invalid (because, for instance,
* we restarted the tomcat in-between).
* [I can't think of this being the correct behaviour, so I assume
* it is a bug. ]
*
* So we have to workaround this here: if we actually got the
* session id from a cookie, but it is not valid, we don't do
* the encodeURL() here: we just leave the requestURL as it is
* in the properties .. and this is url-encoded, since
* we had set it up in the very beginning of this session
* with URL-encoding on (see WingServlet::newSession()).
*
* Vice versa: if the requestedSessionId is valid, then we can
* do the encoding (which then does URL-encoding or not, depending
* whether the servlet engine detected a cookie).
* (hen)
*/
RequestURL requestURL = null;
if (req.isRequestedSessionIdValid()) {
requestURL = new RequestURL("", getSessionEncoding(response));
// this will fire an event, if the encoding has changed ..
session.setProperty("request.url", requestURL);
}
if (log.isDebugEnabled()) {
log.debug("Request URL: " + requestURL);
log.debug("HTTP header:");
for (Enumeration en = req.getHeaderNames(); en.hasMoreElements();) {
String header = (String) en.nextElement();
log.debug(" " + header + ": " + req.getHeader(header));
}
}
handleLocale(req);
// The pathInfo addresses the resource
String pathInfo = req.getPathInfo(); // Note: Websphere returns null
here!
if (pathInfo != null && pathInfo.length() > 0) {
// strip of leading /
pathInfo = pathInfo.substring(1);
}
if (log.isDebugEnabled())
log.debug("pathInfo: " + pathInfo);
ResourceMapper mapper = session.getResourceMapper();
// The externalizer is able to handle static and dynamic resources
ExternalizeManager extManager = session.getExternalizeManager();
ExternalizedResource extInfo;
Resource resource;
if (pathInfo == null || pathInfo.length() == 0)
extInfo = extManager.getExternalizedResource(retrieveCurrentRootFrameResource().getId());
else if (mapper != null && (resource = mapper.mapResource(pathInfo)) != null)
extInfo = extManager.getExternalizedResource(resource.getId());
else if (firstRequest) {
extInfo = extManager.getExternalizedResource(retrieveCurrentRootFrameResource().getId());
}
else
extInfo = extManager.getExternalizedResource(pathInfo);
firstRequest = false;
// Special case handling: We request a .html resource of a session which is not accessible.
// This happens some times and leads to a 404, though it should not be possible.
if (extInfo == null && pathInfo != null && (pathInfo.endsWith(".html") || pathInfo.endsWith(".xml"))) {
log.info("Got a request to an invalid .html during a valid session .. redirecting to root frame.");
response.sendRedirect("");
return;
}
if (extInfo != null && extInfo.getObject() instanceof UpdateResource) {
reloadManager.setUpdateMode(true);
String eventEpoch = req.getParameter("event_epoch");
UpdateResource updateResource = (UpdateResource) extInfo.getObject();
updateResource.getFrame().getEventEpoch();
if (eventEpoch != null && !eventEpoch.equals(updateResource.getFrame().getEventEpoch())) {
reloadManager.setUpdateMode(false);
}
} else {
reloadManager.setUpdateMode(false);
}
// Prior to dispatching the actual events we have to detect
// their epoch and inform the dispatcher which will then be
// able to check if the request is valid and processed. If
// this is not the case, we force a complete page reload.
LowLevelEventDispatcher eventDispatcher = session.getDispatcher();
eventDispatcher.setEventEpoch(req.getParameter("event_epoch"));
Enumeration en = req.getParameterNames();
final Cookie[] cookies = req.getCookies();
// handle debug.cookie - read it every time.
session.removeProperty("debug.cookie");
final Collection cookiesToDispatch = new ArrayList<>();
if (cookies != null) {
//handle cookies
for (Cookie cookie : cookies) {
String paramName = cookie.getName();
if ("DEBUG".equals(paramName)) {
// Cookies have a limited length, therefore we copy
// them trustingly into the session.
// Use a Tokenizer for performance.
String paramValue = URLDecoder.decode(cookie.getValue(), "ISO-8859-1");
StringTokenizer tokenizer = new StringTokenizer(paramValue, "|");
String[] values = new String[tokenizer.countTokens()];
for (int j = 0; j < values.length; j++) {
values[j] = tokenizer.nextToken();
}
session.setProperty("debug.cookie", values);
} else {
cookiesToDispatch.add(cookie);
}
}
}
// are there parameters/low level events to dispatch
if (en.hasMoreElements()) {
// only fire DISPATCH_START if we have parameters to dispatch
session.fireRequestEvent(SRequestEvent.DISPATCH_START);
eventDispatcher.startLowLevelEventPhase();
if (cookiesToDispatch != null) {
//dispatch cookies
for (Cookie cookie : cookiesToDispatch) {
String paramName = cookie.getName();
String value = cookie.getValue();
if (log.isDebugEnabled())
log.debug("dispatching cookie " + paramName + " = " + value);
eventDispatcher.dispatch(paramName, value);
}
}
if (log.isDebugEnabled()) {
log.debug("Parameters:");
for (Enumeration e = req.getParameterNames(); e.hasMoreElements();) {
String paramName = (String) e.nextElement();
StringBuilder param = new StringBuilder();
param.append(" ").append(paramName).append(": ");
final String[] values = req.getParameterValues(paramName);
param.append(values != null ? Arrays.toString(values) : "null");
log.debug(param.toString());
}
}
while (en.hasMoreElements()) {
String paramName = (String) en.nextElement();
String[] values = req.getParameterValues(paramName);
//We do not need to dispatch the event epoch since it is already
// handled a few lines above. Furthermore we will not dispatch any
// names that start with an '_' (e.g. _xhrId or parts of XCalendar).
if (paramName.equals("event_epoch") || paramName.startsWith("_")
|| paramName.equals("comet") || paramName.equals("polling")) {
continue;
}
String value = values[0];
// Split the values of the event trigger
if (paramName.equals("event_trigger")) {
int pos = value.indexOf('|');
paramName = value.substring(0, pos);
values = new String[] { value.substring(pos +1) };
}
// Handle form submit via default button
if (paramName.equals("default_button")) {
if (value.equals("undefined")) {
continue;
} else {
paramName = values[0];
values = new String[] { "1" };
}
}
if (log.isDebugEnabled())
log.debug("dispatching " + paramName + " = " + Arrays.asList(values));
eventDispatcher.dispatch(paramName, values);
}
eventDispatcher.endLowLevelEventPhase();
SForm.fireEvents();
// only fire DISPATCH DONE if we have parameters to dispatch
session.fireRequestEvent(SRequestEvent.DISPATCH_DONE);
}
session.fireRequestEvent(SRequestEvent.PROCESS_REQUEST);
eventDispatcher.invokeRunnables();
// if the user chose to exit the session as a reaction on an
// event, we got an URL to redirect after the session.
/*
* where is the right place?
* The right place is
* - _after_ we processed the events
* (e.g. the 'Pressed Exit-Button'-event or gave
* the user the chance to exit this session in the custom
* processRequest())
* - but _before_ the rendering of the page,
* because otherwise an redirect won't work, since we must
* not have sent anything to the output stream).
*/
if (session.getExitAddress() != null) {
try {
session.firePrepareExit();
session.fireRequestEvent(SRequestEvent.REQUEST_END);
String redirectAddress;
if (session.getExitAddress().length() > 0) {
// redirect to user requested URL.
redirectAddress = session.getExitAddress();
} else {
// redirect to a fresh session.
redirectAddress = req.getRequestURL().toString();
if (pathInfo != null) { // Websphere pathinfo is null
// Make sure that the redirect address doesn't contain any path info.
redirectAddress = redirectAddress.substring(0, redirectAddress.length() - pathInfo.length());
}
}
exitSessionWorkaround = redirectAddress;
if (reloadManager.isUpdateMode()) {
ScriptListener listener = new JavaScriptListener(null, null, "location.href='" + redirectAddress + '\'');
ScriptManager.getInstance().addScriptListener(listener);
req.getSession().invalidate(); // calls destroy implicitly
}
else {
response.sendRedirect(redirectAddress);
req.getSession().invalidate(); // calls destroy implicitly
return;
}
} catch (ExitVetoException ex) {
session.exit(null);
} // end of try-catch
}
if (session.getRedirectAddress() != null) {
handleRedirect(response);
return;
}
reloadManager.invalidateFrames();
if (extInfo != null) {
outputDevice = DeviceFactory.createDevice(extInfo);
try {
session.fireRequestEvent(SRequestEvent.DELIVER_START, extInfo);
long startTime = System.currentTimeMillis();
extManager.deliver(extInfo, response, outputDevice);
if (log.isDebugEnabled()) {
log.debug("Rendering time: " + (System.currentTimeMillis() - startTime) + " ms");
}
} finally {
session.fireRequestEvent(SRequestEvent.DELIVER_DONE, extInfo);
}
} else {
handleUnknownResourceRequested(req, response);
}
}
catch (Throwable e) {
log.error("Uncaught Exception", e);
handleException(response, e);
}
finally {
if (session != null) {
session.fireRequestEvent(SRequestEvent.REQUEST_END);
}
if (outputDevice != null) {
try {
outputDevice.close();
} catch (Exception e) {
}
}
/*
* the session might be null due to destroy().
*/
if (session != null) {
reloadManager.clear();
session.setServletRequest(null);
session.setServletResponse(null);
}
// make sure that the session association to the thread is removed
// from the SessionManager
SessionManager.removeSession();
SForm.clearArmedComponents();
}
}
/**
* Searches the current session for the root HTML frame and returns the Resource
* representing this root HTML frame (i.e. for you to retrieve the externalizer id
* via getId()
-method).
* @return Resource of the root HTML frame
*/
private Resource retrieveCurrentRootFrameResource() throws ServletException {
log.debug("delivering default frame");
if (session.getFrames().size() == 0)
throw new ServletException("no frame visible");
// get the first frame from the set and walk up the hierarchy.
// this should work in most cases. if there are more than one
// toplevel frames, the developer has to care about the resource
// ids anyway ..
SFrame defaultFrame = (SFrame) session.getFrames().iterator().next();
while (defaultFrame.getParent() != null)
defaultFrame = (SFrame) defaultFrame.getParent();
return defaultFrame.getDynamicResource(ReloadResource.class);
}
private void handleRedirect(HttpServletResponse response) {
try {
ReloadManager reloadManager = session.getReloadManager();
if (reloadManager.isUpdateMode()) {
String script = "wingS.request.sendRedirect(\"" + session.getRedirectAddress() + "\");";
session.getScriptManager().addScriptListener(new JavaScriptListener(null, null, script));
/*
Resource root = retrieveCurrentRootFrameResource();
ExternalizedResource externalizedResource = session.getExternalizeManager().getExternalizedResource(root.getId());
session.fireRequestEvent(SRequestEvent.DELIVER_START, externalizedResource);
String encoding = session.getCharacterEncoding();
response.setContentType("text/xml; charset=" + encoding);
ServletOutputStream out = response.getOutputStream();
Device outputDevice = new ServletDevice(out);
UpdateResource.writeHeader(outputDevice);
UpdateResource.writeUpdate(outputDevice, "wingS.request.sendRedirect(\"" + session.getRedirectAddress() + "\");");
UpdateResource.writeFooter(outputDevice);
outputDevice.flush();
session.fireRequestEvent(SRequestEvent.DELIVER_DONE, externalizedResource);
session.fireRequestEvent(SRequestEvent.REQUEST_END);
reloadManager.clear();
session.setServletRequest(null);
session.setServletResponse(null);
SessionManager.removeSession();
SForm.clearArmedComponents();
*/
}
else {
response.sendRedirect(session.getRedirectAddress());
}
}
catch (Exception e) {
log.warn(e.getMessage(), e);
}
finally {
session.setRedirectAddress(null);
}
}
/**
* In case of an error, display an error page to the user. This is only
* done if there is a properties wings.error.handler
defined
* in the web.xml file. If the property is present, the following steps
* are performed:
* Load the class named by the value of that property, using the
* current thread's context class loader,
* Instantiate that class using its zero-argument constructor,
* Cast the instance to ExceptionHandler,
* Invoke the handler's handle method, passing it the
* thrown argument that was passed to this method.
*
*
* @see DefaultExceptionHandler
* @param response the HTTP Response to use
* @param thrown the Exception to report
*/
protected void handleException(HttpServletResponse response, Throwable thrown) {
try {
String className = (String)session.getProperty("wings.error.handler");
if (className == null)
className = DefaultExceptionHandler.class.getName();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class clazz = classLoader.loadClass(className);
ExceptionHandler exceptionHandler = (ExceptionHandler)clazz.newInstance();
StringBuilderDevice device = new StringBuilderDevice(4096);
exceptionHandler.handle(device, thrown);
Resource resource = new StringResource(device.toString(), "html", "text/html");
String url = session.getExternalizeManager().externalize(resource);
ReloadManager reloadManager = session.getReloadManager();
session.fireRequestEvent(SRequestEvent.DISPATCH_DONE);
session.fireRequestEvent(SRequestEvent.PROCESS_REQUEST);
if (reloadManager.isUpdateMode()) {
Resource root = retrieveCurrentRootFrameResource();
ExternalizedResource externalizedResource = session.getExternalizeManager().getExternalizedResource(root.getId());
session.fireRequestEvent(SRequestEvent.DELIVER_START, externalizedResource);
String encoding = session.getCharacterEncoding();
response.setContentType("text/xml; charset=" + encoding);
ServletOutputStream out = response.getOutputStream();
Device outputDevice = new ServletDevice(out, encoding);
UpdateResource.writeHeader(outputDevice);
UpdateResource.writeUpdate(outputDevice, "wingS.request.sendRedirect(\"" + url + "\");");
UpdateResource.writeFooter(outputDevice);
outputDevice.flush();
session.fireRequestEvent(SRequestEvent.DELIVER_DONE, externalizedResource);
session.fireRequestEvent(SRequestEvent.REQUEST_END);
reloadManager.clear();
session.setServletRequest(null);
session.setServletResponse(null);
SessionManager.removeSession();
SForm.clearArmedComponents();
}
else {
// TODO FIXME: This redirect is in most times too late. Redirect works only if no byte
// has yet been sent to the client (dispatch phase)
// Won't work if exception occurred during rendering phase
// Solution: Provide / send javascript code to redirect.
response.sendRedirect(url);
}
}
catch (Exception e) {
log.warn(e.getMessage(), thrown);
}
}
/**
* This method is called, whenever a Resource is requested whose
* name is not known within this session.
*
* @param req the causing HttpServletRequest
* @param res the HttpServletResponse of this request
*/
protected static void handleUnknownResourceRequested(HttpServletRequest req,
HttpServletResponse res)
throws IOException {
// res.setStatus(HttpServletResponse.SC_NOT_FOUND);
// res.setContentType("text/html");
// res.getOutputStream().println("404 Not Found
Unknown Resource Requested: " + req.getPathInfo());
res.reset();
res.sendError(HttpServletResponse.SC_NOT_FOUND, req.getPathInfo());
}
/* HttpSessionBindingListener */
@Override
public void valueBound(HttpSessionBindingEvent event) {
}
/* HttpSessionBindingListener */
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
destroy();
}
/**
* get the Session Encoding, that is appended to each URL.
* Basically, this is response.encodeURL(""), but unfortuntatly, this
* empty encoding isn't supported by Tomcat 4.x anymore.
*/
public static String getSessionEncoding(HttpServletResponse response) {
if (response == null) return "";
// encode dummy non-empty URL.
return response.encodeURL("foo").substring(3);
}
/**
* Destroy and cleanup the session servlet.
*/
@Override
public void destroy() {
log.info("destroy called");
if (session != null) {
// Session is needed on destroying the session
SessionManager.setSession(session);
session.destroy();
}
}
/**
* A check if this session servlet seems to be alive or is i.e. invalid because it
* was deserialized.
*
* @return true
, if this session servlet seems to be valid and alive.
*/
public boolean isValid() {
return session != null && parent != null;
}
}