/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.jsp;
import com.caucho.java.JavaCompiler;
import com.caucho.java.LineMap;
import com.caucho.server.dispatch.ServletConfigImpl;
import com.caucho.server.http.CauchoRequest;
import com.caucho.server.http.CauchoResponse;
import com.caucho.server.http.RequestAdapter;
import com.caucho.server.http.ResponseAdapter;
import com.caucho.server.webapp.WebApp;
import com.caucho.util.Base64;
import com.caucho.util.CharBuffer;
import com.caucho.util.RegistryException;
import com.caucho.vfs.Depend;
import com.caucho.vfs.Path;
import com.caucho.vfs.PersistentDependency;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;
import com.caucho.xml.CauchoDocument;
import com.caucho.xml.Html;
import com.caucho.xml.Xml;
import com.caucho.xml.XmlParser;
import com.caucho.xml.XmlUtil;
import com.caucho.xpath.XPath;
import com.caucho.xpath.XPathException;
import com.caucho.xsl.CauchoStylesheet;
import com.caucho.xsl.StylesheetImpl;
import com.caucho.xsl.TransformerImpl;
import com.caucho.xsl.XslParseException;
import org.w3c.dom.Document;
import org.w3c.dom.ProcessingInstruction;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.PageContext;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.concurrent.*;
/**
* XtpPage represents the compiled page.
*/
class XtpPage extends Page {
private static final Logger log
= Logger.getLogger(XtpPage.class.getName());
private boolean _strictXml;
private boolean _toLower = true;
private boolean _entitiesAsText = false;
private Path _sourcePath;
private Path _pwd;
private String _uri;
private String _className;
private String _errorPage;
private WebApp _webApp;
private XslManager _xslManager;
private Page _page;
private HashMap<String,SoftReference<Page>> _varyMap;
private ArrayList<String> _paramNames;
private JspManager _jspManager;
private final Semaphore _compileSemaphore = new Semaphore(1, false);
/**
* Creates a new XTP page.
*
* @param path file containing the xtp page
* @param uri the request uri for the page
* @param className the mangled classname for the page
* @param uriPwd uri working dir for include() or forward()
* @param req the servlet request
* @param xslManager manager for the XSL stylesheets
* @param strictXml if true, use strict XML, now HTML
*/
XtpPage(Path path, String uri, String className,
WebApp app,
XslManager xslManager, boolean strictXml)
throws ServletException, RegistryException
{
_sourcePath = path;
_sourcePath.setUserPath(uri);
_pwd = _sourcePath.getParent();
_className = className;
_webApp = app;
_strictXml = strictXml;
_xslManager = xslManager;
_uri = uri;
ServletConfigImpl config = new ServletConfigImpl();
config.setServletContext(_webApp);
init(config);
}
/**
* Sets the JspManager for the page.
*/
void setManager(JspManager manager)
{
_jspManager = manager;
}
/**
* When true, HTML in XTP is normalized to lower-case.
*/
void setHtmlToLower(boolean toLower)
{
_toLower = toLower;
}
/**
* When true, XML entities are parsed as text.
*/
void setEntitiesAsText(boolean entitiesAsText)
{
_entitiesAsText = entitiesAsText;
}
/**
* Returns true if the sources creating this page have been modified.
*/
public boolean _caucho_isModified()
{
return false;
}
/**
* Handle a request.
*
* @param req the servlet request
* @param res the servlet response
*/
public void service(ServletRequest request, ServletResponse response)
throws IOException, ServletException
{
CauchoRequest req;
if (request instanceof CauchoRequest)
req = (CauchoRequest) request;
else
req = RequestAdapter.create((HttpServletRequest) request, _webApp);
CauchoResponse res;
ResponseAdapter resAdapt = null;
if (response instanceof CauchoResponse)
res = (CauchoResponse) response;
else {
resAdapt = ResponseAdapter.create((HttpServletResponse) response);
res = resAdapt;
}
try {
service(req, res);
} catch (InterruptedException e) {
log.log(Level.FINE, e.toString(), e);
log.warning("XTP: interrupted for " + req.getPageURI());
res.sendError(503, "Server busy: XTP generation delayed");
} finally {
if (resAdapt != null)
resAdapt.close();
}
}
/**
* Handle a request.
*
* @param req the servlet request
* @param res the servlet response
*/
public void service(CauchoRequest req, CauchoResponse res)
throws IOException, ServletException, InterruptedException
{
Page page = getPage(req, res);
if (page != null) {
page.pageservice(req, res);
}
else {
log.warning("XTP: server busy on " + req.getPageURI());
res.setHeader("Retry-After", "15");
res.sendError(503, "Server busy: XTP generation delayed");
}
}
/**
* Returns the page.
*/
private Page getPage(CauchoRequest req, CauchoResponse res)
throws IOException, ServletException, InterruptedException
{
String ss = null;
String varyName = null;
Page page = _page;
Page deadPage = null;
if (page == null) {
if (_varyMap != null) {
varyName = generateVaryName(req);
if (varyName != null) {
SoftReference<Page> ref = _varyMap.get(varyName);
page = ref != null ? ref.get() : null;
}
}
}
if (page != null && ! page._caucho_isModified())
return page;
deadPage = page;
page = null;
long timeout = deadPage == null ? 30L : 5L;
Thread.interrupted();
if (_compileSemaphore.tryAcquire(timeout, TimeUnit.SECONDS)) {
try {
varyName = generateVaryName(req);
page = getPrecompiledPage(req, varyName);
if (page == null) {
CauchoDocument doc;
try {
doc = parseXtp();
} catch (FileNotFoundException e) {
res.sendError(404);
throw e;
}
Templates stylesheet = compileStylesheet(req, doc);
// the new stylesheet affects the vary name
varyName = generateVaryName(req);
page = getPrecompiledPage(req, varyName);
if (page == null)
page = compileJspPage(req, res, doc, stylesheet, varyName);
}
if (page != null) {
ServletConfigImpl config = new ServletConfigImpl();
config.setServletContext(_webApp);
page.init(config);
if (varyName != null && _varyMap == null)
_varyMap = new HashMap<String,SoftReference<Page>>(8);
if (varyName != null)
_varyMap.put(varyName, new SoftReference<Page>(page));
else
_page = page;
}
else if (deadPage != null) {
_page = null;
if (varyName != null && _varyMap != null)
_varyMap.remove(varyName);
}
} finally {
_compileSemaphore.release();
}
}
else {
log.warning("XTP: semaphore timed out on " + req.getPageURI());
}
if (page != null)
return page;
else
return deadPage;
}
/**
* Try to load a precompiled version of the page.
*
* @param req the request for the page.
* @param varyName encoding for the variable stylesheet and parameters
* @return the precompiled page or null
*/
private Page getPrecompiledPage(CauchoRequest req, String varyName)
throws IOException, ServletException
{
Page page = null;
String className = getClassName(varyName);
try {
page = _jspManager.preload(className,
_webApp.getClassLoader(),
_webApp.getRootDirectory(),
null);
if (page != null) {
if (log.isLoggable(Level.FINE))
log.fine("XTP using precompiled page " + className);
return page;
}
} catch (Throwable e) {
log.log(Level.FINE, e.toString(), e);
}
return null;
}
/**
* Parses the XTP file as either an XML document or an HTML document.
*/
private CauchoDocument parseXtp()
throws IOException, ServletException
{
ReadStream is = _sourcePath.openRead();
try {
XmlParser parser;
if (_strictXml) {
parser = new Xml();
parser.setEntitiesAsText(_entitiesAsText);
}
else {
parser = new Html();
parser.setAutodetectXml(true);
parser.setEntitiesAsText(true);
// parser.setXmlEntitiesAsText(entitiesAsText);
parser.setToLower(_toLower);
}
parser.setResinInclude(true);
parser.setJsp(true);
return (CauchoDocument) parser.parseDocument(is);
} catch (Exception e) {
JspParseException jspE = JspParseException.create(e);
jspE.setErrorPage(_errorPage);
throw jspE;
} finally {
is.close();
}
}
/**
* Compiles a stylesheet pages on request parameters and the parsed
* XML file.
*
* @param req the servlet request.
* @param doc the parsed XTP file as a DOM tree.
*
* @return the compiled stylesheet
*/
private Templates compileStylesheet(CauchoRequest req, CauchoDocument doc)
throws IOException, ServletException
{
String ssName = (String) req.getAttribute("caucho.xsl.stylesheet");
Templates stylesheet = null;
try {
if (ssName == null)
ssName = getStylesheetHref(doc, null);
stylesheet = _xslManager.get(ssName, req);
} catch (XslParseException e) {
JspParseException jspE;
if (e.getException() != null)
jspE = new JspParseException(e.getException());
else
jspE = new JspParseException(e);
jspE.setErrorPage(_errorPage);
throw jspE;
} catch (Exception e) {
JspParseException jspE;
jspE = new JspParseException(e);
jspE.setErrorPage(_errorPage);
throw jspE;
}
ArrayList<String> params = null;
if (stylesheet instanceof StylesheetImpl) {
StylesheetImpl ss = (StylesheetImpl) stylesheet;
params = (ArrayList) ss.getProperty(CauchoStylesheet.GLOBAL_PARAM);
}
for (int i = 0; params != null && i < params.size(); i++) {
String param = params.get(i);
if (_paramNames == null)
_paramNames = new ArrayList<String>();
if (param.equals("xtp:context_path") ||
param.equals("xtp:servlet_path"))
continue;
if (! _paramNames.contains(param))
_paramNames.add(param);
}
return stylesheet;
}
/**
* Mangles the page name to generate a unique page name.
*
* @param req the servlet request.
* @param res the servlet response.
* @param stylesheet the stylesheet.
* @param varyName the unique query.
*
* @return the compiled page.
*/
private Page compileJspPage(CauchoRequest req,
CauchoResponse res,
CauchoDocument doc,
Templates stylesheet,
String varyName)
throws IOException, ServletException
{
// changing paramNames changes the varyName
varyName = generateVaryName(req);
String className = getClassName(varyName);
try {
return getJspPage(doc, stylesheet, req, res, className);
} catch (TransformerConfigurationException e) {
throw new ServletException(e);
} catch (JspException e) {
throw new ServletException(e);
}
}
/**
* Mangles the classname
*/
private String getClassName(String varyName)
{
if (varyName == null)
return _className;
else
return _className + JavaCompiler.mangleName("?" + varyName);
}
/**
* Generates a unique string for the variable parameters. The parameters
* depend on:
* <ul>
* <li>The value of caucho.xsl.stylesheet selecting the stylesheet.
* <li>The top-level xsl:param variables, which use request parameters.
* <li>The request's path-info.
* </ul>
*
* @param req the page request.
*
* @return a unique string encoding the important variations of the request.
*/
private String generateVaryName(CauchoRequest req)
{
CharBuffer cb = CharBuffer.allocate();
String ss = (String) req.getAttribute("caucho.xsl.stylesheet");
if (ss == null && (_paramNames == null || _paramNames.size() == 0))
return null;
if (ss != null) {
cb.append("ss.");
cb.append(ss);
}
for (int i = 0; _paramNames != null && i < _paramNames.size(); i++) {
String name = (String) _paramNames.get(i);
String value;
if (name.equals("xtp:path_info"))
value = req.getPathInfo();
else
value = req.getParameter(name);
cb.append(".");
cb.append(name);
if (value != null) {
cb.append(".");
cb.append(value);
}
}
if (cb.length() == 0)
return null;
if (cb.length() < 64)
return cb.close();
long hash = 37;
for (int i = 0; i < cb.length(); i++)
hash = 65521 * hash + cb.charAt(i);
cb.setLength(32);
Base64.encode(cb, hash);
return cb.close();
}
/**
* Compile a JSP page.
*
* @param doc the parsed Serif page.
* @param stylesheet the stylesheet
* @param req the servlet request
* @param res the servlet response
* @param className the className of the generated page
*
* @return the compiled JspPage
*/
private Page getJspPage(CauchoDocument doc, Templates stylesheet,
CauchoRequest req, CauchoResponse res,
String className)
throws IOException, ServletException, JspException, TransformerConfigurationException
{
Path workDir = _jspManager.getClassDir();
String fullClassName = className;
Path path = workDir.lookup(fullClassName.replace('.', '/') + ".jsp");
path.getParent().mkdirs();
Properties output = stylesheet.getOutputProperties();
String encoding = (String) output.get(OutputKeys.ENCODING);
String mimeType = (String) output.get(OutputKeys.MEDIA_TYPE);
String method = (String) output.get(OutputKeys.METHOD);
if (method == null || encoding != null) {
}
else if (method.equals("xml"))
encoding = "UTF-8";
javax.xml.transform.Transformer transformer;
transformer = stylesheet.newTransformer();
for (int i = 0; _paramNames != null && i < _paramNames.size(); i++) {
String param = (String) _paramNames.get(i);
transformer.setParameter(param, req.getParameter(param));
}
String contextPath = req.getContextPath();
if (contextPath != null && ! contextPath.equals(""))
transformer.setParameter("xtp:context_path", contextPath);
String servletPath = req.getServletPath();
if (servletPath != null && ! servletPath.equals(""))
transformer.setParameter("xtp:servlet_path", servletPath);
String pathInfo = req.getPathInfo();
if (pathInfo != null && ! pathInfo.equals(""))
transformer.setParameter("xtp:path_info", pathInfo);
transformer.setOutputProperty("caucho.jsp", "true");
LineMap lineMap = null;
WriteStream os = path.openWrite();
try {
if (encoding != null) {
os.setEncoding(encoding);
if (mimeType == null)
mimeType = "text/html";
os.print("<%@ page contentType=\"" + mimeType + "; charset=" +
encoding + "\" %>");
}
else if (mimeType != null)
os.print("<%@ page contentType=\"" + mimeType + "\" %>");
lineMap = writeJspDoc(os, doc, transformer, req, res);
} finally {
os.close();
}
StylesheetImpl ss = null;
if (stylesheet instanceof StylesheetImpl)
ss = (StylesheetImpl) stylesheet;
try {
path.setUserPath(_sourcePath.getPath());
boolean cacheable = true; // jspDoc.isCacheable();
ArrayList<PersistentDependency> depends =
new ArrayList<PersistentDependency>();
ArrayList<Depend> styleDepends = null;
if (ss != null)
styleDepends = (ArrayList) ss.getProperty(StylesheetImpl.DEPENDS);
for (int i = 0; styleDepends != null && i < styleDepends.size(); i++) {
Depend depend = styleDepends.get(i);
Depend jspDepend = new Depend(depend.getPath(),
depend.getLastModified(),
depend.getLength());
jspDepend.setRequireSource(true);
if (! depends.contains(jspDepend))
depends.add(jspDepend);
}
// Fill the page dependency information from the document into
// the jsp page.
ArrayList<Path> docDepends;
docDepends = (ArrayList) doc.getProperty(CauchoDocument.DEPENDS);
for (int i = 0; docDepends != null && i < docDepends.size(); i++) {
Path depend = docDepends.get(i);
Depend jspDepend = new Depend(depend);
if (! depends.contains(jspDepend))
depends.add(jspDepend);
}
// stylesheet cache dependencies are normal dependencies for JSP
ArrayList<Path> cacheDepends = null;
TransformerImpl xform = null;
if (transformer instanceof TransformerImpl)
xform = (TransformerImpl) transformer;
if (xform != null)
cacheDepends = (ArrayList) xform.getProperty(TransformerImpl.CACHE_DEPENDS);
for (int i = 0; cacheDepends != null && i < cacheDepends.size(); i++) {
Path depend = cacheDepends.get(i);
Depend jspDepend = new Depend(depend);
if (! depends.contains(jspDepend))
depends.add(jspDepend);
}
ServletConfig config = null;
Page page = _jspManager.createGeneratedPage(path, _uri, className,
config, depends);
return page;
} catch (IOException e) {
throw e;
} catch (ServletException e) {
throw e;
} catch (Exception e) {
throw new QJspException(e);
}
}
/**
* Transform XTP page with the stylesheet to JSP source.
*
* @param os the output stream to the generated JSP.
* @param doc the parsed XTP file.
* @param transformed the XSL stylesheet with parameters applied.
* @param req the servlet request.
* @param res the servlet response.
*
* @return the line map from the JSP to the original source.
*/
private LineMap writeJspDoc(WriteStream os,
Document doc,
javax.xml.transform.Transformer transformer,
CauchoRequest req,
CauchoResponse res)
throws IOException, ServletException
{
PageContext pageContext;
JspFactory factory = JspFactory.getDefaultFactory();
TransformerImpl xform = null;
if (transformer instanceof TransformerImpl)
xform = (TransformerImpl) transformer;
String errorPage = null;
if (xform != null)
errorPage = (String) xform.getProperty("caucho.error.page");
pageContext = factory.getPageContext(this,
req, res,
errorPage,
false,
8192, // bufferSize,
false); // autoFlush);
try {
if (xform != null) {
xform.setProperty("caucho.page.context", pageContext);
xform.setProperty("caucho.pwd", Vfs.lookup());
}
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(os);
xform.setFeature(TransformerImpl.GENERATE_LOCATION, true);
transformer.transform(source, result);
if (xform != null)
return (LineMap) xform.getProperty(TransformerImpl.LINE_MAP);
else
return null;
} catch (Exception e) {
pageContext.handlePageException(e);
} finally {
factory.releasePageContext(pageContext);
}
return null;
}
/**
* Returns the stylesheet specified by the page.
*
* The syntax is:
* <pre>
* <?xml-stylesheet href='...' media='...'?>
* </pre>
*
* @param doc the XTP document
* @param media the http request media
*
* @return the href of the xml-stylesheet processing-instruction or
* "default.xsl" if none is found.
*/
private String getStylesheetHref(Document doc, String media)
throws XPathException
{
Iterator iter = XPath.select("//processing-instruction('xml-stylesheet')",
doc);
while (iter.hasNext()) {
ProcessingInstruction pi = (ProcessingInstruction) iter.next();
String value = pi.getNodeValue();
String piMedia = XmlUtil.getPIAttribute(value, "media");
if (piMedia == null || piMedia.equals(media))
return XmlUtil.getPIAttribute(value, "href");
}
return "default.xsl"; // xslManager.getDefaultStylesheet();
}
/**
* Returns true if the document varies according to the "media".
* (Currently unused.)
*/
private boolean varyMedia(Document doc)
throws XPathException
{
Iterator iter = XPath.select("//processing-instruction('xml-stylesheet')",
doc);
while (iter.hasNext()) {
ProcessingInstruction pi = (ProcessingInstruction) iter.next();
String value = pi.getNodeValue();
String piMedia = XmlUtil.getPIAttribute(value, "media");
if (piMedia != null)
return true;
}
return false;
}
public boolean disableLog()
{
return true;
}
/**
* Returns a printable version of the page object
*/
public String toString()
{
return "XtpPage[" + _uri + "]";
}
}