package com.dbxml.labrador.xmlrpc;
/*
* The dbXML Labrador Software License, Version 1.0
*
*
* Copyright (c) 2003 The dbXML Group, L.L.C. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by The
* dbXML Group, L.L.C. (http://www.dbxml.com/)."
* Alternately, this acknowledgment may appear in the software
* itself, if and wherever such third-party acknowledgments normally
* appear.
*
* 4. The names "Labrador" and "dbXML Group" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For written permission, please contact
* info@dbxml.com
*
* 5. Products derived from this software may not be called "Labrador",
* nor may "Labrador" appear in their name, without prior written
* permission of The dbXML Group, L.L.C..
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE DBXML GROUP, L.L.C. OR ITS
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* $Id: XMLRPCHandler.java,v 1.23 2004/07/28 17:08:42 bradford Exp $
*/
import com.dbxml.labrador.*;
import java.io.*;
import java.util.*;
import com.dbxml.labrador.exceptions.RequestException;
import com.dbxml.labrador.http.HTTP;
import com.dbxml.labrador.types.ArrayConversions;
import com.dbxml.labrador.types.Types;
import com.dbxml.labrador.types.Variant;
import com.dbxml.labrador.util.Base64;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
/**
* XML-RPC Handler.
* <br /><br />
* @todo *Nothing* here has been optimized.
*
* There's a bunch of automatic conversion going on that can be
* unloaded into type-specific routines for performance. Also,
* most of the conversion routines should probably be moved into
* a class in the util package.
*/
public final class XMLRPCHandler implements Handler {
private static final String PROTOCOL = "XML-RPC";
private static final String SERVER_ERROR = "500";
public static final String PREFIX = "/xmlrpc/";
private static final SAXParserFactory saxFactory = SAXParserFactory.newInstance();
private static final SimpleDateFormat IDF = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss", new DateFormatSymbols(Locale.US));
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
private static final String METHOD_CALL = "methodCall";
private static final String METHOD_NAME = "methodName";
private static final String MEMBER = "member";
private static final String NAME = "name";
private static final String VALUE = "value";
private static final String PARAMS = "params";
private static final String PARAM = "param";
private static final String DATA = "data";
private static final String NIL = "nil";
private static final String I4 = "i4";
private static final String INT = "int";
private static final String BOOLEAN = "boolean";
private static final String STRING = "string";
private static final String DOUBLE = "double";
private static final String DATETIME = "dateTime.iso8601";
private static final String BASE64 = "base64";
private static final String STRUCT = "struct";
private static final String ARRAY = "array";
private static final String METHOD_RESPONSE = "methodResponse";
private static final String FAULT = "fault";
private static final String FAULT_CODE = "faultCode";
private static final String FAULT_STRING = "faultString";
private static final int STATE_INVALID = 0;
private static final int STATE_FAULT = 1;
private static final int STATE_START = 2;
private static final int STATE_METHOD_CALL = 3;
private static final int STATE_METHOD_NAME = 4;
private static final int STATE_MEMBER = 5;
private static final int STATE_NAME = 6;
private static final int STATE_VALUE = 7;
private static final int STATE_PARAMS = 8;
private static final int STATE_PARAM = 9;
private static final int STATE_DATA = 10;
private static final int STATE_I4 = 11;
private static final int STATE_INT = 12;
private static final int STATE_BOOLEAN = 13;
private static final int STATE_STRING = 14;
private static final int STATE_DOUBLE = 15;
private static final int STATE_DATETIME = 16;
private static final int STATE_BASE64 = 17;
private static final int STATE_STRUCT = 18;
private static final int STATE_ARRAY = 19;
private static final int STATE_NIL = 20;
private static final int STATE_PCDATA = 21;
private static final State[] states = {
new State(STATE_INVALID),
new State(STATE_FAULT),
new State(STATE_START, new int[]{STATE_METHOD_CALL}),
new State(STATE_METHOD_CALL, METHOD_CALL,
new int[]{STATE_METHOD_NAME, STATE_PARAMS}),
new State(STATE_METHOD_NAME, METHOD_NAME, new int[]{STATE_PCDATA}),
new State(STATE_MEMBER, MEMBER,
new int[]{STATE_NAME, STATE_VALUE}),
new State(STATE_NAME, NAME, new int[]{STATE_PCDATA}),
new State(STATE_VALUE, VALUE,
new int[]{STATE_I4, STATE_INT, STATE_BOOLEAN, STATE_STRING,
STATE_DOUBLE, STATE_DATETIME, STATE_BASE64,
STATE_STRUCT, STATE_ARRAY, STATE_PCDATA, STATE_NIL}),
new State(STATE_PARAMS, PARAMS, new int[]{STATE_PARAM}),
new State(STATE_PARAM, PARAM, new int[]{STATE_VALUE}),
new State(STATE_DATA, DATA, new int[]{STATE_VALUE}),
new State(STATE_I4, I4, new int[]{STATE_PCDATA}),
new State(STATE_INT, INT, new int[]{STATE_PCDATA}),
new State(STATE_BOOLEAN, BOOLEAN, new int[]{STATE_PCDATA}),
new State(STATE_STRING, STRING, new int[]{STATE_PCDATA}),
new State(STATE_DOUBLE, DOUBLE, new int[]{STATE_PCDATA}),
new State(STATE_DATETIME, DATETIME, new int[]{STATE_PCDATA}),
new State(STATE_BASE64, BASE64, new int[]{STATE_PCDATA}),
new State(STATE_STRUCT, STRUCT, new int[]{STATE_MEMBER}),
new State(STATE_ARRAY, ARRAY, new int[]{STATE_DATA}),
new State(STATE_NIL, NIL),
new State(STATE_PCDATA)
};
public XMLRPCHandler() {
}
public String getProtocol() {
return PROTOCOL;
}
public boolean isRequestValid(Request request) {
return request.hasContent() && request.getPath().startsWith(PREFIX);
}
public ID getInstanceID(Request request) {
return new ID(request.getPath().substring(PREFIX.length() - 1));
}
public void processRequest(Request request, Response response, Instance instance) throws RequestException {
try {
InputStreamReader isr = new InputStreamReader(request.getInputStream(), "UTF8");
BufferedReader br = new BufferedReader(isr, 4096);
InputSource is = new InputSource(br);
DefaultHandler handler = new SAXHandler(request, response, instance);
SAXParser sp = saxFactory.newSAXParser();
sp.parse(is, handler);
}
catch ( Exception e ) {
processError(response, SERVER_ERROR, e.getMessage());
}
}
public void processError(Response response, String code, String message) {
try {
response.setHeader(HTTP.HEADER_CONTENT_TYPE, Headers.TYPE_TEXT_XML);
OutputStream os = response.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF8");
BufferedWriter bw = new BufferedWriter(osw, 4096);
PrintWriter pw = new PrintWriter(bw);
pw.println(XML_HEADER);
pw.println("<"+METHOD_RESPONSE+"><"+FAULT+">");
Map m = new HashMap();
m.put(FAULT_CODE, code);
m.put(FAULT_STRING, message);
writeVariant(pw, new Variant(m));
pw.println("</"+FAULT+"></"+METHOD_RESPONSE+">");
pw.flush(); // Is this needed?
pw.close(); // Is this needed?
response.close();
}
catch ( Exception e ) {
/** @todo What do we do here? */
}
}
protected DefaultHandler createSAXHandler(Request request, Response response, Instance instance) {
return new SAXHandler(request, response, instance);
}
private void writeEscapedString(PrintWriter pw, String s) throws IOException {
// Only Ampersands (&) and Less-Thans (<) really need to be
// encoded in order to fool an XML parser.
char[] c = s.toCharArray();
int start = 0;
int pos = 0;
for ( ; pos < c.length; pos++ ) {
switch ( c[pos] ) {
case '<':
if ( start < pos )
pw.write(c, start, pos - start);
pw.write("<");
start = pos + 1;
break;
case '>':
if ( pos > 1 && c[pos-1] == ']' && c[pos-2] == ']' ) {
if ( start < pos )
pw.write(c, start, pos - start);
pw.write(">");
start = pos + 1;
}
break;
case '&':
if ( start < pos )
pw.write(c, start, pos - start);
pw.write("&");
start = pos + 1;
break;
}
}
if ( start < pos )
pw.write(c, start, pos - start);
}
private void writeValue(PrintWriter pw, Object o) throws IOException {
if ( o != null && o.getClass().isArray() )
writeArray(pw, o);
else
writeVariant(pw, new Variant(o));
}
private void writeArray(PrintWriter pw, Object o) throws IOException {
if ( Types.typeOf(o) == Types.BYTE ) {
// Byte Arrays are converted to Base64
try {
byte[] b = (byte[])o;
ByteArrayInputStream bis = new ByteArrayInputStream(b);
ByteArrayOutputStream bos = new ByteArrayOutputStream((b.length * 4) / 3);
Base64.encode(bis, bos);
pw.println("<"+VALUE+"><"+BASE64+">");
pw.println(new String(bos.toByteArray()));
pw.println("</"+BASE64+"></"+VALUE+">");
}
catch ( Exception e ) {
/** @todo This */
e.printStackTrace(System.err);
}
}
else {
List l = ArrayConversions.toList(o);
writeVariant(pw, new Variant(l));
}
}
private void writeVariant(PrintWriter pw, Variant v) throws IOException {
if ( v.isNull() ) {
pw.println("<"+VALUE+"><"+NIL+"/></"+VALUE+">");
return;
}
pw.println("<"+VALUE+">");
switch ( v.getType() ) {
case Types.VOID:
case Types.EMPTY:
pw.println("<"+NIL+"/>");
break;
case Types.BOOLEAN:
if ( v.getBoolean() )
pw.println("<"+BOOLEAN+">1</"+BOOLEAN+">");
else
pw.println("<"+BOOLEAN+">0</"+BOOLEAN+">");
break;
case Types.BYTE:
case Types.SHORT:
case Types.INT:
pw.println("<"+I4+">" + v.toString() + "</"+I4+">");
break;
case Types.LONG:
pw.println("<"+INT+">" + v.toString() + "</"+INT+">");
break;
case Types.DOUBLE:
case Types.FLOAT:
pw.println("<"+DOUBLE+">" + v.toString() + "</"+DOUBLE+">");
break;
case Types.STRING:
case Types.CHAR:
pw.print("<"+STRING+">");
writeEscapedString(pw, v.toString());
pw.println("</"+STRING+">");
break;
case Types.NODE:
pw.println("<"+STRING+">");
// If XML-RPC actually supported an XML type, we could
// serialize the Document directly to the stream. Instead,
// we have to use a home grown serializer.
try {
XMLDocWriter.write((Node)v.getObject(), pw);
}
catch ( Exception e ) {
/** @todo This */
e.printStackTrace(System.err);
}
pw.println();
pw.println("</"+STRING+">");
break;
case Types.LIST:
pw.println("<"+ARRAY+"><"+DATA+">");
Iterator l = v.getList().iterator();
while ( l.hasNext() )
writeVariant(pw, new Variant(l.next()));
pw.println("</"+DATA+"></"+ARRAY+">");
break;
case Types.MAP:
pw.println("<"+STRUCT+">");
Map m = v.getMap();
Iterator k = m.keySet().iterator();
while ( k.hasNext() ) {
pw.println("<"+MEMBER+">");
Object key = k.next();
pw.print("<"+NAME+">");
writeEscapedString(pw, key.toString());
pw.println("</"+NAME+">");
writeValue(pw, m.get(key));
pw.println("</"+MEMBER+">");
}
pw.println("</"+STRUCT+">");
break;
case Types.DATE:
pw.print("<"+DATETIME+">");
pw.print(IDF.format(v.getDate()));
pw.println("</"+DATETIME+">");
break;
case Types.OBJECT:
writeValue(pw, v.getObject());
break;
}
pw.println("</"+VALUE+">");
}
/**
* SAXHandler
*/
private class SAXHandler extends DefaultHandler {
public Request request;
public Response response;
public Instance instance;
public String methodName;
public List params;
public Stack stack = new Stack();
public StackInfo info = new StackInfo(STATE_START);
public SAXHandler(Request request, Response response, Instance instance) {
this.request = request;
this.response = response;
this.instance = instance;
}
public void startElement(String namespaceURI, String localName, String qName, Attributes atts) {
int newState = STATE_INVALID;
State s = states[info.state];
if ( s.states != null )
for ( int i = 0; i < s.states.length; i++ )
if ( qName.equals(states[s.states[i]].element) )
newState = s.states[i];
switch ( newState ) {
case STATE_METHOD_CALL:
{
methodName = null;
params = null;
stack.push(info);
info = new StackInfo(newState);
break;
}
case STATE_STRUCT:
{
stack.push(info);
info = new StackInfo(newState, new HashMap());
break;
}
case STATE_MEMBER:
{
stack.push(info);
info = new StackInfo(newState, new NameValue());
break;
}
case STATE_ARRAY:
{
stack.push(info);
info = new StackInfo(newState, new ArrayList());
break;
}
case STATE_INVALID:
{
StringBuffer sb = new StringBuffer("Invalid Parse State: " + qName);
if ( s.element != null )
sb.append(" in " + s.element);
processError(response, SERVER_ERROR, sb.toString());
break;
}
case STATE_FAULT:
{
break;
}
default:
{
stack.push(info);
info = new StackInfo(newState);
break;
}
}
}
public void endElement(String namespaceURI, String localName, String qName) {
Object obj;
// If we've just been parsing a String, pop the state
boolean wasString = info.state == STATE_PCDATA;
if ( wasString ) {
obj = info.sb.toString();
info = (StackInfo)stack.pop();
}
else
obj = info.obj;
switch ( info.state ) {
case STATE_METHOD_NAME:
{
obj = ((String)obj).trim();
methodName = (String)obj;
break;
}
case STATE_MEMBER:
{
StackInfo struct = (StackInfo)stack.peek();
NameValue nv = (NameValue)obj;
((Map)struct.obj).put(nv.name, nv.value);
obj = struct.obj;
break;
}
case STATE_PARAM:
{
if ( params == null )
params = new ArrayList();
params.add(obj);
break;
}
case STATE_DATA:
{
StackInfo array = (StackInfo)stack.peek();
((List)array.obj).add(obj);
break;
}
case STATE_NAME:
{
StackInfo struct = (StackInfo)stack.peek();
NameValue nv = (NameValue)struct.obj;
nv.name = (String)obj;
obj = nv;
break;
}
case STATE_VALUE:
{
StackInfo struct = (StackInfo)stack.peek();
if ( struct.state == STATE_MEMBER ) {
NameValue nv = (NameValue)struct.obj;
nv.value = (String)obj;
obj = nv;
}
break;
}
case STATE_INT:
{
obj = new Integer(((String)obj).trim());
break;
}
case STATE_I4:
{
obj = new Long(((String)obj).trim());
break;
}
case STATE_BOOLEAN:
{
obj = new Boolean(((String)obj).trim().equalsIgnoreCase("1"));
break;
}
case STATE_DOUBLE:
{
obj = new Double(((String)obj).trim());
break;
}
case STATE_BASE64:
{
try {
// Base-64 is converted into a byte array
String s = ((String)obj).trim();
StringBufferInputStream sbis = new StringBufferInputStream(s);
ByteArrayOutputStream bos = new ByteArrayOutputStream((s.length() * 3) / 4);
Base64.decode(sbis, bos);
obj = bos.toByteArray();
}
catch ( Exception e ) {
/** @todo This */
e.printStackTrace(System.err);
}
break;
}
case STATE_DATETIME:
{
try {
obj = IDF.parse(((String)obj).trim());
}
catch ( Exception e ) {
/** @todo This */
e.printStackTrace(System.err);
}
break;
}
case STATE_NIL:
{
obj = null;
break;
}
}
info = (StackInfo)stack.pop();
info.obj = obj;
}
public void characters(char ch[], int start, int length) {
String val = new String(ch, start, length);
if ( info.state != STATE_PCDATA && val.trim().length() > 0 ) {
boolean validState = false;
State s = states[info.state];
if ( s.states != null )
for ( int i = 0; i < s.states.length; i++ )
if ( s.states[i] == STATE_PCDATA )
validState = true;
if ( validState ) {
stack.push(info);
info = new StackInfo(STATE_PCDATA);
}
else {
stack.push(new StackInfo(STATE_FAULT));
StringBuffer sb = new StringBuffer("Invalid Parse State");
if ( s.element != null )
sb.append(": " + s.element);
processError(response, SERVER_ERROR, sb.toString());
}
}
if ( info.state == STATE_PCDATA ) {
if ( info.sb == null )
info.sb = new StringBuffer(length);
info.sb.append(val);
}
}
public void endDocument() {
if ( info.state != STATE_FAULT ) {
try {
instance.setMethodName(methodName);
for ( int i = 0; params != null && i < params.size(); i++ )
instance.setParameter(i, params.get(i));
Object o = instance.invoke();
response.setHeader(HTTP.HEADER_CONTENT_TYPE, Headers.TYPE_TEXT_XML);
OutputStream os = response.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF8");
BufferedWriter bw = new BufferedWriter(osw, 4096);
PrintWriter pw = new PrintWriter(bw);
pw.println(XML_HEADER);
pw.println("<"+METHOD_RESPONSE+"><"+PARAMS+"><"+PARAM+">");
writeValue(pw, o);
pw.println("</"+PARAM+"></"+PARAMS+"></"+METHOD_RESPONSE+">");
pw.flush(); // Is this needed?
pw.close(); // Is this needed?
response.close();
}
catch ( Exception e ) {
processError(response, SERVER_ERROR, e.getMessage());
}
}
}
}
/**
* StackInfo
*/
private class StackInfo {
public int state;
public Object obj;
public StringBuffer sb;
public StackInfo() {
}
public StackInfo(int state) {
this.state = state;
}
public StackInfo(int state, Object obj) {
this.state = state;
this.obj = obj;
}
public int getState() {
return state;
}
public Object getObject() {
return obj;
}
public StringBuffer getStringBuffer() {
return sb;
}
}
/**
* NameValue
*/
private class NameValue {
public String name;
public Object value;
public NameValue() {
}
public NameValue(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
/**
* State
*/
private static class State {
public int id;
public String element;
public int[] states;
public State() {
}
public State(int id, String element, int[] states) {
this.id = id;
this.element = element;
this.states = states;
}
public State(int id, int[] states) {
this.id = id;
this.states = states;
}
public State(int id, String element) {
this.id = id;
this.element = element;
}
public State(int id) {
this.id = id;
}
public int getID() {
return id;
}
public String getElement() {
return element;
}
public int[] getStates() {
return states;
}
}
}