Java tutorial
/* * jabsorb - a Java to JavaScript Advanced Object Request Broker * http://www.jabsorb.org * * Copyright 2007-2008 The jabsorb team * * based on original code from * JSON-RPC-Java - a JSON-RPC to Java Bridge with dynamic invocation * * Copyright Metaparadigm Pte. Ltd. 2004. * Michael Clark <michael@metaparadigm.com> * * 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 org.jabsorb.ng; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.CharArrayWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletConfig; 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 org.jabsorb.ng.logging.ILogger; import org.jabsorb.ng.logging.LoggerFactory; import org.json.JSONException; import org.json.JSONObject; /** * <p> * This servlet handles JSON-RPC requests over HTTP and hands them to a * JSONRPCBridge instance (either a global instance or one in the user's * HttpSession). * </p> * <p> * The following can be added to your web.xml to export the servlet under the * URI "<code>/JSON-RPC</code>" * </p> * * <pre> * <servlet> * <servlet-name>org.jabsorb.JSONRPCServlet</servlet-name> * <servlet-class>org.jabsorb.JSONRPCServlet</servlet-class> * <!-- optional gzip threshold control --> * <init-param> * <param-name>gzip_threshold</param-name> * <param-value>200</param-value> * </init-param> * </servlet> * <servlet-mapping> * <servlet-name>org.jabsorb.JSONRPCServlet</servlet-name> * <url-pattern>/JSON-RPC</url-pattern> * </servlet-mapping> * </pre> * * </p> The JSONRPCServlet looks for a session specific bridge object under the * attribute <code>"JSONRPCBridge"</code> in the HttpSession associated with the * request (without creating a session if one does not already exist). If it * can't find a session specific bridge instance, it will default to invoking * against the global bridge. </p> * <p> * Using a session specific bridge allows you to export certain object instances * or classes only to specific users, and of course these instances could be * stateful and contain data specific to the user's session. * </p> * <p> * An example or creating a session specific bridge in JSP is as follows: * </p> * <code> * <jsp:useBean id="JSONRPCBridge" scope="session" * class="org.jabsorb.JSONRPCBridge"/> * </code> * <p> * An example in Java (i.e. in another Servlet): * </p> * <code> * HttpSession session = request.getSession();<br /> * JSONRPCBridge bridge = (JSONRPCBridge) session.getAttribute("JSONRPCBridge");<br> * if(bridge == null) {<br /> * bridge = new JSONRPCBridge();<br /> * session.setAttribute("JSONRPCBridge", bridge);<br /> * }<br /> * </code> */ public class JSONRPCServlet extends HttpServlet { /** * The size of the buffer used for reading requests */ private static final int buf_size = 4096; /** * The GZIP_THRESHOLD indicates the response size at which the servlet will * attempt to gzip the response if it can. Gzipping smaller responses is * counter productive for 2 reasons: * * 1. if the response is really small, the gzipped output can actually be * larger than the non-compressed original. because of the gzip header and * the general overhead of the gzipping. This is a lose-lose situation, so * the original should always be sent in this case. * * 2. gzipping imposes a small performance penality in that it takes a * little more time to gzip the content. There is also a corresponding * penality on the browser side when the content has to be uncompressed. * * This penalty is really small, and is normally more than outweighed by the * bandwidth savings provided by gzip (the response is typically 1/10th the * size when gzipped! Especially for json data which tends to have a lot of * repetition. * * So, the GZIP_THRESHOLD should be tuned to a size that is optimal for your * application. If your application is always served from a high speed * network, you might want to set this to a very high number-- (or even -1 * to turn it off) for slower networks where it's more important to conserve * bandwidth, set this to a lower number (but not too low!) * * Set this to zero if you want to always attempt to gzip the output when * the browser can accept gzip encoded responses. This is useful for * analyzing what a good gzip setting should be for potential responses from * your application. * * You can set this to -1 if you want to turn off gzip encoding for some * reason. */ private static int GZIP_THRESHOLD = 200; /** * The logger for this class */ private static final ILogger log = LoggerFactory.getLogger(JSONRPCServlet.class); /** * Unique serialisation id. */ private static final long serialVersionUID = 2; /** * Can browser accept gzip encoding? * * @param request * browser request object. * @return true if gzip encoding accepted. */ private boolean acceptsGzip(final HttpServletRequest request) { // can browser accept gzip encoding? String ae = request.getHeader("accept-encoding"); return ae != null && ae.indexOf("gzip") != -1; } /** * Find the JSONRPCBridge from the current session. If it can't be found in * the session, or there is no session, then return the global bridge. * * @param request * The message received * @return the JSONRPCBridge to use for this request */ protected JSONRPCBridge findBridge(final HttpServletRequest request) { // Find the JSONRPCBridge for this session or create one // if it doesn't exist HttpSession session = request.getSession(false); JSONRPCBridge json_bridge = null; if (session != null) { json_bridge = (JSONRPCBridge) session.getAttribute("JSONRPCBridge"); } if (json_bridge == null) { // Use the global bridge if we can't find a bridge in the session. json_bridge = JSONRPCBridge.getGlobalBridge(); if (log.isDebugEnabled()) { log.debug("findBridge", "Using global bridge."); } } return json_bridge; } /** * Gzip something. * * @param in * original content * @return size gzipped content */ private byte[] gzip(final byte[] in) { if (in != null && in.length > 0) { long tstart = System.currentTimeMillis(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); try { GZIPOutputStream gout = new GZIPOutputStream(bout); gout.write(in); gout.flush(); gout.close(); if (log.isDebugEnabled()) { log.debug("gzip", "gzipping took " + (System.currentTimeMillis() - tstart) + " msec"); } return bout.toByteArray(); } catch (IOException io) { log.error("gzip", "io exception gzipping byte array", io); } } return new byte[0]; } /** * Called by the container when the servlet is initialized. Check for * optional configuration parameters. * <p> * At this time, only gzip_threshold is looked for. * </p> * <p> * If it is found, and a valid Integer is specified, then that is used for * the GZIP_THRESHOLD. * </p> * <p> * If an invalid Integer is specified, then the GZIP_THRESHOLD is set to -1 * which disables GZIP compression. * </p> * <p> * The gzip_threshold indicates the response size at which the servlet will * attempt to gzip the response if it can. * </p> * <p> * Set this to -1 if you want to disable gzip compression for some reason, * or if you have another filter or other mechanism to handle gzipping for * you. * </p> * <p> * Set this to 0 to attempt to gzip all responses from this servlet. * otherwise, set it to the minimum response size at which gzip compression * is attempted. * </p> * <p> * <b>NOTE:</b> if the browser making the request does not accept gzip * compressed content, or the result of gzipping would cause the response * size to be larger (this could happen with very small responses) then the * content will be returned without gzipping, regardless. * </p> * <p> * of this setting, so it is very reasonable idea to set this to 0 for * maximum bandwidth savings, at the (very minor) expense of having the * server attempt to gzip all responses. * </p> * * @param config * ServletConfig from container. * @throws ServletException * if something goes wrong during initialization. */ @Override public void init(final ServletConfig config) throws ServletException { super.init(config); String gzipThresh = config.getInitParameter("gzip_threshold"); if (gzipThresh != null && gzipThresh.length() > 0) { try { JSONRPCServlet.GZIP_THRESHOLD = Integer.parseInt(gzipThresh); } catch (NumberFormatException n) { log.debug("init", "could not parse " + gzipThresh + " as an integer... defaulting to -1 (gzip compression off)"); JSONRPCServlet.GZIP_THRESHOLD = -1; } } log.debug("init", "GZIP_THRESHOLD is " + JSONRPCServlet.GZIP_THRESHOLD); if (JSONRPCServlet.GZIP_THRESHOLD == -1) { log.debug("init", "Gzipping is turned OFF. No attempts will be made to gzip content from this servlet."); } else if (JSONRPCServlet.GZIP_THRESHOLD == 0) { log.debug("init", "All responses will be Gzipped when gzipping results in a smaller response size."); } else { log.debug("init", "Responses over this size will be Gzipped when gzipping results in a smaller response size."); } } /** * Format (pretty print) json nicely for debugging output. If the pretty * printing fails for any reason (this is not expected) then the original, * unformatted json will be returned. * * @param unformattedJSON * a json string. * * @return a String containing the formatted json text for the passed in * json object. */ private String prettyPrintJson(final String unformattedJSON) { if (unformattedJSON == null || "".equals(unformattedJSON)) { return unformattedJSON; } try { // sort the keys in the output as well return new JSONObject(unformattedJSON).toString(2); } catch (JSONException je) { return unformattedJSON; // fall back to unformatted json, if pretty // print fails... } } /** * Called when a JSON-RPC requests comes in. Looks in the session for a * JSONRPCBridge and if not found there, uses the global bridge; then passes * off the JSON-PRC call to be handled by the JSONRPCBridge found. * * @param request * servlet request from browser. * @param response * servlet response to browser. * * @throws IOException * if an IOException occurs during processing. */ @Override public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException { // Use protected method in case someone wants to override it JSONRPCBridge json_bridge = findBridge(request); // Decode using the charset in the request if it exists otherwise // use UTF-8 as this is what all browser implementations use. // The JSON-RPC-Java JavaScript client is ASCII clean so it // although here we can correctly handle data from other clients // that do not escape non ASCII data String charset = request.getCharacterEncoding(); if (charset == null) { charset = "UTF-8"; } BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), charset)); String receiveString = (String) request.getAttribute("_jabsorb_beenHere"); // if JSON data is found in a special request attribute, it means // that a continuation was used and this request is being retried // as a consequence of a Jetty continuation // see http://blogs.webtide.com/gregw/2007/11/18/1195421880000.html if (receiveString == null) { // Read the request CharArrayWriter data = new CharArrayWriter(); char buf[] = new char[buf_size]; int ret; while ((ret = in.read(buf, 0, buf_size)) != -1) { data.write(buf, 0, ret); } receiveString = data.toString(); // save the json-rpc data in a special request attribute, in case a // jetty // continuation exception (org.mortbay.jetty.RetryRequest) is thrown // and this // request is retried by the container request.setAttribute("_jabsorb_beenHere", receiveString); } else { log.debug("service", "jetty continuation resumed..."); } if (log.isDebugEnabled()) { log.debug("service", "receive: " + receiveString); log.debug("service", "receive: " + prettyPrintJson(receiveString)); } // Process the request JSONObject json_req; JSONRPCResult json_res; try { json_req = new JSONObject(receiveString); json_res = json_bridge.call(new Object[] { request, response }, json_req); } catch (JSONException e) { log.error("service", "can't parse call" + receiveString, e); json_res = new JSONRPCResult(JSONRPCResult.CODE_ERR_PARSE, null, JSONRPCResult.MSG_ERR_PARSE); } String sendString = json_res.toString(); // dump the received string if (log.isDebugEnabled()) { log.debug("service", "send: " + sendString); log.debug("service", "send: " + prettyPrintJson(sendString)); } // Write the response byte[] bout = sendString.getBytes("UTF-8"); // handle gzipping of the response if it is turned on if (JSONRPCServlet.GZIP_THRESHOLD != -1) { // if the request header says that the browser can take gzip // compressed output, then gzip the output // but only if the response is large enough to warrant it and if the // resultant compressed output is // actually smaller. if (acceptsGzip(request)) { if (bout.length > JSONRPCServlet.GZIP_THRESHOLD) { byte[] gzippedOut = gzip(bout); log.debug("service", "gzipping! original size = " + bout.length + " gzipped size = " + gzippedOut.length); // if gzip didn't actually help, abort if (bout.length <= gzippedOut.length) { log.warning("service", "gzipping resulted in a larger output size! " + "aborting (sending non-gzipped response)... " + "you may want to increase the gzip threshold if this happens a lot!" + " original size = " + bout.length + " gzipped size = " + gzippedOut.length); } else { // go with the gzipped output bout = gzippedOut; response.addHeader("Content-Encoding", "gzip"); } } else { log.debug("service", "not gzipping because size is " + bout.length + " (less than the GZIP_THRESHOLD of " + JSONRPCServlet.GZIP_THRESHOLD + " bytes)"); } } else { // this should be rare with modern user agents log.debug("service", "not gzipping because user agent doesn't accept gzip encoding..."); } } // Encode using UTF-8, although We are actually ASCII clean as // all unicode data is JSON escaped using backslash u. This is // less data efficient for foreign character sets but it is // needed to support naughty browsers such as Konqueror and Safari // which do not honour the charset set in the response response.setContentType("application/json;charset=utf-8"); OutputStream out = response.getOutputStream(); response.setIntHeader("Content-Length", bout.length); out.write(bout); out.flush(); out.close(); } }