org.xins.server.XMLRPCCallingConvention Maven / Gradle / Ivy
/*
* $Id: XMLRPCCallingConvention.java,v 1.55 2012/03/15 21:07:39 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.server;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.w3c.dom.Attr;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.spec.DataSectionElementSpec;
import org.xins.common.spec.EntityNotFoundException;
import org.xins.common.spec.FunctionSpec;
import org.xins.common.spec.InvalidSpecificationException;
import org.xins.common.text.ParseException;
import org.xins.common.text.TextUtils;
import org.xins.common.types.Type;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xins.common.xml.DataElementBuilder;
import org.xins.common.xml.ElementList;
import org.znerd.xmlenc.XMLOutputter;
/**
* The XML-RPC calling convention.
*
* @version $Revision: 1.55 $ $Date: 2012/03/15 21:07:39 $
* @author Anthony Goubard
*/
public class XMLRPCCallingConvention extends CallingConvention {
/**
* The formatter for XINS Date type.
*/
private static final DateFormat XINS_DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
/**
* The formatter for XINS Timestamp type.
*/
private static final DateFormat XINS_TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyyMMddHHmmss");
/**
* The formatter for XML-RPC dateTime.iso8601 type.
*/
private static final DateFormat XML_RPC_TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
/**
* The key used to store the parsing fault in the request attributes.
*/
private static final String FAULT_KEY = "org.xins.server.xml-rpc.fault";
/**
* The key used to store the name of the function in the request attributes.
*/
private static final String FUNCTION_NAME = "org.xins.server.xml-rpc.function";
/**
* The response encoding format.
*/
protected static final String RESPONSE_ENCODING = "UTF-8";
/**
* The content type of the HTTP response.
*/
protected static final String RESPONSE_CONTENT_TYPE = "text/xml; charset=" + RESPONSE_ENCODING;
/**
* The API. Never null
.
*/
private final API _api;
/**
* Creates a new XMLRPCCallingConvention
instance.
*
* @param api
* the API, needed for the XML-RPC messages, cannot be
* null
.
*
* @throws IllegalArgumentException
* if api == null
.
*/
public XMLRPCCallingConvention(API api)
throws IllegalArgumentException {
// Check arguments
MandatoryArgumentChecker.check("api", api);
// Store the API reference (can be null!)
_api = api;
}
/**
* Returns the XML-RPC equivalent for the XINS type.
*
* @param parameterType
* the XINS type, cannot be null
.
*
* @return
* the XML-RPC type, never null
.
*/
private static String convertType(Type parameterType) {
if (parameterType instanceof org.xins.common.types.standard.Boolean) {
return "boolean";
} else if (parameterType instanceof org.xins.common.types.standard.Int8
|| parameterType instanceof org.xins.common.types.standard.Int16
|| parameterType instanceof org.xins.common.types.standard.Int32) {
return "int";
} else if (parameterType instanceof org.xins.common.types.standard.Float32
|| parameterType instanceof org.xins.common.types.standard.Float64) {
return "double";
} else if (parameterType instanceof org.xins.common.types.standard.Date
|| parameterType instanceof org.xins.common.types.standard.Timestamp) {
return "dateTime.iso8601";
} else if (parameterType instanceof org.xins.common.types.standard.Base64) {
return "base64";
} else {
return "string";
}
}
/**
* Attribute a number for the error code.
*
* @param errorCode
* the error code, cannot be null
.
*
* @return
* the error code number, always > 0;
*/
private static int getErrorCodeNumber(String errorCode) {
if (errorCode.equals("_DisabledFunction")) {
return 1;
} else if (errorCode.equals("_InternalError")) {
return 2;
} else if (errorCode.equals("_InvalidRequest")) {
return 3;
} else if (errorCode.equals("_InvalidResponse")) {
return 4;
} else {
// Defined error code returned. For more information, see the
// faultString element.
return 99;
}
}
protected String[] getSupportedMethods() {
return new String[] { "POST" };
}
/**
* Checks if the specified request can be handled by this calling
* convention.
*
* This method will not throw any exception.
*
* @param httpRequest
* the HTTP request to investigate, cannot be null
.
*
* @return
* true
if this calling convention is possibly
* able to handle this request, or false
if it
* definitely not able to handle this request.
*
* @throws Exception
* if analysis of the request causes an exception;
* false
will be assumed.
*/
protected boolean matches(HttpServletRequest httpRequest)
throws Exception {
// Parse the XML in the request (if any)
Element element = parseXMLRequest(httpRequest);
// The root element must be
if (element.getTagName().equals("methodCall")) {
// The text within the element is the function name
String function = new ElementList(element, "methodName").get(0).getTextContent();
// There is a match only if the function name is non-empty
if (! TextUtils.isEmpty(function)) {
return true;
}
}
return false;
}
protected FunctionRequest convertRequestImpl(HttpServletRequest httpRequest)
throws InvalidRequestException,
FunctionNotSpecifiedException {
Map backpack = new HashMap();
backpack.put(BackpackConstants.SKIP_FUNCTION_CALL, true);
Element xmlRequest = parseXMLRequest(httpRequest);
if (xmlRequest.getNamespaceURI() != null) {
backpack.put(FAULT_KEY, "Namespace not allowed in XML-RPC requests");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
if (!xmlRequest.getTagName().equals("methodCall")) {
String faultMessage = "Root element is not \"methodCall\" but \"" + xmlRequest.getTagName() + "\".";
backpack.put(FAULT_KEY, faultMessage);
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
Element methodNameElem;
try {
methodNameElem = new ElementList(xmlRequest, "methodName").getUniqueChildElement();
} catch (ParseException pex) {
backpack.put(FAULT_KEY, "No unique methodName found");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
if (methodNameElem.getNamespaceURI() != null) {
backpack.put(FAULT_KEY, "Namespace not allowed in XML-RPC requests");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
String functionName = methodNameElem.getTextContent();
backpack.put(BackpackConstants.FUNCTION_NAME, functionName);
// Determine function parameters and the data section
Map functionParams = new HashMap();
Element dataSection = null;
ElementList params = new ElementList(xmlRequest, "params");
if (params.isEmpty()) {
return new FunctionRequest(functionName, functionParams, null);
} else if (params.size() > 1) {
backpack.put(FAULT_KEY, "More than one params specified in the XML-RPC request.");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
Element paramsElem = (Element) params.get(0);
ElementList paramsNodes = new ElementList(paramsElem, "param");
for (int i = 0; i < paramsNodes.size(); i++) {
Element nextParam = (Element) paramsNodes.get(i);
Element structElem;
Element valueElem;
try {
valueElem = new ElementList(nextParam, "value").getUniqueChildElement();
structElem = new ElementList(valueElem).getUniqueChildElement();
} catch (ParseException pex) {
backpack.put(FAULT_KEY, "Invalid XML-RPC request.");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
if (structElem.getTagName().equals("struct")) {
// Parse the input parameter
String parameterName = null;
String parameterValue = null;
try {
Element memberElem = new ElementList(structElem, "member").getUniqueChildElement();
Element memberNameElem = new ElementList(memberElem, "name").getUniqueChildElement();
Element memberValueElem = new ElementList(memberElem, "value").getUniqueChildElement();
Element typeElem = new ElementList(memberValueElem).getUniqueChildElement();
parameterName = memberNameElem.getTextContent();
parameterValue = typeElem.getTextContent();
FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
Type parameterType = functionSpec.getInputParameter(parameterName).getType();
parameterValue = convertInput(parameterType, typeElem);
} catch (InvalidSpecificationException ise) {
// keep the old value
} catch (EntityNotFoundException enfe) {
// keep the old value
} catch (java.text.ParseException pex) {
backpack.put(FAULT_KEY, "Invalid value for parameter \"" + parameterName + "\".");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
} catch (ParseException pex) {
backpack.put(FAULT_KEY, "Invalid XML-RPC request: " + pex.getMessage());
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
functionParams.put(parameterName, parameterValue);
} else if (structElem.getTagName().equals("array")) {
// Parse the input data section
Element dataElem;
try {
Element arrayElem = new ElementList(valueElem, "array").getUniqueChildElement();
dataElem = new ElementList(arrayElem, "data").getUniqueChildElement();
} catch (ParseException pex) {
backpack.put(FAULT_KEY, "Incorrect specification of the input data section: " + pex.getMessage());
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
if (dataSection != null) {
backpack.put(FAULT_KEY, "Only one data section is allowed per request.");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
Map dataSectionSpec = null;
try {
FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
dataSectionSpec = functionSpec.getInputDataSectionElements();
} catch (InvalidSpecificationException ise) {
// keep the old value
} catch (EntityNotFoundException enfe) {
// keep the old value
}
DataElementBuilder builder = new DataElementBuilder();
for (Element childValueElem : new ElementList(dataElem, "value")) {
try {
Element childElem = parseElement(childValueElem, dataSectionSpec);
builder.getDataElement().appendChild(childElem);
} catch (ParseException pex) {
backpack.put(FAULT_KEY, "Incorrect format for data element in XML-RPC request: " + pex.getMessage());
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
}
dataSection = builder.getDataElement();
} else {
backpack.put(FAULT_KEY, "Only \"struct\" and \"array\" are valid as parameter type.");
return new FunctionRequest("InvalidRequest", new HashMap(), null, backpack);
}
}
backpack.put(BackpackConstants.SKIP_FUNCTION_CALL, false);
return new FunctionRequest(functionName, functionParams, null, backpack);
}
protected void convertResultImpl(FunctionResult xinsResult,
HttpServletResponse httpResponse,
Map backpack)
throws IOException {
// Send the XML output to the stream and flush
httpResponse.setContentType(RESPONSE_CONTENT_TYPE);
PrintWriter out = httpResponse.getWriter();
httpResponse.setStatus(HttpServletResponse.SC_OK);
// Store the result in a StringWriter before sending it.
Writer buffer = new StringWriter(1024);
// Create an XMLOutputter
XMLOutputter xmlout = new XMLOutputter(buffer, RESPONSE_ENCODING);
// Output the declaration
xmlout.declaration();
xmlout.startTag("methodResponse");
String errorCode = xinsResult.getErrorCode();
String faultRequest = (String) backpack.get(FAULT_KEY);
if (errorCode != null || faultRequest != null) {
xmlout.startTag("fault");
xmlout.startTag("value");
xmlout.startTag("struct");
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata("faultCode");
xmlout.endTag(); // name
xmlout.startTag("value");
xmlout.startTag("int");
if (errorCode != null) {
xmlout.pcdata(String.valueOf(getErrorCodeNumber(errorCode)));
} else {
xmlout.pcdata("10");
}
xmlout.endTag(); // int
xmlout.endTag(); // value
xmlout.endTag(); // member
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata("faultString");
xmlout.endTag(); // name
xmlout.startTag("value");
xmlout.startTag("string");
if (errorCode != null) {
xmlout.pcdata(errorCode);
} else {
xmlout.pcdata(faultRequest);
}
xmlout.endTag(); // string
xmlout.endTag(); // value
xmlout.endTag(); // member
xmlout.endTag(); // struct
xmlout.endTag(); // value
xmlout.endTag(); // fault
} else {
String functionName = (String) backpack.get(BackpackConstants.FUNCTION_NAME);
xmlout.startTag("params");
xmlout.startTag("param");
xmlout.startTag("value");
xmlout.startTag("struct");
// Write the output parameters
Map outputParameters = xinsResult.getParameters();
for (Map.Entry outputParameter : outputParameters.entrySet()) {
String parameterName = outputParameter.getKey();
String parameterValue = outputParameter.getValue();
String parameterTag = "string";
try {
FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
Type parameterType = functionSpec.getOutputParameter(parameterName).getType();
parameterValue = convertOutput(parameterType, parameterValue);
parameterTag = convertType(parameterType);
} catch (InvalidSpecificationException ise) {
// keep the old value
} catch (EntityNotFoundException enfe) {
// keep the old value
} catch (java.text.ParseException pex) {
throw new IOException("Invalid value for parameter \"" + parameterName + "\".");
}
// Write the member element
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata(parameterName);
xmlout.endTag();
xmlout.startTag("value");
xmlout.startTag(parameterTag);
xmlout.pcdata(parameterValue);
xmlout.endTag(); // type tag
xmlout.endTag(); // value
xmlout.endTag(); // member
}
// Write the data section if needed
Element dataSection = xinsResult.getDataElement();
if (dataSection != null) {
Map dataSectionSpec = null;
try {
FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
dataSectionSpec = functionSpec.getOutputDataSectionElements();
} catch (InvalidSpecificationException ise) {
// keep the old value
} catch (EntityNotFoundException enfe) {
// keep the old value
}
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata("data");
xmlout.endTag();
xmlout.startTag("value");
xmlout.startTag("array");
xmlout.startTag("data");
for (Element child : new ElementList(dataSection)) {
writeElement(child, xmlout, dataSectionSpec);
}
xmlout.endTag(); // data
xmlout.endTag(); // array
xmlout.endTag(); // value
xmlout.endTag(); // member
}
xmlout.endTag(); // struct
xmlout.endTag(); // value
xmlout.endTag(); // param
xmlout.endTag(); // params
}
xmlout.endTag(); // methodResponse
// Write the result to the servlet response
out.write(buffer.toString());
out.close();
}
/**
* Parses the data section element.
*
* @param valueElem
* the value element, cannot be null
.
*
* @param dataSection
* the specification of the elements, cannot be null
.
*
* @return
* the data section element, never null
.
*
* @throws ParseException
* if the XML request is incorrect.
*/
private Element parseElement(Element valueElem, Map dataSection) throws ParseException {
Element structElem = new ElementList(valueElem, "struct").getUniqueChildElement();
DataSectionElementSpec elementSpec;
Iterator itMemberElems = new ElementList(structElem, "member").iterator();
Element builder;
if (itMemberElems.hasNext()) {
Element memberElem = (Element) itMemberElems.next();
Element memberNameElem = new ElementList(memberElem, "name").getUniqueChildElement();
Element memberValueElem = new ElementList(memberElem, "value").getUniqueChildElement();
Element typeElem = new ElementList(memberValueElem).getUniqueChildElement();
String parameterName = memberNameElem.getTextContent();
elementSpec = (DataSectionElementSpec) dataSection.get(parameterName);
builder = valueElem.getOwnerDocument().createElement(parameterName);
if (typeElem.getTagName().equals("string")) {
builder.setTextContent(typeElem.getTextContent());
} else if (typeElem.getTagName().equals("array")) {
Map childrenSpec = elementSpec.getSubElements();
Element dataElem = new ElementList(typeElem, "data").getUniqueChildElement();
for (Element childValueElem : new ElementList(dataElem, "value")) {
Element childElem = parseElement(childValueElem, childrenSpec);
builder.appendChild(childElem);
}
} else {
throw new ParseException("Only \"string\" and \"array\" are valid as member value type.");
}
} else {
throw new ParseException("The \"struct\" element should at least have one member.");
}
// Fill in the attributes
while (itMemberElems.hasNext()) {
Element memberElem = (Element) itMemberElems.next();
Element memberNameElem = new ElementList(memberElem, "name").getUniqueChildElement();
Element memberValueElem = new ElementList(memberElem, "value").getUniqueChildElement();
Element typeElem = new ElementList(memberValueElem).getUniqueChildElement();
String parameterName = memberNameElem.getTextContent();
String parameterValue = typeElem.getTextContent();
try {
Type xinsElemType = elementSpec.getAttribute(parameterName).getType();
parameterValue = convertInput(xinsElemType, memberValueElem);
} catch (EntityNotFoundException enfe) {
// keep the old value
} catch (java.text.ParseException pex) {
throw new ParseException("Invalid value for parameter \"" + parameterName + "\".");
}
builder.setAttribute(parameterName, parameterValue);
}
return builder;
}
/**
* Write the given data section element to the output.
*
* @param dataElement
* the data section element, cannot be null
.
*
* @param xmlout
* the output where the data section element should be serialised, cannot be null
.
*
* @param dataSectionSpec
* the specification of the data element to be written, cannot be null
.
*
* @throws IOException
* if an IO error occurs while writing on the output.
*/
private void writeElement(Element dataElement, XMLOutputter xmlout, Map dataSectionSpec) throws IOException {
xmlout.startTag("value");
xmlout.startTag("struct");
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata(dataElement.getTagName());
xmlout.endTag(); // name
xmlout.startTag("value");
DataSectionElementSpec elementSpec = (DataSectionElementSpec) dataSectionSpec.get(dataElement.getTagName());
ElementList children = new ElementList(dataElement);;
if (!children.isEmpty()) {
Map childrenSpec = elementSpec.getSubElements();
xmlout.startTag("array");
xmlout.startTag("data");
for (Element nextChild : children) {
writeElement(nextChild, xmlout, childrenSpec);
}
xmlout.endTag(); // data
xmlout.endTag(); // array
} else {
xmlout.startTag("string");
if (dataElement.getTextContent() != null) {
xmlout.pcdata(dataElement.getTextContent());
}
xmlout.endTag(); // string
}
xmlout.endTag(); // value
xmlout.endTag(); // member
// Write the attributes
NamedNodeMap attributesMap = dataElement.getAttributes();
for (int i = 0; i < attributesMap.getLength(); i++) {
Attr attribute = (Attr) attributesMap.item(i);
String attributeName = attribute.getName();
String attributeValue = attribute.getValue();
String attributeTag;
try {
Type attributeType = elementSpec.getAttribute(attributeName).getType();
attributeValue = convertOutput(attributeType, attributeValue);
attributeTag = convertType(attributeType);
} catch (EntityNotFoundException enfe) {
attributeTag = "string";
} catch (java.text.ParseException pex) {
throw new IOException("Invalid value for parameter \"" + attributeName + "\".");
}
xmlout.startTag("member");
xmlout.startTag("name");
xmlout.pcdata(attributeName);
xmlout.endTag(); // name
xmlout.startTag("value");
xmlout.startTag(attributeTag);
xmlout.pcdata(attributeValue);
xmlout.endTag(); // tag
xmlout.endTag(); // value
xmlout.endTag(); // member
}
xmlout.endTag(); // struct
xmlout.endTag(); // value
}
/**
* Converts the XML-RPC input values to XINS input values.
*
* @param parameterType
* the type of the XINS parameter, cannot be null
.
*
* @param typeElem
* the content of the XML-RPC value, cannot be null
.
*
* @return
* the XINS value, never null
.
*
* @throws java.text.ParseException
* if the parameterValue is incorrect for the type.
*/
private String convertInput(Type parameterType, Element typeElem) throws java.text.ParseException {
String xmlRpcType = typeElem.getTagName();
String parameterValue = typeElem.getTextContent();
if (parameterType instanceof org.xins.common.types.standard.Boolean) {
if (parameterValue.equals("1")) {
return "true";
} else if (parameterValue.equals("0")) {
return "false";
} else {
throw new java.text.ParseException("Incorrect value for boolean: " + parameterValue, 0);
}
}
//System.err.println("type: " + xmlRpcType + " ; value: " + parameterValue);
if (xmlRpcType.equals("dateTime.iso8601")) {
Date date = XML_RPC_TIMESTAMP_FORMATTER.parse(parameterValue);
if (parameterType instanceof org.xins.common.types.standard.Date) {
synchronized (XINS_DATE_FORMATTER) {
return XINS_DATE_FORMATTER.format(date);
}
} else if (parameterType instanceof org.xins.common.types.standard.Timestamp) {
synchronized (XINS_TIMESTAMP_FORMATTER) {
return XINS_TIMESTAMP_FORMATTER.format(date);
}
}
}
return parameterValue;
}
/**
* Converts the XINS output values to XML-RPC output values.
*
* @param parameterType
* the type of the XINS parameter, cannot be null
.
*
* @param parameterValue
* the XINS parameter value to convert, cannot be null
.
*
* @return
* the XML-RPC value, never null
.
*
* @throws java.text.ParseException
* if the parameterValue is incorrect for the type.
*/
private String convertOutput(Type parameterType, String parameterValue) throws java.text.ParseException {
if (parameterType instanceof org.xins.common.types.standard.Boolean) {
if (parameterValue.equals("true")) {
return "1";
} else if (parameterValue.equals("false")) {
return "0";
} else {
throw new java.text.ParseException("Incorrect value for boolean: " + parameterValue, 0);
}
} else if (parameterType instanceof org.xins.common.types.standard.Date) {
Date date = null;
synchronized (XINS_DATE_FORMATTER) {
date = XINS_DATE_FORMATTER.parse(parameterValue);
}
synchronized (XML_RPC_TIMESTAMP_FORMATTER) {
return XML_RPC_TIMESTAMP_FORMATTER.format(date);
}
} else if (parameterType instanceof org.xins.common.types.standard.Timestamp) {
Date date = null;
synchronized (XINS_TIMESTAMP_FORMATTER) {
date = XINS_TIMESTAMP_FORMATTER.parse(parameterValue);
}
synchronized (XML_RPC_TIMESTAMP_FORMATTER) {
return XML_RPC_TIMESTAMP_FORMATTER.format(date);
}
}
return parameterValue;
}
}