/*
ESXX - The friendly ECMAscript/XML Application Server
Copyright (C) 2007-2015 Martin Blom <martin@blom.org>
This program 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 3
of the License, or (at your option) any later version.
This program 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. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.esxx;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
import javax.mail.internet.ContentType;
import org.esxx.cache.*;
import org.esxx.jmx.WorkloadInfo;
import org.esxx.saxon.*;
import org.esxx.util.SingleThreadedExecutor;
import org.esxx.util.ThreadSafeExecutor;
import org.esxx.util.SyslogHandler;
import org.esxx.util.TrivialFormatter;
import org.esxx.util.URIResolver;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.WrapFactory;
import org.w3c.dom.*;
import org.w3c.dom.bootstrap.*;
import org.w3c.dom.ls.*;
import net.sf.saxon.s9api.*;
import net.sf.saxon.*;
import net.sf.saxon.functions.FunctionLibrary;
import net.sf.saxon.functions.FunctionLibraryList;
public class ESXX {
/** A string that defines the ESXX XML namespace */
public static final String NAMESPACE = "http://esxx.org/1.0/";
public static int THREADS_PER_CORE = 8;
private static ESXX esxx;
public static ESXX getInstance() {
return esxx;
}
public static ESXX initInstance(Properties p, Object h) {
esxx = new ESXX(p, h);
return esxx;
}
public static void destroyInstance() {
if (esxx != null && esxx.shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(esxx.shutdownHook);
esxx.terminate();
esxx = null;
}
}
/** The constructor.
*
* Will initialize the operating environment, start the worker
* threads and initialize the JavaScript contexts.
*
* @param p A set of properties that can be used to tune the
* execution.
*
* @param h A host object that can later be referenced by the
* JavaScript code as 'esxx.host'. May be null.
*
*/
private ESXX(Properties p, Object h) {
settings = p;
hostObject = h;
defaultTimeout = (int) (Double.parseDouble(p.getProperty("esxx.app.timeout", "60"))
* 1000);
addShutdownHook = Boolean.parseBoolean(p.getProperty("esxx.app.clean_shutdown", "true"));
try {
String[] path = p.getProperty("esxx.app.include_path", "").split(File.pathSeparator);
includePath = new URI[path.length];
for (int i = 0; i < path.length; ++i) {
includePath[i] = new File(path[i]).toURI();
}
}
catch (Exception ex) {
throw new ESXXException("Illegal esxx.app.include_path value: " + ex.getMessage(), ex);
}
applicationCache = new LRUCache<String, Application>(
Integer.parseInt(p.getProperty("esxx.cache.apps.max_entries", "1024")),
(long) (Double.parseDouble(p.getProperty("esxx.cache.apps.max_age", "3600")) * 1000));
applicationCache.addListener(new ApplicationCacheListener());
stylesheetCache = new LRUCache<String, Stylesheet>(
Integer.parseInt(p.getProperty("esxx.cache.xslt.max_entries", "1024")),
(long) (Double.parseDouble(p.getProperty("esxx.cache.xslt.max_age", "3600")) * 1000));
stylesheetCache.addListener(new StylesheetCacheListener());
schemaCache = new LRUCache<String, Schema>(
Integer.parseInt(p.getProperty("esxx.cache.schema.max_entries", "1024")),
(long) (Double.parseDouble(p.getProperty("esxx.cache.schema.max_age", "3600")) * 1000));
schemaCache.addListener(new SchemaCacheListener());
parsers = new Parsers();
// Custom CGI-to-HTTP translations
cgiToHTTPMap = new HashMap<String,String>();
cgiToHTTPMap.put("HTTP_SOAPACTION", "SOAPAction");
cgiToHTTPMap.put("CONTENT_TYPE", "Content-Type");
cgiToHTTPMap.put("CONTENT_LENGTH", "Content-Length");
cgiToHTTPMap.put("Authorization", "Authorization"); // For mod_fastcgi
// ESXX is a singleton, so it's OK to call the static method
// ContextFactory.initGlobal() here
contextFactory = new ESXXContextFactory();
ContextFactory.initGlobal(contextFactory);
// Make sure all threads we create ourselves have a valid Context
ThreadFactory tf = new ThreadFactory() {
public Thread newThread(final Runnable r) {
return new Thread("ESXX-Worker-" + cnt.incrementAndGet()) {
@Override public void run() {
contextFactory.call(new ContextAction() {
@Override public Object run(Context cx) {
r.run();
return null;
}
});
}
};
}
private java.util.concurrent.atomic.AtomicInteger cnt = new java.util.concurrent.atomic.AtomicInteger();
};
int default_threads = THREADS_PER_CORE * Runtime.getRuntime().availableProcessors();
int worker_threads = Integer.parseInt(p.getProperty("esxx.worker_threads", Integer.toString(default_threads)));
if (worker_threads == -1) {
// Use an unbounded thread pool
executorService = new ThreadSafeExecutor(tf);
}
else if (worker_threads == 0) {
executorService = new SingleThreadedExecutor();
}
else {
executorService = new ThreadSafeExecutor(worker_threads, tf);
}
workloadSet = new PriorityBlockingQueue<Workload>(16, new Comparator<Workload>() {
public int compare(Workload w1, Workload w2) {
return Long.signum(w1.getExpires() - w2.getExpires());
}
});
mxRegister("Workloads", null, new WorkloadJMXBean());
// Add periodic Workload cancellation (if not single-threaded)
if (worker_threads != 0) {
executorService.scheduleAtFixedRate(new WorkloadCancellator(), 1, 1, TimeUnit.SECONDS);
}
// Add periodic check to expunge applications and xslt stylesheets
executorService.scheduleWithFixedDelay(new CacheFilter(), 1, 1, TimeUnit.SECONDS);
// org.mozilla.javascript.tools.debugger.Main main =
// new org.mozilla.javascript.tools.debugger.Main("ESXX Debugger");
// main.doBreak();
// main.attachTo(contextFactory);
// main.pack();
// main.setSize(800, 600);
// main.setVisible(true);
if (addShutdownHook) {
try {
// Terminate all apps when the JVM exits
shutdownHook = new Thread() {
public void run() {
terminate();
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
catch(Exception ex) {
getLogger().logp(Level.WARNING, null, null, "Failed to add shutdown hook");
}
}
}
/** Terminate all apps and shut down worker threads */
private void terminate() {
// Workload w;
// while ((w = workloadSet.poll()) != null) {
// if (w.interruptable) {
// w.future.cancel(true /* may interrupt */);
// }
// }
applicationCache.clear();
stylesheetCache.clear();
schemaCache.clear();
shutdownAndAwaitTermination(executorService);
}
/** Returns the settings Properties object
*
* @returns A Properties object.
*/
public Properties getSettings() {
return settings;
}
/** Returns the host object
*
* @returns A host object (for instance, a Servlet).
*/
public Object getHostObject() {
return hostObject;
}
public void setNoHandlerMode(String match) {
noHandlerMode = Pattern.compile(match == null ? ".*" : match);
}
public boolean isHandlerMode(String server_software) {
if (server_software == null) {
return true;
}
return ! noHandlerMode.matcher(server_software).matches();
}
/** Returns a global, non-application tied Logger.
*
* @returns A Logger object (singleton).
*/
public synchronized Logger getLogger() {
if (logger == null) {
logger = SyslogHandler.createLogger(ESXX.class.getName(), Level.CONFIG, "esxx", -1);
}
return logger;
}
/** Return the ScheduledExecutorService used to execute jobs. */
public ScheduledExecutorService getExecutor() {
return executorService;
}
/** Adds a Request to the work queue.
*
* Once the request has been executed, Request.finished will be
* called with an ignorable returncode and a set of HTTP headers.
*
* @param request The Request object that is to be executed.
* @param timeout The timeout in milliseconds. Note that this is
* the time from the time of submission, not from the
* time the request actually starts processing.
*/
public Workload addRequest(final Request request, final ResponseHandler rh, int timeout) {
return addContextAction(null, new ContextAction() {
public Object run(Context cx) {
try {
Response response = request.getQuickResponse();
if (response == null) {
response = new Worker(ESXX.this).handleRequest(cx, request);
}
return rh.handleResponse(response);
}
catch (Throwable t) {
return rh.handleError(t);
}
}
}, request.toString(), timeout);
}
public Workload addContextAction(Context old_cx, final ContextAction ca,
final String name, int timeout) {
long expires;
if (timeout == -1) {
expires = Long.MAX_VALUE;
}
else if (timeout == 0) {
expires = System.currentTimeMillis() + defaultTimeout;
}
else {
expires = System.currentTimeMillis() + timeout;
}
if (old_cx != null) {
Workload old_work = (Workload) old_cx.getThreadLocal(Workload.class);
if (old_work != null && old_work.getExpires() < expires) {
// If we're already executing a workload, never extend the timeout
expires = old_work.getExpires();
}
}
final Workload workload = new Workload(name, expires);
workloadSet.add(workload);
synchronized (workload) {
workload.future = executorService.submit(new Callable<Object>() {
public Object call()
throws Exception {
return contextFactory.call(new ContextAction() {
@Override public Object run(Context new_cx) {
Object old_workload = new_cx.getThreadLocal(Workload.class);
// Thread thread = Thread.currentThread();
// String old_name = thread.getName();
workload.open();
new_cx.putThreadLocal(Workload.class, workload);
// thread.setName(old_name + " - " + name);
try {
return ca.run(new_cx);
}
finally {
if (old_workload != null) {
new_cx.putThreadLocal(Workload.class, old_workload);
}
else {
new_cx.removeThreadLocal(Workload.class);
}
workload.close();
workloadSet.remove(workload);
// thread.setName(old_name);
}
}
});
}
});
}
return workload;
}
public static void checkTimeout(Context cx)
throws ESXXException.TimeOut {
Workload workload = (Workload) cx.getThreadLocal(Workload.class);
if (workload == null) {
return;
}
if (workload.isTimedOut()) {
ESXX.getInstance().getLogger().logp(Level.FINE, null, null,
"Workload " + workload
+ " timed out: throwing TimeOut");
throw new ESXXException.TimeOut();
}
}
/** Utility method that serializes a W3C DOM Node to a String.
*
* @param node The Node to be serialized.
*
* @return A String containing the XML representation of the supplied Node.
*/
public String serializeNode(org.w3c.dom.Node node) {
try {
LSSerializer ser = getDOMImplementationLS().createLSSerializer();
DOMConfiguration dc = ser.getDomConfig();
dc.setParameter("xml-declaration", false);
return ser.writeToString(node);
}
catch (LSException ex) {
// Should never happen
ex.printStackTrace();
throw new ESXXException("Unable to serialize DOM Node: " + ex.getMessage(), ex);
}
}
/** Utility method that converts a W3C DOM Node into an E4X XML object.
*
* @param node The Node to be converted.
*
* @param cx The current JavaScript context.
*
* @param scope The current JavaScript scope.
*
* @return A Scriptable representing an E4X XML object.
*/
public static Scriptable domToE4X(org.w3c.dom.Node node, Context cx, Scriptable scope) {
if (node == null) {
return null;
}
return cx.newObject(scope, "XML", new org.w3c.dom.Node[] { node });
}
/** Utility method that converts an E4X XML object into a W3C DOM Node.
*
* @param node The E4X XML node to be converted.
*
* @return A W3C DOM Node.
*/
public static org.w3c.dom.Node e4xToDOM(Scriptable node) {
if ("XMLList".equals(node.getClassName())) {
// If an XMLList object, return only the first member
node = (Scriptable) node.get(0, node);
}
return org.mozilla.javascript.xmlimpl.XMLLibImpl.toDomNode(node);
}
/** Utility method to create a new W3C DOM document.
*
* @param name The name of the document element
*
* @return A W3C DOM Document.
*/
public Document createDocument(String name) {
return getDOMImplementation().createDocument(null, name, null);
}
/** Utility method that translates the name of a CGI environment
* variable into it's original HTTP header name.
*
* @param name The name of a CGI variable.
*
* @return The name of the original HTTP header, or null if this
* variable name is unknown.
*/
public String cgiToHTTP(String name) {
String h = cgiToHTTPMap.get(name);
// If there was a mapping, use it
if (h != null) {
return h;
}
if (name.startsWith("HTTP_")) {
// "Guess" the name by capitalizing the variable name
StringBuilder str = new StringBuilder();
boolean cap = true;
for (int i = 5; i < name.length(); ++i) {
char c = name.charAt(i);
if (c == '_') {
str.append('-');
cap = true;
}
else if (cap) {
str.append(Character.toUpperCase(c));
cap = false;
}
else {
str.append(Character.toLowerCase(c));
}
}
return str.toString();
}
else {
return null;
}
}
/** Utility method that translates the name of an HTTP header into
* the CGI environment variable name.
*
* @param name The name of an HTTP header.
*
* @return The name of the CGI variable.
*/
public static String httpToCGI(String name) {
if (name.equals("Content-Type")) {
return "CONTENT_TYPE";
}
else if (name.equals("Content-Length")) {
return "CONTENT_LENGTH";
}
else {
return "HTTP_" + name.toUpperCase().replaceAll("-", "_");
}
}
public static <T> T coalesce(T ...objects) {
for (T obj : objects) {
if (obj != null) {
return obj;
}
}
return null;
}
public URI[] getIncludePath() {
return includePath;
}
/** Utility method that parses an InputStream into a W3C DOM
* Document.
*
* @param is The InputStream to be parsed.
*
* @param is_uri The location of the InputStream.
*
* @param external_uris A Collection of URIs that will be
* populated with all URIs visited during the parsing. Can be
* 'null'.
*
* @param err A PrintWriter that will be used to report parser
* errors. Can be 'null'.
*
* @return A W3C DOM Document.
*
* @throws org.xml.sax.SAXException On parser errors.
*
* @throws IOException On I/O errors.
*/
public Document parseXML(InputStream is, URI is_uri,
final Collection<URI> external_uris,
final PrintWriter err)
throws ESXXException {
DOMImplementationLS di = getDOMImplementationLS();
LSInput in = di.createLSInput();
in.setSystemId(is_uri.toString());
in.setByteStream(is);
LSParser p = di.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
DOMErrorHandler eh = new DOMErrorHandler() {
public boolean handleError(DOMError error) {
DOMLocator dl = error.getLocation();
String pos = (dl.getUri() + ", line " + dl.getLineNumber() +
", column " + dl.getColumnNumber());
Throwable rel = (Throwable) error.getRelatedException();
switch (error.getSeverity()) {
case DOMError.SEVERITY_FATAL_ERROR:
case DOMError.SEVERITY_ERROR:
if (rel instanceof ESXXException) {
throw (ESXXException) rel;
}
else {
throw new ESXXException(pos + ": " + error.getMessage(), rel);
}
case DOMError.SEVERITY_WARNING:
err.println(pos + ": " + error.getMessage());
return true;
}
return false;
}
};
DOMConfiguration dc = p.getDomConfig();
URIResolver ur = new URIResolver(this, is_uri, external_uris);
try {
dc.setParameter("comments", false);
dc.setParameter("cdata-sections", false);
dc.setParameter("entities", false);
// dc.setParameter("validate-if-schema", true);
dc.setParameter("error-handler", eh);
dc.setParameter("resource-resolver", ur);
dc.setParameter("http://apache.org/xml/features/xinclude", true);
return p.parse(in);
}
finally {
ur.closeAllStreams();
}
}
public InputStream openCachedURI(URI uri)
throws IOException {
if ("esxx-rsrc".equals(uri.getScheme())) {
InputStream rsrc = getClass().getResourceAsStream("/rsrc/" + uri.getSchemeSpecificPart());
if (rsrc == null) {
throw new FileNotFoundException(uri.toString());
}
return rsrc;
}
URLConnection uc = uri.toURL().openConnection();
uc.setDoInput(true);
uc.setDoOutput(false);
uc.connect();
return uc.getInputStream();
}
public File createTempFile(Context cx)
throws IOException {
File temp = File.createTempFile(getClass().getName(), null);
temp.deleteOnExit();
Workload workload = (Workload) cx.getThreadLocal(Workload.class);
if (workload != null) {
workload.addTempFile(temp);
}
return temp;
}
public Object parseStream(String mime_type, InputStream is, URI is_uri,
Collection<URI> external_uris,
PrintWriter err,
Context cx, Scriptable scope)
throws Exception {
try {
return parseStream( new ContentType(mime_type), is, is_uri, external_uris, err, cx, scope);
}
catch (javax.mail.internet.ParseException ex) {
throw new IOException("Invalid Content-Type: " + ex.getMessage(), ex);
}
}
public Object parseStream(ContentType ct, InputStream is, URI is_uri,
Collection<URI> external_uris,
PrintWriter err,
Context cx, Scriptable scope)
throws Exception {
return parsers.parse(ct, is, is_uri, external_uris, err, cx, scope);
}
public ContentType serializeObject(Object data, ContentType ct,
OutputStream os, boolean close)
throws IOException {
try {
Response response = new Response(0, ct != null ? ct.toString() : null, data, null);
response.writeResult(os);
if (ct == null) {
ct = new ContentType(response.getContentType(true));
}
return ct;
}
catch (javax.mail.internet.ParseException ex) {
// (This should never happen, unless Response.getContentType() is broken)
throw new ESXXException("Invalid Content-Type from Response.getContentType(): "
+ ex.getMessage(), ex);
}
finally {
try {
if (close) {
os.close();
}
else {
os.flush();
}
}
catch (IOException ignored) {}
}
}
public Application getCachedApplication(final Context cx, final Request request)
throws Exception {
String url_string = request.getScriptFilename().toString();
Application app;
while (true) {
app = applicationCache.add(url_string, new LRUCache.ValueFactory<String, Application>() {
public Application create(String key, long expires)
throws IOException {
// The application cache makes sure we are
// single-threaded (per application URL) here, so only
// one Application will ever be created, no matter how
// many concurrent requests there are.
return new Application(cx, request);
}
}, 0);
if (app.enter()) {
break;
}
// We could not "enter" the application, because it had been
// marked for termination but not yet removed from the
// cache. In this (rather unusual) situation, we let some
// other thread execute for a while and then retry again.
Thread.yield();
}
return app;
}
public void releaseApplication(Application app, long start_time) {
app.logUsage(start_time);
app.exit();
}
public void removeCachedApplication(Application app) {
applicationCache.remove(app.getFilename());
}
public Stylesheet getCachedStylesheet(final URI uri, final InputStream is)
throws IOException {
try {
return stylesheetCache.add(uri.toString(), new LRUCache.ValueFactory<String, Stylesheet>() {
public Stylesheet create(String key, long expires)
throws IOException {
return new Stylesheet(uri, is);
}
}, 0);
}
catch (IOException ex) {
throw ex;
}
catch (ESXXException ex) {
throw ex;
}
catch (Exception ex) {
throw new ESXXException("Unexpected exception in getCachedStylesheet(): " + ex.toString(),
ex);
}
}
public void removeCachedStylesheet(Stylesheet xslt) {
stylesheetCache.remove(xslt.getFilename());
}
public Schema getCachedSchema(final URI uri, final InputStream is, final String type)
throws IOException {
try {
return schemaCache.add(uri.toString(), new LRUCache.ValueFactory<String, Schema>() {
public Schema create(String key, long expires)
throws IOException {
return new Schema(uri, is, type);
}
}, 0);
}
catch (IOException ex) {
throw ex;
}
catch (ESXXException ex) {
throw ex;
}
catch (Exception ex) {
ex.printStackTrace();
throw new ESXXException("Unexpected exception in getCachedSchema(): " + ex.toString(),
ex);
}
}
public void removeCachedSchema(Schema schema) {
schemaCache.remove(schema.getFilename());
}
public DOMImplementationLS getDOMImplementationLS() {
return (DOMImplementationLS) getDOMImplementation();
}
public synchronized DOMImplementation getDOMImplementation() {
if (domImplementation == null) {
try {
DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
domImplementation = reg.getDOMImplementation("XML 3.0");
}
catch (Exception ex) {
throw new ESXXException("Unable to get a DOM implementation object: "
+ ex.getMessage(), ex);
}
}
return domImplementation;
}
public synchronized Processor getSaxonProcessor() {
if (saxonProcessor == null) {
saxonProcessor = new Processor(false);
// Hook in our own extension functions
Configuration cfg = saxonProcessor.getUnderlyingConfiguration();
FunctionLibrary java = cfg.getExtensionBinder("java");
FunctionLibraryList fl = new FunctionLibraryList();
fl.addFunctionLibrary(new ESXXFunctionLibrary());
fl.addFunctionLibrary(java);
cfg.setExtensionBinder("java", fl);
}
return saxonProcessor;
}
public synchronized DocumentBuilder getSaxonDocumentBuilder() {
if (saxonDocumentBuilder == null) {
saxonDocumentBuilder = getSaxonProcessor().newDocumentBuilder();
}
return saxonDocumentBuilder;
}
public void mxRegister(String type, String name, javax.management.StandardMBean object) {
try {
javax.management.MBeanServer mbs =
java.lang.management.ManagementFactory.getPlatformMBeanServer();
mbs.registerMBean(object, mxObjectName(type, name));
}
catch (Throwable ex) {
// Probably a Google App Engine problem
getLogger().logp(Level.WARNING, null, null,
"Failed to register " + type + " MXBean " + name);
}
}
public void mxUnregister(String type, String name) {
try {
javax.management.MBeanServer mbs =
java.lang.management.ManagementFactory.getPlatformMBeanServer();
mbs.unregisterMBean(mxObjectName(type, name));
}
catch (Throwable ex) {
// Probably a Google App Engine problem
getLogger().logp(Level.WARNING, null, null,
"Failed to unregister " + type + " MXBean " + name);
}
}
/** A pattern that matches the characters '"', '\', '?' and '*'. */
private static java.util.regex.Pattern mxObjectNamePattern =
java.util.regex.Pattern.compile("(\"\\\\\\?\\*)");
private javax.management.ObjectName mxObjectName(String type, String name)
throws javax.management.MalformedObjectNameException {
String object_name = "org.esxx:type=" + type;
if (name != null) {
// Quote illegal characters
name = mxObjectNamePattern.matcher(name).replaceAll("\\\\$1");
object_name += ",name=\"" + name + "\"";
}
return new javax.management.ObjectName(object_name);
}
public ContextFactory getContextFactory() {
return contextFactory;
}
public static String parseMIMEType(String ct, HashMap<String,String> params) {
String[] parts = ct.split(";");
String type = parts[0].trim();
if (params != null) {
params.clear();
// Add all attributes
for (int i = 1; i < parts.length; ++i) {
String[] attr = parts[i].split("=", 2);
if (attr.length == 2) {
params.put(attr[0].trim(), attr[1].trim());
}
}
}
return type;
}
public static String combineMIMEType(String type, HashMap<String,String> params) {
if (type == null) {
return null;
}
try {
javax.mail.internet.ContentType ct = new javax.mail.internet.ContentType(type);
if (params != null) {
for (Map.Entry<String,String> e : params.entrySet()) {
ct.setParameter(e.getKey(), e.getValue());
}
}
return ct.toString();
}
catch (javax.mail.internet.ParseException ex) {
throw new ESXXException("Failed to parse MIME type " + type + ": " + ex.getMessage(), ex);
}
}
void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
getLogger().logp(Level.SEVERE, null, null, "Pool did not terminate");
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
/** This class monitors all applications that are being loaded and
* unload, and handles MX registration and application exit
* handlers.
*/
private class ApplicationCacheListener
implements LRUCache.LRUListener<String, Application> {
public void entryAdded(String key, Application app) {
mxRegister("Applications", app.getFilename(), app.getJMXBean());
getLogger().logp(Level.CONFIG, null, null, app + " loaded.");
}
public void entryRemoved(String key, final Application app) {
getLogger().logp(Level.CONFIG, null, null, app + " unloading ...");
// In this function, we're single-threaded (per application URI)
app.terminate(defaultTimeout);
// Execute the exit handler in one of the worker threads
Workload workload = addContextAction(null, new ContextAction() {
public Object run(Context cx) {
app.executeExitHandler(cx);
app.clearPLS();
return null;
}
}, app + " exit handler", defaultTimeout);
try {
workload.getResult();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
ex.printStackTrace();
}
catch (Exception ex) {
ex.printStackTrace();
}
finally {
mxUnregister("Applications", app.getFilename());
getLogger().logp(Level.CONFIG, null, null, app + " unloaded.");
}
}
}
/** This class monitors all stylesheets that are being loaded and
* unload and handles MX registration.
*/
private class StylesheetCacheListener
implements LRUCache.LRUListener<String, Stylesheet> {
public void entryAdded(String key, Stylesheet xslt) {
mxRegister("Stylesheets", xslt.getFilename(), xslt.getJMXBean());
getLogger().logp(Level.CONFIG, null, null, xslt + " loaded.");
}
public void entryRemoved(String key, Stylesheet xslt) {
mxUnregister("Stylesheets", xslt.getFilename());
getLogger().logp(Level.CONFIG, null, null, xslt + " unloaded.");
}
}
/** This class monitors all schemas that are being loaded and
* unload and handles MX registration.
*/
private class SchemaCacheListener
implements LRUCache.LRUListener<String, Schema> {
public void entryAdded(String key, Schema sch) {
mxRegister("Schemas", sch.getFilename(), sch.getJMXBean());
getLogger().logp(Level.CONFIG, null, null, sch + " loaded.");
}
public void entryRemoved(String key, Schema sch) {
mxUnregister("Schemas", sch.getFilename());
getLogger().logp(Level.CONFIG, null, null, sch + " unloaded.");
}
}
/** This class checks if any of an Application's, Stylesheet's or Schema's
* files have been modified since it was loaded, and if so,
* unloads the cached object.
*/
private class CacheFilter
implements Runnable {
@Override public void run() {
getLogger().logp(Level.FINEST, null, null, "Checking for modified applications");
applicationCache.filterEntries(new LRUCache.EntryFilter<String, Application>() {
public boolean isStale(String key, Application app, long created) {
for (URI uri : app.getExternalURIs()) {
if (checkTag(uri)) {
getLogger().logp(Level.FINE, null, null, uri + " modification detected");
return true;
}
}
return false;
}
});
getLogger().logp(Level.FINEST, null, null, "Checking for modified stylesheets");
stylesheetCache.filterEntries(new LRUCache.EntryFilter<String, Stylesheet>() {
public boolean isStale(String key, Stylesheet xslt, long created) {
for (URI uri : xslt.getExternalURIs()) {
if (checkTag(uri)) {
getLogger().logp(Level.FINE, null, null, uri + " modification detected");
return true;
}
}
return false;
}
});
getLogger().logp(Level.FINEST, null, null, "Checking for modified schemas");
schemaCache.filterEntries(new LRUCache.EntryFilter<String, Schema>() {
public boolean isStale(String key, Schema sch, long created) {
for (URI uri : sch.getExternalURIs()) {
if (checkTag(uri)) {
getLogger().logp(Level.FINE, null, null, uri + " modification detected");
return true;
}
}
return false;
}
});
getLogger().logp(Level.FINEST, null, null, "Finished checking for modified resources");
}
private boolean checkTag(URI uri) {
URLConnection uc = null;
try {
uc = uri.toURL().openConnection();
uc.setDoInput(true);
uc.setDoOutput(false);
uc.setUseCaches(false);
uc.setConnectTimeout(3000);
uc.setReadTimeout(3000);
if (uc instanceof HttpURLConnection) {
HttpURLConnection huc = (HttpURLConnection) uc;
huc.setRequestMethod("HEAD");
uc.getInputStream();
}
else {
uc.connect();
}
String new_tag = uc.getHeaderField("ETag");
if (new_tag == null) {
new_tag = Long.toString(uc.getLastModified());
}
String old_tag = tagMap.get(uri);
if (old_tag == null) {
// Not previously present
tagMap.put(uri, new_tag);
old_tag = new_tag;
}
if (old_tag.equals(new_tag)) {
// No modification
return false;
}
else {
// File updated
tagMap.put(uri, new_tag);
return true;
}
}
catch (IOException ex) {
// Assume nothing has changed
return false;
}
finally {
if (uc != null) {
try { uc.getInputStream().close(); } catch (IOException ignored) {}
}
}
}
private Map<URI, String> tagMap = new HashMap<URI, String>();
}
/** Prepare a file path to be used as a root URI
*
* This function makes the path absolute and ensures that it
* ends with a slash.
*
* @param fs_root A native file system path
* @return An absolute file URI than ends with a slash
*/
public static URI createFSRootURI(String fs_root) {
URI fs_root_uri = new File(fs_root).getAbsoluteFile().toURI();
String fs_root_uri_str = fs_root_uri.toString();
if (!fs_root_uri_str.endsWith("/")) {
fs_root_uri = URI.create(fs_root_uri_str + "/");
}
return fs_root_uri;
}
/** This is ESXX ContextFactory, which makes sure all Rhino
* Context objects are set up with the desired properties and
* features.
*/
private static class ESXXContextFactory
extends ContextFactory {
public ESXXContextFactory() {
super();
addListener(new ContextFactory.Listener() {
@Override public void contextCreated(Context cx) {
// Enable all optimizations, but do count instructions
cx.setOptimizationLevel(9);
cx.setInstructionObserverThreshold((int) 100e6);
cx.setLanguageVersion(Context.VERSION_1_7);
// Provide a better mapping for primitive types on this context
WrapFactory wf = new WrapFactory() {
@Override public Object wrap(Context cx, Scriptable scope,
Object obj, Class<?> static_type) {
if (obj instanceof char[]) {
return new String((char[]) obj);
}
else {
return super.wrap(cx, scope, obj, static_type);
}
}
};
wf.setJavaPrimitiveWrap(false);
cx.setWrapFactory(wf);
}
@Override public void contextReleased(Context cx) {
// At this point, cx is un-associated, so we need to
// enter it again in order to call clearTLS(). And then
// we (naturally) have to exit it ... which will call
// contextReleased! Argh.
if (cx.getThreadLocal(ESXXContextFactory.class) != null) {
// Prevent infinite recursion
return;
}
cx.putThreadLocal(ESXXContextFactory.class, this);
enterContext(cx);
try {
Application.clearTLS(cx);
}
finally {
Context.exit();
}
cx.removeThreadLocal(ESXXContextFactory.class);
}
});
}
@Override public boolean hasFeature(Context cx, int feature) {
if (//feature == Context.FEATURE_DYNAMIC_SCOPE ||
feature == Context.FEATURE_LOCATION_INFORMATION_IN_ERROR ||
feature == Context.FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME ||
//feature == Context.FEATURE_WARNING_AS_ERROR ||
feature == Context.FEATURE_STRICT_MODE) {
return true;
}
else {
return super.hasFeature(cx, feature);
}
}
@Override public void observeInstructionCount(Context cx, int instruction_count) {
checkTimeout(cx);
}
}
private class WorkloadCancellator
implements Runnable {
@Override public void run() {
getLogger().logp(Level.FINEST, null, null, "Checking for workloads to cancel");
while (true) {
Workload w = workloadSet.peek();
if (w == null) {
break;
}
if (w.shouldCancel()) {
getLogger().logp(Level.FINE, null, null, "Cancelling workload " + w);
w.cancel();
workloadSet.poll();
}
else {
// No need to look futher, since the workloads are
// sorted by expiration time
break;
}
}
getLogger().logp(Level.FINEST, null, null, "Finished checking for workloads to cancel");
}
}
public static class Workload {
public Workload(String name, long exp) {
future = null;
this.name = name;
expires = exp;
}
public synchronized void addTempFile(File file) {
tempFiles.add(file);
}
public synchronized void open() {
thread = Thread.currentThread();
}
public synchronized void close() {
for (File temp : tempFiles) {
try { temp.delete(); } catch (Exception ignored) {}
}
tempFiles.clear();
thread = null;
}
public String getName() {
return name;
}
public long getExpires() {
return expires;
}
public synchronized Thread getThread() {
return thread;
}
public Object getResult()
throws InterruptedException, ExecutionException {
return future.get();
}
public boolean isDone() {
return future.isDone();
}
public boolean isTimedOut() {
return System.currentTimeMillis() > expires;
}
public boolean shouldCancel() {
// 10 seconds grace time. (Note that we cannot add to expires,
// since it may be set to Long.MAX_VALUE.)
return (System.currentTimeMillis() - 10000) > expires;
}
public void cancel() {
future.cancel(true);
}
@Override public String toString() {
return "[Workload: " + name + " (expires " + new java.util.Date(expires) + ")]";
}
@Override protected void finalize()
throws Throwable {
try {
close();
}
finally {
super.finalize();
}
}
private Future<Object> future;
private Thread thread;
private String name;
private long expires;
private Collection<File> tempFiles = new ArrayList<File>();
}
private class WorkloadJMXBean
extends javax.management.StandardEmitterMBean
implements org.esxx.jmx.WorkloadMXBean {
public WorkloadJMXBean() {
super(org.esxx.jmx.WorkloadMXBean.class, true,
new javax.management.NotificationBroadcasterSupport());
}
@Override public int getWorkloadCount() {
return ESXX.this.workloadSet.size();
}
@Override public List<WorkloadInfo> getWorkloads() {
ArrayList<Workload> workloads = new ArrayList<Workload>(workloadSet);
ArrayList<WorkloadInfo> infos = new ArrayList<WorkloadInfo>(workloads.size());
for (Workload w : workloads) {
synchronized (w) {
infos.add(new WorkloadInfo(w.getName(), new Date(w.getExpires()),
w.getThread().getName(),
w.isTimedOut(), w.isDone()));
}
}
return infos;
}
}
public interface ResponseHandler {
Integer handleResponse(Response result)
throws Exception;
Integer handleError(Throwable error);
}
public static final FileTypeMap fileTypeMap = new ESXXFileTypeMap();
private static class ESXXFileTypeMap
extends MimetypesFileTypeMap {
public ESXXFileTypeMap() {
super();
addIfMissing("css", "text/css");
addIfMissing("csv", "text/csv");
addIfMissing("eml", "message/rfc822");
addIfMissing("esxx", "application/vnd.esxx.webapp+xml");
addIfMissing("gif", "image/gif");
addIfMissing("html", "text/html");
addIfMissing("jpg", "image/jpeg");
addIfMissing("js", "text/javascript");
addIfMissing("json", "application/json");
addIfMissing("nrl", "application/x-nrl+xml");
addIfMissing("nvdl", "application/x-nvdl+xml");
addIfMissing("pdf", "application/pdf");
addIfMissing("png", "image/png");
addIfMissing("rnc", "application/relax-ng-compact-syntax");
addIfMissing("rng", "application/x-rng+xml");
addIfMissing("sch", "application/x-schematron+xml");
addIfMissing("txt", "text/plain");
addIfMissing("xhtml", "application/xhtml+xml");
addIfMissing("xml", "application/xml");
addIfMissing("xsd", "application/x-xsd+xml");
addIfMissing("xsl", "text/xsl");
addIfMissing("xslt", "text/xsl");
}
@Override public String getContentType(File file) {
if (file.isDirectory()) {
return "application/vnd.esxx.directory+xml";
}
else if (!file.isFile()) {
return "application/vnd.esxx.object";
}
else {
return super.getContentType(file);
}
}
private void addIfMissing(String ext, String type) {
if (getContentType("file." + ext).equals("application/octet-stream")) {
addMimeTypes(type + " " + ext + " " + ext.toUpperCase());
}
}
}
private Pattern noHandlerMode = Pattern.compile("");
private int defaultTimeout;
private boolean addShutdownHook;
private URI[] includePath;
private LRUCache<String, Application> applicationCache;
private LRUCache<String, Stylesheet> stylesheetCache;
private LRUCache<String, Schema> schemaCache;
private Parsers parsers;
private Properties settings;
private Object hostObject;
private HashMap<String,String> cgiToHTTPMap;
private DOMImplementation domImplementation;
private Processor saxonProcessor;
private DocumentBuilder saxonDocumentBuilder;
private ContextFactory contextFactory;
private ScheduledExecutorService executorService;
private PriorityBlockingQueue<Workload> workloadSet;
private WorkloadJMXBean workloadJMXBean;
private Logger logger;
private Thread shutdownHook;
}