001: /*
002: * $Id: JSONRPCCallingConvention.java,v 1.21 2007/09/18 08:45:03 agoubard Exp $
003: *
004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
005: * See the COPYRIGHT file for redistribution and use restrictions.
006: */
007: package org.xins.server;
008:
009: import java.io.IOException;
010: import java.io.PrintWriter;
011: import java.io.Reader;
012: import java.io.StringReader;
013: import java.util.Iterator;
014: import java.util.Map;
015: import java.util.Properties;
016:
017: import javax.servlet.http.HttpServletRequest;
018: import javax.servlet.http.HttpServletResponse;
019:
020: import org.json.JSONArray;
021: import org.json.JSONException;
022: import org.json.JSONObject;
023: import org.json.XML;
024:
025: import org.xins.common.MandatoryArgumentChecker;
026: import org.xins.common.Utils;
027: import org.xins.common.collections.BasicPropertyReader;
028: import org.xins.common.collections.PropertyReader;
029: import org.xins.common.collections.PropertyReaderConverter;
030: import org.xins.common.spec.APISpec;
031: import org.xins.common.spec.EntityNotFoundException;
032: import org.xins.common.spec.ErrorCodeSpec;
033: import org.xins.common.spec.FunctionSpec;
034: import org.xins.common.spec.InvalidSpecificationException;
035: import org.xins.common.spec.ParameterSpec;
036: import org.xins.common.text.ParseException;
037: import org.xins.common.types.Type;
038: import org.xins.common.xml.Element;
039: import org.xins.common.xml.ElementParser;
040: import org.xins.logdoc.ExceptionUtils;
041:
042: /**
043: * The JSON-RPC calling convention.
044: * Version <a href='http://json-rpc.org/wiki/specification'>1.0</a>
045: * and <a href='http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html'>1.1</a> are supported.
046: * The service description is also returned on request when calling the
047: * <em>system.describe</em> function.
048: * The returned object is a JSON Object with a similar structure as the input
049: * parameters when HTTP POST is used.
050: *
051: * @since XINS 2.0.
052: * @version $Revision: 1.21 $ $Date: 2007/09/18 08:45:03 $
053: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
054: */
055: public class JSONRPCCallingConvention extends CallingConvention {
056:
057: /**
058: * The content type of the HTTP response.
059: */
060: protected static final String RESPONSE_CONTENT_TYPE = "application/json";
061:
062: /**
063: * The API. Never <code>null</code>.
064: */
065: private final API _api;
066:
067: /**
068: * Creates a new <code>JSONRPCCallingConvention</code> instance.
069: *
070: * @param api
071: * the API, needed for the JSON-RPC messages, cannot be <code>null</code>.
072: *
073: * @throws IllegalArgumentException
074: * if <code>api == null</code>.
075: */
076: public JSONRPCCallingConvention(API api)
077: throws IllegalArgumentException {
078: MandatoryArgumentChecker.check("api", api);
079: _api = api;
080: }
081:
082: /**
083: * Returns the XML-RPC equivalent for the XINS type.
084: *
085: * @param parameterType
086: * the XINS type, cannot be <code>null</code>.
087: *
088: * @return
089: * the XML-RPC type, never <code>null</code>.
090: */
091: private static String convertType(Type parameterType) {
092: if (parameterType instanceof org.xins.common.types.standard.Boolean) {
093: return "bit";
094: } else if (parameterType instanceof org.xins.common.types.standard.Int8
095: || parameterType instanceof org.xins.common.types.standard.Int16
096: || parameterType instanceof org.xins.common.types.standard.Int32
097: || parameterType instanceof org.xins.common.types.standard.Int64
098: || parameterType instanceof org.xins.common.types.standard.Float32
099: || parameterType instanceof org.xins.common.types.standard.Float64) {
100: return "num";
101: } else {
102: return "str";
103: }
104: }
105:
106: protected String[] getSupportedMethods() {
107: return new String[] { "GET", "POST" };
108: }
109:
110: protected boolean matches(HttpServletRequest httpRequest)
111: throws Exception {
112:
113: // Note that matches will only accept calls that matches the 1.1 specification of JSON-RPC.
114: if (httpRequest.getHeader("User-Agent") == null) {
115: return false;
116: }
117: if (!"application/json".equals(httpRequest.getHeader("Accept"))) {
118: return false;
119: }
120: if ("post".equalsIgnoreCase(httpRequest.getMethod())) {
121: return "application/json".equals(httpRequest
122: .getContentType());
123: }
124: return true;
125: }
126:
127: protected FunctionRequest convertRequestImpl(
128: HttpServletRequest httpRequest)
129: throws InvalidRequestException,
130: FunctionNotSpecifiedException {
131:
132: if ("post".equalsIgnoreCase(httpRequest.getMethod())) {
133: return parsePostRequest(httpRequest);
134: } else if ("get".equalsIgnoreCase(httpRequest.getMethod())) {
135: return parseGetRequest(httpRequest);
136: } else {
137: throw new InvalidRequestException("Incorrect HTTP method: "
138: + httpRequest.getMethod());
139: }
140: }
141:
142: protected void convertResultImpl(FunctionResult xinsResult,
143: HttpServletResponse httpResponse,
144: HttpServletRequest httpRequest) throws IOException {
145:
146: // Send the XML output to the stream and flush
147: httpResponse.setContentType(RESPONSE_CONTENT_TYPE);
148: PrintWriter out = httpResponse.getWriter();
149: httpResponse.setStatus(HttpServletResponse.SC_OK);
150:
151: // Return the service description when asked.
152: String functionName = (String) httpRequest.getSession()
153: .getAttribute("functionName");
154: if ("system.describe".equals(functionName)) {
155: String uri = httpRequest.getRequestURI();
156: if (uri.indexOf("system.describe") != -1) {
157: uri = uri.substring(0, uri.indexOf("system.describe"));
158: }
159: try {
160: JSONObject serviceDescriptionObject = createServiceDescriptionObject(uri);
161: out.print(serviceDescriptionObject.toString());
162: out.close();
163: return;
164: } catch (JSONException jsonex) {
165: throw new IOException(jsonex.getMessage());
166: }
167: }
168:
169: // Transform the XINS result to a JSON object
170: JSONObject returnObject = new JSONObject();
171: try {
172: String version = (String) httpRequest.getSession()
173: .getAttribute("version");
174: if (version != null) {
175: returnObject.put("version", version);
176: }
177: if (xinsResult.getErrorCode() != null) {
178: if (version == null) {
179: returnObject.put("result", JSONObject.NULL);
180: returnObject
181: .put("error", xinsResult.getErrorCode());
182: } else {
183: JSONObject errorObject = new JSONObject();
184: String errorCode = xinsResult.getErrorCode();
185: errorObject.put("name", errorCode);
186: errorObject.put("code", new Integer(123));
187: errorObject.put("message", getErrorDescription(
188: functionName, errorCode));
189: JSONObject paramsObject = createResultObject(xinsResult);
190: errorObject.put("error", paramsObject);
191: returnObject.put("error", errorObject);
192: }
193: } else {
194: JSONObject paramsObject = createResultObject(xinsResult);
195: returnObject.put("result", paramsObject);
196: if (version == null) {
197: returnObject.put("error", JSONObject.NULL);
198: }
199: }
200: Object requestId = httpRequest.getSession().getAttribute(
201: "id");
202: if (requestId != null) {
203: returnObject.put("id", requestId);
204: }
205:
206: // Write the result to the servlet response
207: String returnString = returnObject.toString();
208: out.print(returnString);
209: } catch (JSONException jsonex) {
210: throw new IOException(jsonex.getMessage());
211: }
212:
213: out.close();
214: }
215:
216: /**
217: * Parses the JSON-RPC HTTP GET request according to the specs.
218: *
219: * @param httpRequest
220: * the HTTP request.
221: *
222: * @return
223: * the XINS request object, should not be <code>null</code>.
224: *
225: * @throws InvalidRequestException
226: * if the request is considerd to be invalid.
227: *
228: * @throws FunctionNotSpecifiedException
229: * if the request does not indicate the name of the function to execute.
230: */
231: private FunctionRequest parseGetRequest(
232: HttpServletRequest httpRequest)
233: throws InvalidRequestException,
234: FunctionNotSpecifiedException {
235: String functionName;
236: PropertyReader functionParams;
237: Element dataElement = null;
238:
239: String pathInfo = httpRequest.getPathInfo();
240: if (pathInfo.lastIndexOf("/") == pathInfo.length() - 1) {
241: throw new FunctionNotSpecifiedException();
242: } else {
243: functionName = pathInfo
244: .substring(pathInfo.lastIndexOf("/") + 1);
245: }
246: httpRequest.getSession(true).setAttribute("functionName",
247: functionName);
248: if (functionName.equals("system.describe")) {
249: return new FunctionRequest(functionName, null, null, true);
250: }
251: httpRequest.getSession().setAttribute("version", "1.1");
252: functionParams = gatherParams(httpRequest);
253:
254: // Get data section
255: String dataSectionValue = httpRequest.getParameter("_data");
256: if (dataSectionValue != null && dataSectionValue.length() > 0) {
257: ElementParser parser = new ElementParser();
258: try {
259: dataElement = parser.parse(new StringReader(
260: dataSectionValue));
261:
262: // I/O error, should never happen on a StringReader
263: } catch (IOException exception) {
264: throw Utils.logProgrammingError(exception);
265:
266: // Parsing error
267: } catch (ParseException exception) {
268: String detail = "Cannot parse the data section.";
269: throw new InvalidRequestException(detail, exception);
270: }
271: }
272: return new FunctionRequest(functionName, functionParams,
273: dataElement);
274: }
275:
276: /**
277: * Parses the JSON-RPC HTTP POST request according to the specs.
278: * http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html
279: *
280: * @param httpRequest
281: * the HTTP request.
282: *
283: * @return
284: * the XINS request object, should not be <code>null</code>.
285: *
286: * @throws InvalidRequestException
287: * if the request is considerd to be invalid.
288: *
289: * @throws FunctionNotSpecifiedException
290: * if the request does not indicate the name of the function to execute.
291: */
292: private FunctionRequest parsePostRequest(
293: HttpServletRequest httpRequest)
294: throws InvalidRequestException,
295: FunctionNotSpecifiedException {
296: String functionName;
297: BasicPropertyReader functionParams = new BasicPropertyReader();
298: Element dataElement = null;
299:
300: // Read the message
301: // TODO replace with IOReader.readFully()
302: StringBuffer requestBuffer = new StringBuffer(2048);
303: try {
304: Reader reader = httpRequest.getReader();
305: char[] buffer = new char[2048];
306: int length;
307: while ((length = reader.read(buffer)) != -1) {
308: requestBuffer.append(buffer, 0, length);
309: }
310: } catch (IOException ioe) {
311: throw new InvalidRequestException(
312: "I/O Error while reading the request: "
313: + ioe.getMessage());
314: }
315: String requestString = requestBuffer.toString();
316:
317: // Extract the request from the message
318: try {
319: JSONObject requestObject = new JSONObject(requestString);
320:
321: Object version = requestObject.opt("version");
322: if (version != null) {
323: httpRequest.getSession(true).setAttribute("version",
324: (String) version);
325: }
326:
327: functionName = requestObject.getString("method");
328: httpRequest.getSession(true).setAttribute("functionName",
329: functionName);
330: if (functionName.equals("system.describe")) {
331: return new FunctionRequest(functionName, null, null,
332: true);
333: }
334:
335: Object paramsParam = requestObject.get("params");
336: if (paramsParam instanceof JSONArray) {
337: JSONArray paramsArray = (JSONArray) paramsParam;
338: Iterator itInputParams = _api.getAPISpecification()
339: .getFunction(functionName).getInputParameters()
340: .keySet().iterator();
341: int paramPos = 0;
342: while (itInputParams.hasNext()
343: && paramPos < paramsArray.length()) {
344: String nextParamName = (String) itInputParams
345: .next();
346: Object nextParamValue = paramsArray.get(paramPos);
347: functionParams.set(nextParamName, String
348: .valueOf(nextParamValue));
349: paramPos++;
350: }
351: } else if (paramsParam instanceof JSONObject) {
352: JSONObject paramsObject = (JSONObject) paramsParam;
353: JSONArray paramNames = paramsObject.names();
354: for (int i = 0; i < paramNames.length(); i++) {
355: String nextName = paramNames.getString(i);
356: if (nextName.equals("_data")) {
357: JSONObject dataSectionObject = paramsObject
358: .getJSONObject("_data");
359: String dataSectionString = XML
360: .toString(dataSectionObject);
361: dataElement = new ElementParser()
362: .parse(dataSectionString);
363: } else {
364: String value = paramsObject.get(nextName)
365: .toString();
366: functionParams.set(nextName, value);
367: }
368: }
369: }
370: Object id = requestObject.opt("id");
371: if (id != null) {
372: httpRequest.getSession().setAttribute("id", id);
373: }
374: } catch (ParseException parseEx) {
375: throw new InvalidRequestException(parseEx.getMessage());
376: } catch (JSONException jsonex) {
377: throw new InvalidRequestException(jsonex.getMessage());
378: } catch (InvalidSpecificationException isex) {
379: RuntimeException exception = new RuntimeException();
380: ExceptionUtils.setCause(exception, isex);
381: throw exception;
382: } catch (EntityNotFoundException enfex) {
383: RuntimeException exception = new RuntimeException();
384: ExceptionUtils.setCause(exception, enfex);
385: throw exception;
386: }
387: return new FunctionRequest(functionName, functionParams,
388: dataElement);
389: }
390:
391: /**
392: * Creates the JSON object from the result returned by the function.
393: *
394: * @param xinsResult
395: * the result returned by the function, cannot be <code>null</code>.
396: *
397: * @return
398: * the JSON object created from the result of the function, never <code>null</code>.
399: *
400: * @throws JSONException
401: * if the object cannot be created for any reason.
402: */
403: static JSONObject createResultObject(FunctionResult xinsResult)
404: throws JSONException {
405: Properties params = PropertyReaderConverter
406: .toProperties(xinsResult.getParameters());
407: JSONObject paramsObject = new JSONObject(params);
408: if (xinsResult.getDataElement() != null) {
409: String dataSection = xinsResult.getDataElement().toString();
410: JSONObject dataSectionObject = XML
411: .toJSONObject(dataSection);
412: paramsObject.accumulate("data", dataSectionObject);
413: }
414: return paramsObject;
415: }
416:
417: /**
418: * Creates the JSON object containing the description of the API.
419: * Specifications are available at http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html
420: *
421: * @param address
422: * the URL address of the service, cannot be <code>null</code>.
423: *
424: * @return
425: * the JSON object containing the description of the API, or <code>null</code>
426: * if an error occured.
427: *
428: * @throws JSONException
429: * if the object cannot be created for any reason.
430: */
431: private JSONObject createServiceDescriptionObject(String address)
432: throws JSONException {
433: JSONObject serviceObject = new JSONObject();
434: serviceObject.put("sdversion", "1.0");
435: serviceObject.put("name", _api.getName());
436: String apiClassName = _api.getClass().getName();
437: serviceObject.put("id", "xins:"
438: + apiClassName.substring(0, apiClassName
439: .indexOf(".api.API")));
440: serviceObject.put("version", _api.getBootstrapProperties().get(
441: API.API_VERSION_PROPERTY));
442: try {
443: APISpec apiSpec = _api.getAPISpecification();
444: String description = apiSpec.getDescription();
445: serviceObject.put("summary", description);
446: serviceObject.put("address", address);
447:
448: // Add the functions
449: JSONArray procs = new JSONArray();
450: Iterator itFunctions = apiSpec.getFunctions().entrySet()
451: .iterator();
452: while (itFunctions.hasNext()) {
453: Map.Entry nextFunction = (Map.Entry) itFunctions.next();
454: JSONObject functionObject = new JSONObject();
455: functionObject.put("name", (String) nextFunction
456: .getKey());
457: FunctionSpec functionSpec = (FunctionSpec) nextFunction
458: .getValue();
459: functionObject.put("summary", functionSpec
460: .getDescription());
461: JSONArray params = getParamsDescription(functionSpec
462: .getInputParameters(), functionSpec
463: .getInputDataSectionElements());
464: functionObject.put("params", params);
465: JSONArray result = getParamsDescription(functionSpec
466: .getOutputParameters(), functionSpec
467: .getOutputDataSectionElements());
468: functionObject.put("return", result);
469: }
470: serviceObject.put("procs", procs);
471: } catch (InvalidSpecificationException ex) {
472: return serviceObject;
473: }
474: return serviceObject;
475: }
476:
477: /**
478: * Returns the description of the input or output parameters.
479: *
480: * @param paramsSpecs
481: * the specification of the input of output parameters, cannot be <code>null</code>.
482: *
483: * @param dataSectionSpecs
484: * the specification of the input of output data section, cannot be <code>null</code>.
485: *
486: * @return
487: * the JSON array containing the description of the input or output parameters, never <code>null</code>.
488: *
489: * @throws JSONException
490: * if the JSON object cannot be created.
491: */
492: static JSONArray getParamsDescription(Map paramsSpecs,
493: Map dataSectionSpecs) throws JSONException {
494: JSONArray params = new JSONArray();
495: Iterator itParams = paramsSpecs.entrySet().iterator();
496: while (itParams.hasNext()) {
497: Map.Entry nextParam = (Map.Entry) itParams.next();
498: JSONObject paramObject = new JSONObject();
499: paramObject.put("name", (String) nextParam.getKey());
500: String jsonType = convertType(((ParameterSpec) nextParam
501: .getValue()).getType());
502: paramObject.put("type", jsonType);
503: params.put(paramObject);
504: // TODO data section
505: }
506: return params;
507: }
508:
509: /**
510: * Gets a description of the error.
511: *
512: * @param functionName
513: * the name of the function called, cannot be <code>null</code>.
514: * @param errorCode
515: * the error code returned by the function, cannot be <code>null</code>.
516: *
517: * @return
518: * a single sentence containing the description of the error.
519: */
520: private String getErrorDescription(String functionName,
521: String errorCode) {
522: if (errorCode.equals("_InvalidRequest")) {
523: return "The request is invalid.";
524: } else if (errorCode.equals("_InvalidResponse")) {
525: return "The response is invalid.";
526: } else if (errorCode.equals("_DisabledFunction")) {
527: return "The \"" + functionName + "\" function is disabled.";
528: } else if (errorCode.equals("_InternalError")) {
529: return "There was an internal error.";
530: }
531: try {
532: ErrorCodeSpec errorSpec = _api.getAPISpecification()
533: .getFunction(functionName).getErrorCode(errorCode);
534: String errorDescription = errorSpec.getDescription();
535: if (errorDescription.indexOf(". ") != -1) {
536: errorDescription = errorDescription.substring(0,
537: errorDescription.indexOf(". "));
538: } else if (errorDescription.indexOf(".\n") != -1) {
539: errorDescription = errorDescription.substring(0,
540: errorDescription.indexOf(".\n"));
541: }
542: return errorDescription;
543: } catch (Exception ex) {
544: return "Unknown error: \"" + errorCode + "\".";
545: }
546: }
547: }
|