/* *************************************************************************
IT Mill Toolkit
Development of Browser User Interfaces Made Easy
Copyright (C) 2000-2006 IT Mill Ltd
*************************************************************************
This product is distributed under commercial license that can be found
from the product package on license/license.txt. Use of this product might
require purchasing a commercial license from IT Mill Ltd. For guidelines
on usage, see license/licensing-guidelines.html
*************************************************************************
For more information, contact:
IT Mill Ltd phone: +358 2 4802 7180
Ruukinkatu 2-4 fax: +358 2 4802 7181
20540, Turku email: info@itmill.com
Finland company www: www.itmill.com
Primary source for information and releases: www.itmill.com
********************************************************************** */
package com.itmill.toolkit.terminal.web;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.WeakHashMap;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.xml.sax.SAXException;
import com.itmill.toolkit.Application;
import com.itmill.toolkit.Application.WindowAttachEvent;
import com.itmill.toolkit.Application.WindowDetachEvent;
import com.itmill.toolkit.service.FileTypeResolver;
import com.itmill.toolkit.service.License;
import com.itmill.toolkit.service.License.InvalidLicenseFile;
import com.itmill.toolkit.service.License.LicenseFileHasAlreadyBeenRead;
import com.itmill.toolkit.service.License.LicenseFileHasNotBeenRead;
import com.itmill.toolkit.service.License.LicenseSignatureIsInvalid;
import com.itmill.toolkit.service.License.LicenseViolation;
import com.itmill.toolkit.terminal.DownloadStream;
import com.itmill.toolkit.terminal.Paintable;
import com.itmill.toolkit.terminal.ParameterHandler;
import com.itmill.toolkit.terminal.ThemeResource;
import com.itmill.toolkit.terminal.URIHandler;
import com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent;
import com.itmill.toolkit.terminal.web.ThemeSource.ThemeException;
import com.itmill.toolkit.ui.Window;
/**
* This servlet connects IT Mill Toolkit Application to Web. This servlet
* replaces both WebAdapterServlet and AjaxAdapterServlet.
*
* @author IT Mill Ltd.
* @version
* @VERSION@
* @since 4.0
*/
public class ApplicationServlet extends HttpServlet implements
Application.WindowAttachListener, Application.WindowDetachListener,
Paintable.RepaintRequestListener {
private static final long serialVersionUID = -4937882979845826574L;
/** Version number of this release. For example "4.0.0" */
public static final String VERSION;
/** Major version number. For example 4 in 4.1.0. */
public static final int VERSION_MAJOR;
/** Minor version number. For example 1 in 4.1.0. */
public static final int VERSION_MINOR;
/** Build number. For example 0-beta1 in 4.0.0-beta1. */
public static final String VERSION_BUILD;
/* Initialize version numbers from string replaced by build-script. */
static {
if ("@VERSION@".equals("@" + "VERSION" + "@"))
VERSION = "4.0.0-INTERNAL-NONVERSIONED-DEBUG-BUILD";
else
VERSION = "@VERSION@";
String[] digits = VERSION.split("\\.");
VERSION_MAJOR = Integer.parseInt(digits[0]);
VERSION_MINOR = Integer.parseInt(digits[1]);
VERSION_BUILD = digits[2];
}
// Configurable parameter names
private static final String PARAMETER_DEBUG = "Debug";
private static final String PARAMETER_DEFAULT_THEME_JAR = "DefaultThemeJar";
private static final String PARAMETER_THEMESOURCE = "ThemeSource";
private static final String PARAMETER_THEME_CACHETIME = "ThemeCacheTime";
private static final String PARAMETER_MAX_TRANSFORMERS = "MaxTransformers";
private static final String PARAMETER_TRANSFORMER_CACHETIME = "TransformerCacheTime";
private static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24;
private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
private static final int DEFAULT_MAX_TRANSFORMERS = 1;
private static final int MAX_BUFFER_SIZE = 64 * 1024;
private static final String SESSION_ATTR_VARMAP = "itmill-toolkit-varmap";
private static final String SESSION_ATTR_CONTEXT = "itmill-toolkit-context";
protected static final String SESSION_ATTR_APPS = "itmill-toolkit-apps";
private static final String SESSION_BINDING_LISTENER = "itmill-toolkit-bindinglistener";
// TODO Should default or base theme be the default?
protected static final String DEFAULT_THEME = "base";
private static final String RESOURCE_URI = "/RES/";
private static final String AJAX_UIDL_URI = "/UIDL/";
private static final String THEME_DIRECTORY_PATH = "WEB-INF/lib/themes/";
private static final String THEME_LISTING_FILE = THEME_DIRECTORY_PATH
+ "themes.txt";
private static final String DEFAULT_THEME_JAR_PREFIX = "itmill-toolkit-themes";
private static final String DEFAULT_THEME_JAR = "WEB-INF/lib/"
+ DEFAULT_THEME_JAR_PREFIX + "-" + VERSION + ".jar";
private static final String DEFAULT_THEME_TEMP_FILE_PREFIX = "ITMILL_TMP_";
private static final String SERVER_COMMAND_PARAM = "SERVER_COMMANDS";
private static final int SERVER_COMMAND_STREAM_MAINTAIN_PERIOD = 15000;
private static final int SERVER_COMMAND_HEADER_PADDING = 2000;
// Maximum delay between request for an user to be considered active (in ms)
private static final long ACTIVE_USER_REQUEST_INTERVAL = 1000 * 45;
// Private fields
private Class applicationClass;
private Properties applicationProperties;
private UIDLTransformerFactory transformerFactory;
private CollectionThemeSource themeSource;
private String resourcePath = null;
private String debugMode = "";
private int maxConcurrentTransformers;
private long transformerCacheTime;
private long themeCacheTime;
private WeakHashMap applicationToDirtyWindowSetMap = new WeakHashMap();
private WeakHashMap applicationToServerCommandStreamLock = new WeakHashMap();
private static WeakHashMap applicationToLastRequestDate = new WeakHashMap();
private List allWindows = new LinkedList();
private WeakHashMap applicationToAjaxAppMgrMap = new WeakHashMap();
private HashMap licenseForApplicationClass = new HashMap();
private static HashSet licensePrintedForApplicationClass = new HashSet();
/**
* Called by the servlet container to indicate to a servlet that the servlet
* is being placed into service.
*
* @param servletConfig
* object containing the servlet's configuration and
* initialization parameters
* @throws ServletException
* if an exception has occurred that interferes with the
* servlet's normal operation.
*/
public void init(javax.servlet.ServletConfig servletConfig)
throws javax.servlet.ServletException {
super.init(servletConfig);
// Get the application class name
String applicationClassName = servletConfig
.getInitParameter("application");
if (applicationClassName == null) {
Log.error("Application not specified in servlet parameters");
}
// Store the application parameters into Properties object
this.applicationProperties = new Properties();
for (Enumeration e = servletConfig.getInitParameterNames(); e
.hasMoreElements();) {
String name = (String) e.nextElement();
this.applicationProperties.setProperty(name, servletConfig
.getInitParameter(name));
}
// Override with server.xml parameters
ServletContext context = servletConfig.getServletContext();
for (Enumeration e = context.getInitParameterNames(); e
.hasMoreElements();) {
String name = (String) e.nextElement();
this.applicationProperties.setProperty(name, context
.getInitParameter(name));
}
// Get the debug window parameter
String debug = getApplicationOrSystemProperty(PARAMETER_DEBUG, "")
.toLowerCase();
// Enable application specific debug
if (!"".equals(debug) && !"true".equals(debug)
&& !"false".equals(debug))
throw new ServletException(
"If debug parameter is given for an application, it must be 'true' or 'false'");
this.debugMode = debug;
// Get the maximum number of simultaneous transformers
this.maxConcurrentTransformers = Integer
.parseInt(getApplicationOrSystemProperty(
PARAMETER_MAX_TRANSFORMERS, "-1"));
if (this.maxConcurrentTransformers < 1)
this.maxConcurrentTransformers = DEFAULT_MAX_TRANSFORMERS;
// Get cache time for transformers
this.transformerCacheTime = Integer
.parseInt(getApplicationOrSystemProperty(
PARAMETER_TRANSFORMER_CACHETIME, "-1")) * 1000;
// Get cache time for theme resources
this.themeCacheTime = Integer.parseInt(getApplicationOrSystemProperty(
PARAMETER_THEME_CACHETIME, "-1")) * 1000;
if (this.themeCacheTime < 0) {
this.themeCacheTime = DEFAULT_THEME_CACHETIME;
}
// Add all specified theme sources
this.themeSource = new CollectionThemeSource();
List directorySources = getThemeSources();
for (Iterator i = directorySources.iterator(); i.hasNext();) {
this.themeSource.add((ThemeSource) i.next());
}
// Add the default theme source
String[] defaultThemeFiles = new String[] { getApplicationOrSystemProperty(
PARAMETER_DEFAULT_THEME_JAR, DEFAULT_THEME_JAR) };
File f = findDefaultThemeJar(defaultThemeFiles);
try {
// Add themes.jar if exists
if (f != null && f.exists())
this.themeSource.add(new JarThemeSource(f, this, ""));
else {
Log.warn("Default theme JAR not found in: "
+ Arrays.asList(defaultThemeFiles));
}
} catch (Exception e) {
throw new ServletException("Failed to load default theme from "
+ Arrays.asList(defaultThemeFiles), e);
}
// Check that at least one themesource was loaded
if (this.themeSource.getThemes().size() <= 0) {
throw new ServletException(
"No themes found in specified themesources.");
}
// Initialize the transformer factory, if not initialized
if (this.transformerFactory == null) {
this.transformerFactory = new UIDLTransformerFactory(
this.themeSource, this, this.maxConcurrentTransformers,
this.transformerCacheTime);
}
// Load the application class using the same class loader
// as the servlet itself
ClassLoader loader = this.getClass().getClassLoader();
try {
this.applicationClass = loader.loadClass(applicationClassName);
} catch (ClassNotFoundException e) {
throw new ServletException("Failed to load application class: "
+ applicationClassName);
}
}
/**
* Get an application or system property value.
*
* @param parameterName
* Name or the parameter
* @param defaultValue
* Default to be used
* @return String value or default if not found
*/
private String getApplicationOrSystemProperty(String parameterName,
String defaultValue) {
// Try application properties
String val = this.applicationProperties.getProperty(parameterName);
if (val != null) {
return val;
}
// Try lowercased application properties for backward compability with
// 3.0.2 and earlier
val = this.applicationProperties.getProperty(parameterName
.toLowerCase());
if (val != null) {
return val;
}
// Try system properties
String pkgName;
Package pkg = this.getClass().getPackage();
if (pkg != null) {
pkgName = pkg.getName();
} else {
String clazzName = this.getClass().getName();
pkgName = new String(clazzName.toCharArray(), 0, clazzName
.lastIndexOf('.'));
}
val = System.getProperty(pkgName + "." + parameterName);
if (val != null) {
return val;
}
// Try lowercased system properties
val = System.getProperty(pkgName + "." + parameterName.toLowerCase());
if (val != null) {
return val;
}
return defaultValue;
}
/**
* Get ThemeSources from given path. Construct the list of avalable themes
* in path using the following sources: 1. content of THEME_PATH directory
* (if available) 2. The themes listed in THEME_LIST_FILE 3. "themesource"
* application parameter - "ThemeSource" system
* property
*
* @param THEME_DIRECTORY_PATH
* @return List
*/
private List getThemeSources() throws ServletException {
List returnValue = new LinkedList();
// Check the list file in theme directory
List sourcePaths = new LinkedList();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(
this.getServletContext().getResourceAsStream(
THEME_LISTING_FILE)));
String line = null;
while ((line = reader.readLine()) != null) {
sourcePaths.add(THEME_DIRECTORY_PATH + line.trim());
}
if (this.isDebugMode(null)) {
Log.debug("Listed " + sourcePaths.size() + " themes in "
+ THEME_LISTING_FILE + ". Loading " + sourcePaths);
}
} catch (Exception ignored) {
// If the file reading fails, just skip to next method
}
// If no file was found or it was empty,
// try to add themes filesystem directory if it is accessible
if (sourcePaths.size() <= 0) {
if (this.isDebugMode(null)) {
Log.debug("No themes listed in " + THEME_LISTING_FILE
+ ". Trying to read the content of directory "
+ THEME_DIRECTORY_PATH);
}
try {
String path = this.getServletContext().getRealPath(
THEME_DIRECTORY_PATH);
if (path != null) {
File f = new File(path);
if (f != null && f.exists())
returnValue.add(new DirectoryThemeSource(f, this));
}
} catch (java.io.IOException je) {
Log.info("Theme directory " + THEME_DIRECTORY_PATH
+ " not available. Skipped.");
} catch (ThemeException e) {
throw new ServletException("Failed to load themes from "
+ THEME_DIRECTORY_PATH, e);
}
}
// Add the theme sources from application properties
String paramValue = getApplicationOrSystemProperty(
PARAMETER_THEMESOURCE, null);
if (paramValue != null) {
StringTokenizer st = new StringTokenizer(paramValue, ";");
while (st.hasMoreTokens()) {
sourcePaths.add(st.nextToken());
}
}
// Construct appropriate theme source instances for each path
for (Iterator i = sourcePaths.iterator(); i.hasNext();) {
String source = (String) i.next();
File sourceFile = new File(source);
try {
// Relative files are treated as streams (to support
// resource inside WAR files)
if (!sourceFile.isAbsolute()) {
returnValue.add(new ServletThemeSource(this
.getServletContext(), this, source));
} else if (sourceFile.isDirectory()) {
// Absolute directories are read from filesystem
returnValue.add(new DirectoryThemeSource(sourceFile, this));
} else {
// Absolute JAR-files are read from filesystem
returnValue.add(new JarThemeSource(sourceFile, this, ""));
}
} catch (Exception e) {
// Any exception breaks the the init
throw new ServletException("Invalid theme source: " + source, e);
}
}
// Return the constructed list of theme sources
return returnValue;
}
/**
* Receives standard HTTP requests from the public service method and
* dispatches them.
*
* @param request
* object that contains the request the client made of the
* servlet
* @param response
* object that contains the response the servlet returns to the
* client
* @throws ServletException
* if an input or output error occurs while the servlet is
* handling the TRACE request
* @throws IOException
* if the request for the TRACE cannot be handled
*/
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
// Transformer and output stream for the result
UIDLTransformer transformer = null;
HttpVariableMap variableMap = null;
OutputStream out = response.getOutputStream();
HashSet currentlyDirtyWindowsForThisApplication = new HashSet();
Application application = null;
try {
// Handle resource requests
if (handleResourceRequest(request, response))
return;
// Handle server commands
if (handleServerCommands(request, response))
return;
// Get the application
application = getApplication(request);
// Create application if it doesn't exist
if (application == null)
application = createApplication(request);
// Set the last application request date
applicationToLastRequestDate.put(application, new Date());
// Invoke context transaction listeners
((WebApplicationContext) application.getContext())
.startTransaction(application, request);
// Is this a download request from application
DownloadStream download = null;
// The rest of the process is synchronized with the application
// in order to guarantee that no parallel variable handling is
// made
synchronized (application) {
// Handle UIDL requests?
String resourceId = request.getPathInfo();
if (resourceId != null && resourceId.startsWith(AJAX_UIDL_URI)) {
getApplicationManager(application).handleUidlRequest(
request, response);
return;
}
// Get the variable map
variableMap = getVariableMap(application, request);
if (variableMap == null)
return;
// Change all variables based on request parameters
Map unhandledParameters = variableMap.handleVariables(request,
application);
// Check/handle client side feature checks
WebBrowserProbe
.handleProbeRequest(request, unhandledParameters);
// Handle the URI if the application is still running
if (application.isRunning())
download = handleURI(application, request, response);
// If this is not a download request
if (download == null) {
// Window renders are not cacheable
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
// Find the window within the application
Window window = null;
if (application.isRunning())
window = getApplicationWindow(request, application,
unhandledParameters);
// Handle the unhandled parameters if the application is
// still running
if (window != null && unhandledParameters != null
&& !unhandledParameters.isEmpty()) {
try {
window.handleParameters(unhandledParameters);
} catch (Throwable t) {
application
.terminalError(new ParameterHandlerErrorImpl(
window, t));
}
}
// Remove application if it has stopped
if (!application.isRunning()) {
endApplication(request, response, application);
return;
}
// Return blank page, if no window found
if (window == null) {
response.setContentType("text/html");
BufferedWriter page = new BufferedWriter(
new OutputStreamWriter(out));
page.write("
");
page
.write("The requested window has been removed from application.");
page.write("");
page.close();
return;
}
// Get the terminal type for the window
WebBrowser terminalType = (WebBrowser) window.getTerminal();
// Set terminal type for the window, if not already set
if (terminalType == null) {
terminalType = WebBrowserProbe.getTerminalType(request
.getSession());
window.setTerminal(terminalType);
}
// Find theme
String themeName = window.getTheme() != null ? window
.getTheme() : DEFAULT_THEME;
if (unhandledParameters.get("theme") != null) {
themeName = (String) ((Object[]) unhandledParameters
.get("theme"))[0];
}
Theme theme = themeSource
.getThemeByName(themeName);
if (theme == null)
throw new ServletException("Theme (named '"
+ themeName + "') can not be found");
// If UIDL rendering mode is preferred, a page for it is
// rendered
String renderingMode = theme.getPreferredMode(terminalType,
themeSource);
if (unhandledParameters.get("renderingMode") != null)
renderingMode = (String) ((Object[]) unhandledParameters
.get("renderingMode"))[0];
if (Theme.MODE_UIDL.equals(renderingMode)
&& !(window instanceof DebugWindow)) {
response.setContentType("text/html");
BufferedWriter page = new BufferedWriter(
new OutputStreamWriter(out));
page
.write("\n");
page.write("\n"
+ window.getCaption() + "\n");
Theme t = theme;
Vector themes = new Vector();
themes.add(t);
while (t.getParent() != null) {
String parentName = t.getParent();
t = themeSource.getThemeByName(parentName);
themes.add(t);
}
for (int k = themes.size() - 1; k >= 0; k--) {
t = (Theme) themes.get(k);
Collection files = t.getFileNames(terminalType,
Theme.MODE_UIDL);
for (Iterator i = files.iterator(); i.hasNext();) {
String file = (String) i.next();
if (file.endsWith(".css"))
page
.write("\n");
else if (file.endsWith(".js"))
page
.write("\n");
}
}
page.write("\n");
page
.write("
Loading...
\n");
page.write("\n");
page.write("\n");
page.write("\n");
page.close();
return;
}
// If other than XSLT or UIDL mode is requested
if (!Theme.MODE_XSLT.equals(renderingMode)
&& !(window instanceof DebugWindow)) {
// TODO More informal message should be given is browser
// is not supported
response.setContentType("text/html");
BufferedWriter page = new BufferedWriter(
new OutputStreamWriter(out));
page.write("");
page.write("Unsupported browser.");
page.write("");
page.close();
return;
}
// Initialize Transformer
UIDLTransformerType transformerType = new UIDLTransformerType(
terminalType, theme);
transformer = this.transformerFactory
.getTransformer(transformerType);
// Set the response type
response.setContentType(terminalType.getContentType());
// Create UIDL writer
WebPaintTarget paintTarget = transformer
.getPaintTarget(variableMap);
// Assure that the correspoding debug window will be
// repainted property
// by clearing it before the actual paint.
DebugWindow debugWindow = (DebugWindow) application
.getWindow(DebugWindow.WINDOW_NAME);
if (debugWindow != null && debugWindow != window) {
debugWindow.setWindowUIDL(window, "Painting...");
}
// Paint window
window.paint(paintTarget);
paintTarget.close();
// For exception handling, memorize the current dirty status
Collection dirtyWindows = (Collection) applicationToDirtyWindowSetMap
.get(application);
if (dirtyWindows == null) {
dirtyWindows = new HashSet();
applicationToDirtyWindowSetMap.put(application,
dirtyWindows);
}
currentlyDirtyWindowsForThisApplication
.addAll(dirtyWindows);
// Window is now painted
windowPainted(application, window);
// Debug
if (debugWindow != null && debugWindow != window) {
debugWindow
.setWindowUIDL(window, paintTarget.getUIDL());
}
// Set the function library state for this thread
ThemeFunctionLibrary.setState(application, window,
transformerType.getWebBrowser(), request
.getSession(), this, transformerType
.getTheme().getName());
}
}
// For normal requests, transform the window
if (download == null) {
// Transform and output the result to browser
// Note that the transform and transfer of the result is
// not synchronized with the variable map. This allows
// parallel transfers and transforms for better performance,
// but requires that all calls from the XSL to java are
// thread-safe
transformer.transform(out);
}
// For download request, transfer the downloaded data
else {
handleDownload(download, request, response);
}
} catch (UIDLTransformerException te) {
try {
// Write the error report to client
response.setContentType("text/html");
BufferedWriter err = new BufferedWriter(new OutputStreamWriter(
out));
err
.write("Application Internal Error");
err.write("
" + te.getMessage() + "
");
err.write(te.getHTMLDescription());
err.write("");
err.close();
} catch (Throwable t) {
Log.except("Failed to write error page: " + t
+ ". Original exception was: ", te);
}
// Add previously dirty windows to dirtyWindowList in order
// to make sure that eventually they are repainted
Application currentApplication = getApplication(request);
for (Iterator iter = currentlyDirtyWindowsForThisApplication
.iterator(); iter.hasNext();) {
Window dirtyWindow = (Window) iter.next();
addDirtyWindow(currentApplication, dirtyWindow);
}
} catch (Throwable e) {
// Re-throw other exceptions
throw new ServletException(e);
} finally {
// Release transformer
if (transformer != null)
transformerFactory.releaseTransformer(transformer);
// Notify transaction end
if (application != null)
((WebApplicationContext) application.getContext())
.endTransaction(application, request);
// Clean the function library state for this thread
// for security reasons
ThemeFunctionLibrary.cleanState();
}
}
/**
* Handle the requested URI. An application can add handlers to do special
* processing, when a certain URI is requested. The handlers are invoked
* before any windows URIs are processed and if a DownloadStream is returned
* it is sent to the client.
*
* @see com.itmill.toolkit.terminal.URIHandler
*
* @param application
* Application owning the URI
* @param request
* HTTP request instance
* @param response
* HTTP response to write to.
* @return boolean True if the request was handled and further processing
* should be suppressed, false otherwise.
*/
private DownloadStream handleURI(Application application,
HttpServletRequest request, HttpServletResponse response) {
String uri = request.getPathInfo();
// If no URI is available
if (uri == null || uri.length() == 0 || uri.equals("/"))
return null;
// Remove the leading /
while (uri.startsWith("/") && uri.length() > 0)
uri = uri.substring(1);
// Handle the uri
DownloadStream stream = null;
try {
stream = application.handleURI(application.getURL(), uri);
} catch (Throwable t) {
application.terminalError(new URIHandlerErrorImpl(application, t));
}
return stream;
}
/**
* Handle the requested URI. An application can add handlers to do special
* processing, when a certain URI is requested. The handlers are invoked
* before any windows URIs are processed and if a DownloadStream is returned
* it is sent to the client.
*
* @see com.itmill.toolkit.terminal.URIHandler
*
* @param application
* Application owning the URI
* @param request
* HTTP request instance
* @param response
* HTTP response to write to.
* @return boolean True if the request was handled and further processing
* should be suppressed, false otherwise.
*/
private void handleDownload(DownloadStream stream,
HttpServletRequest request, HttpServletResponse response) {
// Download from given stream
InputStream data = stream.getStream();
if (data != null) {
// Set content type
response.setContentType(stream.getContentType());
// Set cache headers
long cacheTime = stream.getCacheTime();
if (cacheTime <= 0) {
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
} else {
response.setHeader("Cache-Control", "max-age=" + cacheTime
/ 1000);
response.setDateHeader("Expires", System.currentTimeMillis()
+ cacheTime);
response.setHeader("Pragma", "cache"); // Required to apply
// caching in some
// Tomcats
}
// Copy download stream parameters directly
// to HTTP headers.
Iterator i = stream.getParameterNames();
if (i != null) {
while (i.hasNext()) {
String param = (String) i.next();
response.setHeader((String) param, stream
.getParameter(param));
}
}
int bufferSize = stream.getBufferSize();
if (bufferSize <= 0 || bufferSize > MAX_BUFFER_SIZE)
bufferSize = DEFAULT_BUFFER_SIZE;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
try {
OutputStream out = response.getOutputStream();
while ((bytesRead = data.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
out.flush();
}
out.close();
} catch (IOException ignored) {
}
}
}
/**
* Look for default theme JAR file.
*
* @return Jar file or null if not found.
*/
private File findDefaultThemeJar(String[] fileList) {
// Try to find the default theme JAR file based on the given path
for (int i = 0; i < fileList.length; i++) {
String path = this.getServletContext().getRealPath(fileList[i]);
File file = null;
if (path != null && (file = new File(path)).exists()) {
return file;
}
}
// If we do not have access to individual files, create a temporary
// file from named resource.
for (int i = 0; i < fileList.length; i++) {
InputStream defaultTheme = this.getServletContext()
.getResourceAsStream(fileList[i]);
// Read the content to temporary file and return it
if (defaultTheme != null) {
return createTemporaryFile(defaultTheme, ".jar");
}
}
// Try to find the default theme JAR file based on file naming scheme
// NOTE: This is for backward compability with 3.0.2 and earlier.
String path = this.getServletContext().getRealPath("/WEB-INF/lib");
if (path != null) {
File lib = new File(path);
String[] files = lib.list();
if (files != null) {
for (int i = 0; i < files.length; i++) {
if (files[i].toLowerCase().endsWith(".jar")
&& files[i].startsWith(DEFAULT_THEME_JAR_PREFIX)) {
return new File(lib, files[i]);
}
}
}
}
// If no file was found return null
return null;
}
/**
* Create a temporary file for given stream.
*
* @param stream
* Stream to be stored into temporary file.
* @param extension
* File type extension
* @return File
*/
private File createTemporaryFile(InputStream stream, String extension) {
File tmpFile;
try {
tmpFile = File.createTempFile(DEFAULT_THEME_TEMP_FILE_PREFIX,
extension);
FileOutputStream out = new FileOutputStream(tmpFile);
byte[] buf = new byte[1024];
int bytes = 0;
while ((bytes = stream.read(buf)) > 0) {
out.write(buf, 0, bytes);
}
out.close();
} catch (IOException e) {
System.err
.println("Failed to create temporary file for default theme: "
+ e);
tmpFile = null;
}
return tmpFile;
}
/**
* Handle theme resource file requests. Resources supplied with the themes
* are provided by the WebAdapterServlet.
*
* @param request
* HTTP request
* @param response
* HTTP response
* @return boolean True if the request was handled and further processing
* should be suppressed, false otherwise.
*/
private boolean handleResourceRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException {
// If the resource path is unassigned, initialize it
if (resourcePath == null)
resourcePath = request.getContextPath() + request.getServletPath()
+ RESOURCE_URI;
String resourceId = request.getPathInfo();
// Check if this really is a resource request
if (resourceId == null || !resourceId.startsWith(RESOURCE_URI))
return false;
// Check the resource type
resourceId = resourceId.substring(RESOURCE_URI.length());
InputStream data = null;
// Get theme resources
try {
data = themeSource.getResource(resourceId);
} catch (ThemeSource.ThemeException e) {
Log.info(e.getMessage());
data = null;
}
// Write the response
try {
if (data != null) {
response.setContentType(FileTypeResolver
.getMIMEType(resourceId));
// Use default cache time for theme resources
if (this.themeCacheTime > 0) {
response.setHeader("Cache-Control", "max-age="
+ this.themeCacheTime / 1000);
response.setDateHeader("Expires", System
.currentTimeMillis()
+ this.themeCacheTime);
response.setHeader("Pragma", "cache"); // Required to apply
// caching in some
// Tomcats
}
// Write the data to client
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int bytesRead = 0;
OutputStream out = response.getOutputStream();
while ((bytesRead = data.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
out.close();
data.close();
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} catch (java.io.IOException e) {
Log.info("Resource transfer failed: " + request.getRequestURI()
+ ". (" + e.getMessage() + ")");
}
return true;
}
/** Get the variable map for the session */
private static synchronized HttpVariableMap getVariableMap(
Application application, HttpServletRequest request) {
HttpSession session = request.getSession();
// Get the application to variablemap map
Map varMapMap = (Map) session.getAttribute(SESSION_ATTR_VARMAP);
if (varMapMap == null) {
varMapMap = new WeakHashMap();
session.setAttribute(SESSION_ATTR_VARMAP, varMapMap);
}
// Create a variable map, if it does not exists.
HttpVariableMap variableMap = (HttpVariableMap) varMapMap
.get(application);
if (variableMap == null) {
variableMap = new HttpVariableMap();
varMapMap.put(application, variableMap);
}
return variableMap;
}
/** Get the current application URL from request */
private URL getApplicationUrl(HttpServletRequest request)
throws MalformedURLException {
URL applicationUrl;
try {
URL reqURL = new URL((request.isSecure() ? "https://" : "http://")
+ request.getServerName() + ":" + request.getServerPort()
+ request.getRequestURI());
String servletPath = request.getContextPath()
+ request.getServletPath();
if (servletPath.length() == 0
|| servletPath.charAt(servletPath.length() - 1) != '/')
servletPath = servletPath + "/";
applicationUrl = new URL(reqURL, servletPath);
} catch (MalformedURLException e) {
Log.error("Error constructing application url "
+ request.getRequestURI() + " (" + e + ")");
throw e;
}
return applicationUrl;
}
/**
* Get the existing application for given request. Looks for application
* instance for given request based on the requested URL.
*
* @param request
* HTTP request
* @return Application instance, or null if the URL does not map to valid
* application.
*/
private Application getApplication(HttpServletRequest request)
throws MalformedURLException {
// Ensure that the session is still valid
HttpSession session = request.getSession(false);
if (session == null)
return null;
// Get application list for the session.
LinkedList applications = (LinkedList) session
.getAttribute(SESSION_ATTR_APPS);
if (applications == null)
return null;
// Search for the application (using the application URI) from the list
Application application = null;
for (Iterator i = applications.iterator(); i.hasNext()
&& application == null;) {
Application a = (Application) i.next();
String aPath = a.getURL().getPath();
String servletPath = request.getContextPath()
+ request.getServletPath();
if (servletPath.length() < aPath.length())
servletPath += "/";
if (servletPath.equals(aPath))
application = a;
}
// Remove stopped applications from the list
if (application != null && !application.isRunning()) {
applications.remove(application);
application = null;
}
return application;
}
/**
* Create a new application.
*
* @return New application instance
* @throws SAXException
* @throws LicenseViolation
* @throws InvalidLicenseFile
* @throws LicenseSignatureIsInvalid
* @throws LicenseFileHasNotBeenRead
*/
private Application createApplication(HttpServletRequest request)
throws MalformedURLException, InstantiationException,
IllegalAccessException, LicenseFileHasNotBeenRead,
LicenseSignatureIsInvalid, InvalidLicenseFile, LicenseViolation,
SAXException {
Application application = null;
// Get the application url
URL applicationUrl = getApplicationUrl(request);
// Get application list.
HttpSession session = request.getSession();
if (session == null)
return null;
LinkedList applications = (LinkedList) session
.getAttribute(SESSION_ATTR_APPS);
if (applications == null) {
applications = new LinkedList();
session.setAttribute(SESSION_ATTR_APPS, applications);
HttpSessionBindingListener sessionBindingListener = new SessionBindingListener(
applications);
session.setAttribute(SESSION_BINDING_LISTENER,
sessionBindingListener);
}
// Create new application and start it
try {
application = (Application) this.applicationClass.newInstance();
applications.add(application);
// Listen to window add/removes (for web mode)
application.addListener((Application.WindowAttachListener) this);
application.addListener((Application.WindowDetachListener) this);
// Set localte
application.setLocale(request.getLocale());
// Get application context for this session
WebApplicationContext context = (WebApplicationContext) session
.getAttribute(SESSION_ATTR_CONTEXT);
if (context == null) {
context = new WebApplicationContext(session);
session.setAttribute(SESSION_ATTR_CONTEXT, context);
}
// Start application and check license
initializeLicense(application);
application.start(applicationUrl, this.applicationProperties,
context);
checkLicense(application);
} catch (IllegalAccessException e) {
Log.error("Illegal access to application class "
+ this.applicationClass.getName());
throw e;
} catch (InstantiationException e) {
Log.error("Failed to instantiate application class: "
+ this.applicationClass.getName());
throw e;
}
return application;
}
private void initializeLicense(Application application) {
License license = (License) licenseForApplicationClass.get(application
.getClass());
if (license == null) {
license = new License();
licenseForApplicationClass.put(application.getClass(), license);
}
application.setToolkitLicense(license);
}
private void checkLicense(Application application)
throws LicenseFileHasNotBeenRead, LicenseSignatureIsInvalid,
InvalidLicenseFile, LicenseViolation, SAXException {
License license = application.getToolkitLicense();
if (!license.hasBeenRead()) {
InputStream lis;
try {
lis = getServletContext().getResource(
"/WEB-INF/itmill-toolkit-license.xml").openStream();
license.readLicenseFile(lis);
} catch (MalformedURLException e) {
// This should not happen
throw new RuntimeException(e);
} catch (IOException e) {
// This should not happen
throw new RuntimeException(e);
} catch (LicenseFileHasAlreadyBeenRead e) {
// This should not happen
throw new RuntimeException(e);
}
}
// For each application class, print license description - once
if (!licensePrintedForApplicationClass.contains(applicationClass)) {
licensePrintedForApplicationClass.add(applicationClass);
if (license.shouldLimitsBePrintedOnInit())
System.out.print(license.getDescription());
}
// Check license validity
try {
license.check(applicationClass, getNumberOfActiveUsers() + 1,
VERSION_MAJOR, VERSION_MINOR, "IT Mill Toolkit", null);
} catch (LicenseFileHasNotBeenRead e) {
application.close();
throw e;
} catch (LicenseSignatureIsInvalid e) {
application.close();
throw e;
} catch (InvalidLicenseFile e) {
application.close();
throw e;
} catch (LicenseViolation e) {
application.close();
throw e;
}
}
/**
* Get the number of active application-user pairs.
*
* This returns total number of all applications in the server that are
* considered to be active. For an application to be active, it must have
* been accessed less than ACTIVE_USER_REQUEST_INTERVAL ms.
*
* @return Number of active application instances in the server.
*/
private int getNumberOfActiveUsers() {
Set apps = applicationToLastRequestDate.keySet();
int active = 0;
long now = System.currentTimeMillis();
for (Iterator i = apps.iterator(); i.hasNext();) {
Date lastReq = (Date) applicationToLastRequestDate.get(i.next());
if (now - lastReq.getTime() < ACTIVE_USER_REQUEST_INTERVAL)
active++;
}
return active;
}
/** End application */
private void endApplication(HttpServletRequest request,
HttpServletResponse response, Application application)
throws IOException {
String logoutUrl = application.getLogoutURL();
if (logoutUrl == null)
logoutUrl = application.getURL().toString();
HttpSession session = request.getSession();
if (session != null) {
LinkedList applications = (LinkedList) session
.getAttribute(SESSION_ATTR_APPS);
if (applications != null)
applications.remove(application);
}
response.sendRedirect(response.encodeRedirectURL(logoutUrl));
}
/**
* Get the existing application or create a new one. Get a window within an
* application based on the requested URI.
*
* @param request
* HTTP Request.
* @param application
* Application to query for window.
* @return Window mathing the given URI or null if not found.
*/
private Window getApplicationWindow(HttpServletRequest request,
Application application, Map params) throws ServletException {
Window window = null;
// Find the window where the request is handled
String path = request.getPathInfo();
// Main window as the URI is empty
if (path == null || path.length() == 0 || path.equals("/"))
window = application.getMainWindow();
// Try to search by window name
else {
String windowName = null;
if (path.charAt(0) == '/')
path = path.substring(1);
int index = path.indexOf('/');
if (index < 0) {
windowName = path;
path = "";
} else {
windowName = path.substring(0, index);
path = path.substring(index + 1);
}
window = application.getWindow(windowName);
if (window == null) {
// If the window has existed, and is now removed
// send a blank page
if (allWindows.contains(windowName))
return null;
// By default, we use main window
window = application.getMainWindow();
} else if (!window.isVisible()) {
// Implicitly painting without actually invoking paint()
window.requestRepaintRequests();
// If the window is invisible send a blank page
return null;
}
}
// Create and open new debug window for application if requested
Window debugWindow = application.getWindow(DebugWindow.WINDOW_NAME);
if (debugWindow == null) {
if (isDebugMode(params))
try {
debugWindow = new DebugWindow(application, request
.getSession(false), this);
debugWindow.setWidth(370);
debugWindow.setHeight(480);
application.addWindow(debugWindow);
} catch (Exception e) {
throw new ServletException(
"Failed to create debug window for application", e);
}
} else if (window != debugWindow) {
if (isDebugMode(params))
debugWindow.requestRepaint();
else
application.removeWindow(debugWindow);
}
return window;
}
/**
* Get relative location of a theme resource.
*
* @param theme
* Theme name
* @param resource
* Theme resource
* @return External URI specifying the resource
*/
public String getResourceLocation(String theme, ThemeResource resource) {
if (resourcePath == null)
return resource.getResourceId();
return resourcePath + theme + "/" + resource.getResourceId();
}
/**
* Check if web adapter is in debug mode. Extra output is generated to log
* when debug mode is enabled.
*
* @return Debug mode
*/
public boolean isDebugMode(Map parameters) {
if (parameters != null) {
Object[] debug = (Object[]) parameters.get("debug");
if (debug != null && !"false".equals(debug[0].toString())
&& !"false".equals(debugMode))
return true;
}
return "true".equals(debugMode);
}
/**
* Returns the theme source.
*
* @return ThemeSource
*/
public ThemeSource getThemeSource() {
return themeSource;
}
protected void addDirtyWindow(Application application, Window window) {
synchronized (applicationToDirtyWindowSetMap) {
HashSet dirtyWindows = (HashSet) applicationToDirtyWindowSetMap
.get(application);
if (dirtyWindows == null) {
dirtyWindows = new HashSet();
applicationToDirtyWindowSetMap.put(application, dirtyWindows);
}
dirtyWindows.add(window);
}
}
protected void removeDirtyWindow(Application application, Window window) {
synchronized (applicationToDirtyWindowSetMap) {
HashSet dirtyWindows = (HashSet) applicationToDirtyWindowSetMap
.get(application);
if (dirtyWindows != null)
dirtyWindows.remove(window);
}
}
/**
* @see com.itmill.toolkit.Application.WindowAttachListener#windowAttached(Application.WindowAttachEvent)
*/
public void windowAttached(WindowAttachEvent event) {
Window win = event.getWindow();
win.addListener((Paintable.RepaintRequestListener) this);
// Add to window names
allWindows.add(win.getName());
// Add window to dirty window references if it is visible
// Or request the window to pass on the repaint requests
if (win.isVisible())
addDirtyWindow(event.getApplication(), win);
else
win.requestRepaintRequests();
}
/**
* @see com.itmill.toolkit.Application.WindowDetachListener#windowDetached(Application.WindowDetachEvent)
*/
public void windowDetached(WindowDetachEvent event) {
event.getWindow().removeListener(
(Paintable.RepaintRequestListener) this);
// Add dirty window reference for closing the window
addDirtyWindow(event.getApplication(), event.getWindow());
}
/**
* @see com.itmill.toolkit.terminal.Paintable.RepaintRequestListener#repaintRequested(Paintable.RepaintRequestEvent)
*/
public void repaintRequested(RepaintRequestEvent event) {
Paintable p = event.getPaintable();
Application app = null;
if (p instanceof Window)
app = ((Window) p).getApplication();
if (app != null)
addDirtyWindow(app, ((Window) p));
Object lock = applicationToServerCommandStreamLock.get(app);
if (lock != null)
synchronized (lock) {
lock.notifyAll();
}
}
/** Get the list of dirty windows in application */
protected Set getDirtyWindows(Application app) {
HashSet dirtyWindows;
synchronized (applicationToDirtyWindowSetMap) {
dirtyWindows = (HashSet) applicationToDirtyWindowSetMap.get(app);
}
return dirtyWindows;
}
/** Remove a window from the list of dirty windows */
private void windowPainted(Application app, Window window) {
removeDirtyWindow(app, window);
}
/**
* Generate server commands stream. If the server commands are not
* requested, return false
*/
private boolean handleServerCommands(HttpServletRequest request,
HttpServletResponse response) {
// Server commands are allways requested with certain parameter
if (request.getParameter(SERVER_COMMAND_PARAM) == null)
return false;
// Get the application
Application application;
try {
application = getApplication(request);
} catch (MalformedURLException e) {
return false;
}
if (application == null)
return false;
// Create continuous server commands stream
try {
// Writer for writing the stream
PrintWriter w = new PrintWriter(response.getOutputStream());
// Print necessary http page headers and padding
w.println("");
for (int i = 0; i < SERVER_COMMAND_HEADER_PADDING; i++)
w.print(' ');
// Clock for synchronizing the stream
Object lock = new Object();
synchronized (applicationToServerCommandStreamLock) {
Object oldlock = applicationToServerCommandStreamLock
.get(application);
if (oldlock != null)
synchronized (oldlock) {
oldlock.notifyAll();
}
applicationToServerCommandStreamLock.put(application, lock);
}
while (applicationToServerCommandStreamLock.get(application) == lock
&& application.isRunning()) {
synchronized (application) {
// Session expiration
Date lastRequest = (Date) applicationToLastRequestDate
.get(application);
if (lastRequest != null
&& lastRequest.getTime()
+ request.getSession()
.getMaxInactiveInterval() * 1000 < System
.currentTimeMillis()) {
// Session expired, close application
application.close();
} else {
// Application still alive - keep updating windows
Set dws = getDirtyWindows(application);
if (dws != null && !dws.isEmpty()) {
// For one of the dirty windows (in each
// application)
// request redraw
Window win = (Window) dws.iterator().next();
w
.println("");
removeDirtyWindow(application, win);
// Windows that are closed immediately are "painted"
// now
if (win.getApplication() == null
|| !win.isVisible())
win.requestRepaintRequests();
}
}
}
// Send the generated commands and newline immediately to
// browser
w.println(" ");
w.flush();
response.flushBuffer();
synchronized (lock) {
try {
lock.wait(SERVER_COMMAND_STREAM_MAINTAIN_PERIOD);
} catch (InterruptedException ignored) {
}
}
}
} catch (IOException ignore) {
// In case of an Exceptions the server command stream is
// terminated
synchronized (applicationToServerCommandStreamLock) {
if (applicationToServerCommandStreamLock.get(application) == application)
applicationToServerCommandStreamLock.remove(application);
}
}
return true;
}
private class SessionBindingListener implements HttpSessionBindingListener {
private LinkedList applications;
protected SessionBindingListener(LinkedList applications) {
this.applications = applications;
}
/**
* @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)
*/
public void valueBound(HttpSessionBindingEvent arg0) {
// We are not interested in bindings
}
/**
* @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)
*/
public void valueUnbound(HttpSessionBindingEvent event) {
// If the binding listener is unbound from the session, the
// session must be closing
if (event.getName().equals(SESSION_BINDING_LISTENER)) {
// Close all applications
Object[] apps = applications.toArray();
for (int i = 0; i < apps.length; i++) {
if (apps[i] != null) {
// Close app
((Application) apps[i]).close();
// Stop application server commands stream
Object lock = applicationToServerCommandStreamLock
.get(apps[i]);
if (lock != null)
synchronized (lock) {
lock.notifyAll();
}
applicationToServerCommandStreamLock.remove(apps[i]);
// Remove application from applications list
applications.remove(apps[i]);
}
}
}
}
}
/** Implementation of ParameterHandler.ErrorEvent interface. */
public class ParameterHandlerErrorImpl implements
ParameterHandler.ErrorEvent {
private ParameterHandler owner;
private Throwable throwable;
private ParameterHandlerErrorImpl(ParameterHandler owner,
Throwable throwable) {
this.owner = owner;
this.throwable = throwable;
}
/**
* @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
*/
public Throwable getThrowable() {
return this.throwable;
}
/**
* @see com.itmill.toolkit.terminal.ParameterHandler.ErrorEvent#getParameterHandler()
*/
public ParameterHandler getParameterHandler() {
return this.owner;
}
}
/** Implementation of URIHandler.ErrorEvent interface. */
public class URIHandlerErrorImpl implements URIHandler.ErrorEvent {
private URIHandler owner;
private Throwable throwable;
private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) {
this.owner = owner;
this.throwable = throwable;
}
/**
* @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
*/
public Throwable getThrowable() {
return this.throwable;
}
/**
* @see com.itmill.toolkit.terminal.URIHandler.ErrorEvent#getURIHandler()
*/
public URIHandler getURIHandler() {
return this.owner;
}
}
/**
* Get AJAX application manager for an application.
*
* If this application has not been running in ajax mode before, new manager
* is created and web adapter stops listening to changes.
*
* @param application
* @return AJAX Application Manager
*/
private AjaxApplicationManager getApplicationManager(Application application) {
AjaxApplicationManager mgr = (AjaxApplicationManager) applicationToAjaxAppMgrMap
.get(application);
// This application is going from Web to AJAX mode, create new manager
if (mgr == null) {
// Create new manager
mgr = new AjaxApplicationManager(application);
applicationToAjaxAppMgrMap.put(application, mgr);
// Stop sending changes to this servlet because manager will take
// control
application.removeListener((Application.WindowAttachListener) this);
application.removeListener((Application.WindowDetachListener) this);
// Manager takes control over the application
mgr.takeControl();
}
return mgr;
}
}