package org.jboss.seam.remoting; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.jboss.solder.logging.Logger; import org.jboss.seam.remoting.model.ModelHandler; import org.jboss.seam.remoting.validation.ConstraintTranslator; /** * Serves JavaScript implementation of Seam Remoting * * @author Shane Bryzak */ public class Remoting extends HttpServlet { private static final long serialVersionUID = -3911197516105313424L; private static final String REQUEST_PATH_EXECUTE = "/execute"; // private static final String REQUEST_PATH_SUBSCRIPTION = "/subscription"; // private static final String REQUEST_PATH_POLL = "/poll"; private static final String REQUEST_PATH_INTERFACE = "/interface.js"; private static final String REQUEST_PATH_MODEL = "/model"; private static final String REQUEST_PATH_VALIDATION = "/validate"; @Inject Instance<ExecutionHandler> executionHandlerInstance; @Inject Instance<InterfaceGenerator> interfaceHandlerInstance; @Inject Instance<ModelHandler> modelHandlerInstance; @Inject Instance<ConstraintTranslator> translatorInstance; public static final int DEFAULT_POLL_TIMEOUT = 10; // 10 seconds public static final int DEFAULT_POLL_INTERVAL = 1; // 1 second private ServletConfig servletConfig; private int pollTimeout = DEFAULT_POLL_TIMEOUT; private int pollInterval = DEFAULT_POLL_INTERVAL; private boolean debug = false; /** * We use a Map for this because a Servlet can serve requests for more than * one context path. */ private Map<String, byte[]> cachedConfig = new HashMap<String, byte[]>(); private Map<String, byte[]> resourceCache = new HashMap<String, byte[]>(); private static final Logger log = Logger.getLogger(Remoting.class); private static final Pattern pathPattern = Pattern.compile("/(.*?)/([^/]+)"); private static final String REMOTING_RESOURCE_PATH = "resource"; private synchronized void initConfig(String contextPath, HttpServletRequest request) { if (!cachedConfig.containsKey(contextPath)) { StringBuilder sb = new StringBuilder(); sb.append("\nSeam.resourcePath = \""); sb.append(contextPath); sb.append(request.getServletPath()); sb.append(servletConfig.getServletContext().getContextPath()); sb.append("\";"); sb.append("\nSeam.debug = "); sb.append(getDebug() ? "true" : "false"); sb.append(";"); /* * sb.append("\nSeam.pollInterval = "); sb.append(getPollInterval()); * sb.append(";"); sb.append("\nSeam.pollTimeout = "); * sb.append(getPollTimeout()); sb.append(";"); */ cachedConfig.put(contextPath, sb.toString().getBytes()); } } /** * Appends various configuration options to the remoting javascript client * api. * * @param out OutputStream */ private void appendConfig(OutputStream out, String contextPath, HttpServletRequest request) throws IOException { if (!cachedConfig.containsKey(contextPath)) { initConfig(contextPath, request); } out.write(cachedConfig.get(contextPath)); } /** * @param resourceName String The name of the resource to serve * @param out OutputStream The OutputStream to write the resource to */ private void writeResource(String resourceName, HttpServletResponse response, boolean compress) throws IOException { String cacheKey = resourceName + ":" + Boolean.toString(compress); if (!resourceCache.containsKey(cacheKey)) { synchronized (resourceCache) { if (!resourceCache.containsKey(cacheKey)) { ByteArrayOutputStream out = new ByteArrayOutputStream(); // Only allow resource requests for .js files if (resourceName.endsWith(".js")) { InputStream in = this.getClass().getClassLoader().getResourceAsStream("org/jboss/seam/remoting/" + resourceName); try { if (in != null) { response.setContentType("text/javascript"); byte[] buffer = new byte[1024]; int read = in.read(buffer); while (read != -1) { out.write(buffer, 0, read); read = in.read(buffer); } resourceCache.put(cacheKey, compress ? compressResource(out.toByteArray()) : out.toByteArray()); response.getOutputStream().write(resourceCache.get(cacheKey)); } else { log.error(String.format("Resource [%s] not found.", resourceName)); } } finally { if (in != null) in.close(); } } } } } else { response.getOutputStream().write(resourceCache.get(cacheKey)); } } /** * Compresses JavaScript resources by removing comments, cr/lf, leading and * trailing white space. * * @param resourceData The resource data to compress. * @return */ private byte[] compressResource(byte[] resourceData) { String resource = new String(resourceData); // Remove comments resource = resource.replaceAll("/{2,}[^\\n\\r]*[\\n\\r]", ""); resource = resource.replaceAll("/\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*/", ""); // Remove leading and trailing space and CR/LF's for lines with a // statement terminator resource = resource.replaceAll(";\\s*[\\n\\r]+\\s*", ";"); // Remove leading and trailing space and CR/LF's for lines with a block // terminator resource = resource.replaceAll("}\\s*[\\n\\r]+\\s*", "}"); // Replace any remaining leading/trailing space and CR/LF with a single // space resource = resource.replaceAll("\\s*[\\n\\r]+\\s*", " "); return resource.getBytes(); } public int getPollTimeout() { return pollTimeout; } public void setPollTimeout(int pollTimeout) { this.pollTimeout = pollTimeout; } public int getPollInterval() { return pollInterval; } public void setPollInterval(int pollInterval) { this.pollInterval = pollInterval; } public boolean getDebug() { return debug; } public void setDebug(boolean debug) { this.debug = debug; } public void destroy() { } public ServletConfig getServletConfig() { return servletConfig; } public String getServletInfo() { return null; } public void init(ServletConfig config) throws ServletException { this.servletConfig = config; } protected ExecutionHandler getExecutionHandler() { return executionHandlerInstance.get(); } protected InterfaceGenerator getInterfaceHandler() { return interfaceHandlerInstance.get(); } protected ModelHandler getModelHandler() { return modelHandlerInstance.get(); } protected ConstraintTranslator getTranslatorHandler() { return translatorInstance.get(); } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { String pathInfo = request.getPathInfo(); // Nothing to do if (pathInfo == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "No path information provided"); return; } if (pathInfo.startsWith(servletConfig.getServletContext().getContextPath())) { pathInfo = pathInfo.substring(servletConfig.getServletContext().getContextPath().length()); } if (REQUEST_PATH_EXECUTE.equals(pathInfo)) { getExecutionHandler().handle(request, response); } else if (REQUEST_PATH_INTERFACE.equals(pathInfo)) { getInterfaceHandler().handle(request, response); } else if (REQUEST_PATH_MODEL.equals(pathInfo)) { getModelHandler().handle(request, response); } else if (REQUEST_PATH_VALIDATION.equals(pathInfo)) { getTranslatorHandler().handle(request, response); } else { Matcher m = pathPattern.matcher(pathInfo); if (m.matches()) { String path = m.group(1); String resource = m.group(2); if (REMOTING_RESOURCE_PATH.equals(path)) { String compressParam = request.getParameter("compress"); boolean compress = !(compressParam != null && "false".equals(compressParam)); writeResource(resource, response, compress); if ("remote.js".equals(resource)) { appendConfig(response.getOutputStream(), request.getContextPath(), request); } } response.getOutputStream().flush(); } } } catch (Exception ex) { log.error("Error", ex); } } }