// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] package org.ibex.js; import java.io.*; import java.util.*; import org.ibex.net.*; import org.ibex.util.*; import org.ibex.crypto.*; /** * An XML-RPC client implemented as a JavaScript Host Object. See the * Ibex spec for information on its behavior. * * NOTE: this client is EXTREMELY lenient in the responses it will * accept; there are many, many invalid responses that it will * successfully parse and return. Do NOT use this to determine the * validity of your server. * * This client conforms to The * XML-RPC Spec, subject to these limitations: *
    *
  1. XMLRPC cannot invoke methods that require a argument *
  2. if a return value contains a , it will be returned as a string *
  3. The decision to pass a number as or is based * entirely on whether or not the argument is fractional. Thus, it * is impossible to pass a non-fractional number to an xmlrpc * method that insists on being called with a element. We * hope that most xml-rpc servers will be able to automatically * convert. *
*/ public class XMLRPC extends JS { public XMLRPC(String url, String method) { this.http = url.startsWith("stdio:") ? HTTP.stdio : new HTTP(url); this.url = url; this.method = method; } public XMLRPC(String url, String method, XMLRPC httpSource) { this.http = httpSource.http; this.url = url; this.method = method; } public Object get(Object name) { return new XMLRPC(url, (method.equals("") ? "" : method + ".") + name.toString(), this); } /** this holds character content as we read it in -- since there is only one per instance, we don't support mixed content */ protected AccessibleCharArrayWriter content = new AccessibleCharArrayWriter(100); protected String url = null; ///< the url to connect to protected String method = null; ///< the method name to invoke on the remove server protected HTTP http = null; ///< the HTTP connection to use private Hash tracker; ///< used to detect multi-ref data protected boolean fault = false; ///< True iff the return value is a fault (and should be thrown as an exception) /** The object stack. As we process xml elements, pieces of the * return value are pushed onto and popped off of this stack. * * The general protocol is that any time a <value> tag is * encountered, an empty String ("") is pushed onto the stack. If * the <value/> node has content (either an anonymous * string or some other XML node), that content replaces the * empty string. * * If an <array> tag is encountered, a null is pushed onto the * stack. When a </data> is encountered, we search back on the * stack to the last null, replace it with a NativeJSArray, and * insert into it all elements above it on the stack. * * If a <struct> tag is encountered, a JSect is pushed * onto the stack. If a <name> tag is encountered, its CDATA is * pushed onto the stack. When a </member> is encountered, the * name (second element on stack) and value (top of stack) are * popped off the stack and inserted into the struct (third * element on stack). */ protected Vec objects = null; // Recieve //////////////////////////////////////////////////////////////// private class Helper extends XML { public Helper() { super(BUFFER_SIZE); } public void startElement(XML.Element c) { content.reset(); //#switch(c.getLocalName()) case "fault": fault = true; case "struct": objects.setElementAt(new JS(), objects.size() - 1); case "array": objects.setElementAt(null, objects.size() - 1); case "value": objects.addElement(""); //#end } public void endElement(XML.Element c) { //#switch(c.getLocalName()) case "int": objects.setElementAt(new Integer(new String(content.getBuf(), 0, content.size())), objects.size() - 1); case "i4": objects.setElementAt(new Integer(new String(content.getBuf(), 0, content.size())), objects.size() - 1); case "boolean": objects.setElementAt(content.getBuf()[0] == '1' ? Boolean.TRUE : Boolean.FALSE, objects.size() - 1); case "string": objects.setElementAt(new String(content.getBuf(), 0, content.size()), objects.size() - 1); case "double": objects.setElementAt(new Double(new String(content.getBuf(), 0, content.size())), objects.size() - 1); case "base64": objects.setElementAt(new Fountain.ByteArray(Base64.decode(new String(content.getBuf(), 0, content.size())), null), objects.size() - 1); case "name": objects.addElement(new String(content.getBuf(), 0, content.size())); case "value": if ("".equals(objects.lastElement())) objects.setElementAt(new String(content.getBuf(), 0, content.size()), objects.size() - 1); case "dateTime.iso8601": String s = new String(content.getBuf(), 0, content.size()); // strip whitespace int i=0; while(Character.isWhitespace(s.charAt(i))) i++; if (i > 0) s = s.substring(i); try { JSDate nd = new JSDate(); double date = JSDate.date_msecFromDate(Double.valueOf(s.substring(0, 4)).doubleValue(), Double.valueOf(s.substring(4, 6)).doubleValue() - 1, Double.valueOf(s.substring(6, 8)).doubleValue(), Double.valueOf(s.substring(9, 11)).doubleValue(), Double.valueOf(s.substring(12, 14)).doubleValue(), Double.valueOf(s.substring(15, 17)).doubleValue(), (double)0 ); nd.setTime(JSDate.internalUTC(date)); objects.setElementAt(nd, objects.size() - 1); } catch (Exception e) { throw new RuntimeException("ibex.net.rpc.xml.recieve.malformedDateTag" + "the server sent a tag which was malformed: " + s); } case "member": Object memberValue = objects.elementAt(objects.size() - 1); String memberName = (String)objects.elementAt(objects.size() - 2); JS struct = (JS)objects.elementAt(objects.size() - 3); try { struct.put(memberName, memberValue); } catch (JSExn e) { throw new Error("this should never happen"); } objects.setSize(objects.size() - 2); case "data": int i; for(i=objects.size() - 1; objects.elementAt(i) != null; i--); JSArray arr = new JSArray(); try { for(int j = i + 1; j\n"); content.append(" \n"); content.append(" "); content.append(method); content.append("\n"); content.append(" \n"); for(int i=0; i\n"); appendObject(args.elementAt(i), content); content.append(" \n"); } content.append(" \n"); content.append(" "); return content.toString(); } /** Appends the XML-RPC representation of o to sb */ void appendObject(Object o, StringBuffer sb) throws JSExn { if (o == null) { throw new JSExn("attempted to send a null value via XML-RPC"); } else if (o instanceof Number) { if ((double)((Number)o).intValue() == ((Number)o).doubleValue()) { sb.append(" "); sb.append(((Number)o).intValue()); sb.append("\n"); } else { sb.append(" "); sb.append(o); sb.append("\n"); } } else if (o instanceof Boolean) { sb.append(" "); sb.append(((Boolean)o).booleanValue() ? "1" : "0"); sb.append("\n"); } else if (o instanceof Fountain) { try { sb.append(" \n"); InputStream is = ((Fountain)o).getInputStream(); byte[] buf = new byte[54]; while(true) { int numread = is.read(buf, 0, 54); if (numread == -1) break; byte[] writebuf = buf; if (numread < buf.length) { writebuf = new byte[numread]; System.arraycopy(buf, 0, writebuf, 0, numread); } sb.append(" "); sb.append(new String(Base64.encode(writebuf))); sb.append("\n"); } sb.append("\n \n"); } catch (IOException e) { if (Log.on) Log.info(this, "caught IOException while attempting to send a Fountain via XML-RPC"); if (Log.on) Log.info(this, e); throw new JSExn("caught IOException while attempting to send a Fountain via XML-RPC"); } } else if (o instanceof String) { sb.append(" "); String s = (String)o; if (s.indexOf('<') == -1 && s.indexOf('&') == -1) { sb.append(s); } else { char[] cbuf = s.toCharArray(); int oldi = 0, i=0; while(true) { while(i < cbuf.length && cbuf[i] != '<' && cbuf[i] != '&') i++; sb.append(cbuf, oldi, i - oldi); if (i >= cbuf.length) break; if (cbuf[i] == '<') sb.append("<"); else if (cbuf[i] == '&') sb.append("&"); i = oldi = i + 1; if (i >= cbuf.length) break; } } sb.append("\n"); } else if (o instanceof JSDate) { sb.append(" "); java.util.Date d = new java.util.Date(((JSDate)o).getRawTime()); sb.append(d.getYear() + 1900); if (d.getMonth() + 1 < 10) sb.append('0'); sb.append(d.getMonth() + 1); if (d.getDate() < 10) sb.append('0'); sb.append(d.getDate()); sb.append('T'); if (d.getHours() < 10) sb.append('0'); sb.append(d.getHours()); sb.append(':'); if (d.getMinutes() < 10) sb.append('0'); sb.append(d.getMinutes()); sb.append(':'); if (d.getSeconds() < 10) sb.append('0'); sb.append(d.getSeconds()); sb.append("\n"); } else if (o instanceof JSArray) { if (tracker.get(o) != null) throw new JSExn("attempted to send multi-ref data structure via XML-RPC"); tracker.put(o, Boolean.TRUE); sb.append(" \n"); JSArray a = (JSArray)o; for(int i=0; i\n"); } else if (o instanceof JS) { if (tracker.get(o) != null) throw new JSExn("attempted to send multi-ref data structure via XML-RPC"); tracker.put(o, Boolean.TRUE); JS j = (JS)o; sb.append(" \n"); Enumeration e = j.keys(); while(e.hasMoreElements()) { Object key = e.nextElement(); sb.append(" " + key + "\n"); appendObject(j.get(key), sb); sb.append(" \n"); } sb.append(" \n"); } else { throw new JSExn("attempt to send object of type " + o.getClass().getName() + " via XML-RPC"); } } // Call Sequence ////////////////////////////////////////////////////////////////////////// /* FIXME this has been disabled to make XMLRPC usable without Scheduler public final Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn { JSArray args = new JSArray(); for(int i=0; i