/*******************************************************************************
* Copyright 2015 xWic group (http://www.xwic.de)
*
* Licensed under the Apache License, Version 2.0 (the "License").
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*******************************************************************************/
package de.jwic.base;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.app.VelocityEngine;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import de.jwic.events.SessionEvent;
import de.jwic.renderer.velocity.BaseVelocityRenderer;
import de.jwic.util.Compatibility;
import de.jwic.util.DTDEntityResolver;
import de.jwic.util.XMLTool;
/**
* The JWicRuntime manages the lifecycle of jWic applications.
*
* <p>The runtime takes care of the special velocity behaivior since the default jWic
* renderer is velocity, wich makes things easier.</p>
*
* @author Florian Lippisch
* @version $Revision: 1.15 $
*/
public class JWicRuntime {
public final static String DTD_PUBLICID = "-//jWic//DTD jwic-setup 3.2//EN";
public final static String DTD_SYSTEMID = "http://jwic.sourceforge.net/jwic-setup-3.2.dtd";
public final static String DTD_RESOURCEPATH = "/de/jwic/base/jwic-setup.dtd";
private static final String ATTR_NOTIFIER = "JWicRuntime.sessionNotifier";
private static JWicRuntime singleton = null;
private static boolean initialized = false;
protected final Log log = LogFactory.getLog(getClass());
private String rootPath = "./";
private String savePath = "./";
private int sessionStoreTime = 0;
/** The map of registerd velocity engines **/
private Map<String, VelocityEngine> velocityEngines = new HashMap<String, VelocityEngine>();
private Map<String, IControlRenderer> renderers = new HashMap<String, IControlRenderer>();
private SessionManager sessionManager = null;
private String contextPath = null;
/**
* This class is set as a session attribute to get notified when a session
* is closed by the server.
* @author Florian Lippisch
*/
private class HttpSessionClosedListener implements HttpSessionBindingListener,Serializable {
private JWicRuntime runtime;
private String sessionID;
public HttpSessionClosedListener(JWicRuntime rt, String sessionID) {
this.runtime = rt;
this.sessionID = sessionID;
}
public void valueBound(HttpSessionBindingEvent arg0) {
// nothing to do
}
public void valueUnbound(HttpSessionBindingEvent arg0) {
runtime.sessionClosed(sessionID);
}
}
/**
* Default, private contructor. Use getJWicRuntime() to get an instance.
*/
private JWicRuntime() {
}
/**
* This method is invoked by the HttpSessionClosedListener when a session is
* closed.
* @param sessionID
*/
void sessionClosed(String clientID) {
sessionManager.destroyClient(clientID);
}
/**
* Returns the singleton instance of the JWicRuntime.
* @return
*/
public static JWicRuntime getJWicRuntime() {
if (!initialized) {
synchronized (JWicRuntime.class) {
if (!initialized) {
singleton = new JWicRuntime();
initialized = true;
}
}
}
return singleton;
}
/**
* Returns the SessionManager. The SessionManager should only be used
* for debug and diagnostic purpose.
*
* @return
*/
public SessionManager getSessionManager() {
return sessionManager;
}
/**
* Returns the renderer with the specified id. If no renderer can be
* found, a JWicException is thrown.
* @param rendererId
* @return
*/
public static IControlRenderer getRenderer(String rendererId) throws JWicException {
IControlRenderer renderer = singleton.renderers.get(rendererId);;
if (renderer == null) {
throw new JWicException("Renderer unknown: " + rendererId);
}
return renderer;
}
/**
* Returns the session with the specified ID.
* @param clientID
* @param sessionID
* @return
*/
public SessionContext getSessionContext(String clientID, String sessionID) {
return getSessionContext(clientID, sessionID, null);
}
/**
* Returns the session with the specified ID.
* Request is used to pass parameter to SessionEvent.
* @param clientID
* @param sessionID
* @param request
* @return
*/
public SessionContext getSessionContext(String clientID, String sessionID, HttpServletRequest request) {
SessionContainer container = sessionManager.get(clientID, sessionID);
if (container != null) {
if (container.getState() == SessionContainer.STATE_STORED) {
sessionManager.deserialize(container);
} else if (container.getState() == SessionContainer.STATE_DESTROYED) {
throw new JWicException("Session is destroyed.");
}
container.access();
SessionContext sc = container.getSessionContext();
if (request.getMethod().equals("GET")) { // only fire reused event on "GET" requests.
log.debug("SessionReused : " + sessionID);
sc.fireEvent(new SessionEvent(request == null ? null : Compatibility.getParameterMap(request)), SessionContext.SESSION_REUSED);
}
return sc;
}
return null;
}
/**
* Creates a new SessionContext with the application bean specified in the
* appProperties argument. The appProperties must contain the name and beanId
* of the application control.
*
* Sample:<br>
* <pre> appid=myapp.id
* name=app
* control=myapp.AppControl </pre>
*
* The request argument is optional. It can be <code>null</code> if you are creating an
* Application for test cases. In this case, the session is always handled as multisession.
*
* @param appProperties
* @return
*/
public SessionContext createSessionContext(IApplicationSetup appSetup, Locale locale, TimeZone timeZone, HttpServletRequest request) {
HttpSession session = request != null ? request.getSession() : null;
String clientID = session != null ? session.getId() : "test"; // testenvironment
if (session != null && session.getAttribute(ATTR_NOTIFIER) == null) {
// Add a "listener" so we know when the session is closed.
session.setAttribute(ATTR_NOTIFIER, new HttpSessionClosedListener(this, session.getId()));
}
SessionContext sc = null;
if (appSetup.isSingleSession() && session != null) {
SessionContainer container = sessionManager.getByAppID(clientID, appSetup.getName());
if (container == null) {
sc = setupSessionContext(appSetup, locale, timeZone, request);
} else {
// notify the session that it has been "reused"
if (container.getState() == SessionContainer.STATE_STORED) {
sessionManager.deserialize(container);
}
container.access();
sc = container.getSessionContext();
sc.fireEvent(new SessionEvent(Compatibility.getParameterMap(request)), SessionContext.SESSION_REUSED);
}
} else {
sc = setupSessionContext(appSetup, locale, timeZone, request);
}
return sc;
}
/**
* Create a new SessionContext instance.
* @param appProperties
* @param locale
* @param timeZone
* @param request
* @return
*/
private SessionContext setupSessionContext(IApplicationSetup appSetup, Locale locale, TimeZone timeZone, HttpServletRequest request) {
log.debug("creating new SessionContext for application '" + appSetup.getName() + "'.");
SessionContext sc = new SessionContext(appSetup, locale, timeZone);
String clientID;
if (request != null) {
clientID = request.getSession().getId();
HashMap<String, String[]> parameters = new HashMap<String, String[]>();
parameters.putAll(Compatibility.getParameterMap(request));
sc.setInitParameters(parameters);
} else {
clientID = "test";
}
sc.setClientId(clientID);
IApplication app = appSetup.createApplication();
sc.setApplication(app);
SessionContainer container = sessionManager.create(clientID, appSetup.getName());
boolean cleanUp = true;
try {
container.setSessionContext(sc);
sc.setSessionId(container.getId());
sc.setUserAgent(new UserAgentInfo(request));
app.initialize(sc);
Control root = app.createRootControl(sc);
// push root control only if no control had been push during root creation
if (sc.getTopControl() == null) {
sc.pushTopControl(root);
}
sc.fireEvent(new SessionEvent(request == null ? null : Compatibility.getParameterMap(request)), SessionContext.SESSION_STARTED );
cleanUp = false;
} finally {
if (cleanUp) {
log.warn("Session was not created successfully!");
sessionManager.remove(container);
}
}
return sc;
}
/**
* Invoked by the SessionContext when the sessionContext is destroyed.
* The context is then removed from the singleSession map.
* @param context
*/
void sessionDestroyed(SessionContext context) {
log.debug("Session " + context.getClientId() + " destroyed.");
// remove from local single_session cache
SessionContainer container = sessionManager.get(context.getClientId(), context.getSessionId());
if (container != null) {
sessionManager.remove(container);
}
}
/**
* Setup the JWicRuntime from the jwic-setup.xml file. The setup
* defines the available renderer and global settings.
* @param in
*/
public void setupRuntime(InputStream stream) {
try {
SAXReader reader = new SAXReader();
reader.setEntityResolver(new DTDEntityResolver(DTD_PUBLICID, DTD_SYSTEMID, DTD_RESOURCEPATH));
reader.setIncludeExternalDTDDeclarations(false);
Document document = reader.read(stream);
readDocument(document);
sessionManager = new SessionManager(getSavePath());
sessionManager.setStoreTime(sessionStoreTime);
} catch (NoClassDefFoundError ncdfe) {
if (ncdfe.getMessage().indexOf("dom4j") != -1) {
log.error("Can not read jwic-setup.xml: the dom4j library is not in the classpath.");
throw new RuntimeException("Can not read jwic-setup.xml: the dom4j library is not in the classpath.", ncdfe);
}
log.error("Error reading jwic-setup.xml.", ncdfe);
throw new RuntimeException("Error reading jwic-setup.xml: " + ncdfe, ncdfe);
} catch (Exception e) {
throw new RuntimeException("Error reading jwic-setup.xml: " + e, e);
}
}
/**
* Read the setup document.
* @param document
*/
private void readDocument(Document document) {
Element root = document.getRootElement();
for (Iterator<?> it = root.elementIterator(); it.hasNext(); ) {
Element node = (Element)it.next();
String nodeName = node.getName();
// setup a velocity-engine
if (nodeName.equals("velocity-engine")) {
String veId = node.attribute("id").getValue();
Properties prop = null;
for (Iterator<?> pi = node.elementIterator(); pi.hasNext(); ) {
Element node2 = (Element)pi.next();
if (node2.getName().equals("properties")) {
prop = XMLTool.getProperties(node2);
}
}
// replace ${rootPath} with real path
ConfigurationTool.insertRootPath(prop);
VelocityEngine engine = new VelocityEngine();
try {
engine.init(prop);
} catch (Exception e) {
String msg = "Can not instanciate velocity engine '" + veId + "'";
log.error(msg, e);
throw new RuntimeException(msg + e, e);
}
velocityEngines.put(veId, engine);
} else if (nodeName.equals("session-swap-time")) {
sessionStoreTime = Integer.parseInt(node.getText());
} else if (nodeName.equals("session-storage-path")) {
setSavePath(ConfigurationTool.insertRootPath(node.getText()));
} else if (nodeName.equals("renderer")) {
String id = node.attribute("id").getValue();
String type = node.attribute("type").getValue();
String classname = node.attribute("classname").getValue();
IControlRenderer renderer;
try {
renderer = (IControlRenderer) Class.forName(classname).newInstance();
} catch (Exception e) {
String msg = "Can not instanciate renderer '" + id + "'";
log.error(msg, e);
throw new RuntimeException(msg + e, e);
}
if (type.equals("velocity")) {
Element nEngine = node.element("engine");
if (nEngine != null) {
String engineId = nEngine.getText();
if (renderer instanceof BaseVelocityRenderer) {
BaseVelocityRenderer vr = (BaseVelocityRenderer)renderer;
VelocityEngine engine = velocityEngines.get(engineId);
if (engine == null) {
throw new RuntimeException("Specified velocity engine not found: " + engineId);
}
vr.setVelocityEngine(engine);
} else {
throw new RuntimeException("renderer " + id + " is specified as type 'velocity' but is not instance of BaseVelocityRenderer");
}
}
}
renderers.put(id, renderer);
}
}
}
/**
* @return Returns the contextPath.
*/
public String getContextPath() {
return contextPath;
}
/**
* @param contextPath The contextPath to set.
*/
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
/**
* Returns the path where the serialized applications are stored.
* @return
*/
public String getSavePath() {
return savePath;
}
/**
* Set the path where the serialized applications are stored.
* @param savePath
*/
public void setSavePath(String savePath) {
if (savePath.endsWith("/") || savePath.endsWith("\\")) {
this.savePath = savePath;
} else {
this.savePath = savePath + "/";
}
}
/**
* @return Returns the rootPath.
*/
public String getRootPath() {
return rootPath;
}
/**
* Set the root path of the WebApplication.
* @param savepath
* @return
*/
public void setRootPath(String rootPath) {
this.rootPath = rootPath;
}
/**
* Destroy the runtime.
*/
public void destroy() {
log.info("JWicRuntime.destroy()");
sessionManager.destroy();
initialized = false;
singleton = null;
}
}