// Copyright 2000-2005 the Contributors, as shown in the revision logs. // Licensed under the Apache Public Source License 2.0 ("the License"). // You may not use this file except in compliance with the License. package org.ibex.js; import java.io.*; import org.ibex.net.*; import org.ibex.util.*; /** * 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.Immutable { 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 JS get(JS name) throws JSExn { return new XMLRPC(url, (method.equals("") ? "" : method + ".") + JSU.toString(name), this); } /** this holds character content as we read it in -- since there is only one per instance, we don't support mixed content */ protected PublicCharArrayWriter content = new PublicCharArrayWriter(100); public static final class PublicCharArrayWriter extends CharArrayWriter { public PublicCharArrayWriter(int i) { super(i); } public char[] buf; }; 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 Basket.Map 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 Basket.Array objects = null; private void setLast(Object o) { objects.set(objects.size() - 1, o); } // Recieve //////////////////////////////////////////////////////////////// private class Helper extends XML { public Helper() { super(BUFFER_SIZE, true); } public void startElement(Tree.Element c) { content.reset(); //#switch(c.getLocalName()) case "fault": fault = true; case "struct": setLast(new JS.Obj()); case "array": setLast(null); case "value": objects.add(""); //#end } public void endElement(Tree.Element c) { //#switch(c.getLocalName()) case "int": setLast(new Integer(new String(content.buf, 0, content.size()))); case "i4": setLast(new Integer(new String(content.buf, 0, content.size()))); case "boolean": setLast(content.buf[0] == '1' ? Boolean.TRUE : Boolean.FALSE); case "string": setLast(new String(content.buf, 0, content.size())); case "double": setLast(new Double(new String(content.buf, 0, content.size()))); case "base64": setLast(new Fountain.ByteArray(Encode.fromBase64(new String(content.buf, 0, content.size())), null)); case "name": objects.add(new String(content.buf, 0, content.size())); case "value": if ("".equals(objects.peek())) setLast(new String(content.buf, 0, content.size())); case "dateTime.iso8601": String s = new String(content.buf, 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)); setLast(nd); } catch (Exception e) { throw new RuntimeException("ibex.net.rpc.xml.recieve.malformedDateTag" + "the server sent a tag which was malformed: " + s); } case "member": JS memberValue = (JS)objects.get(objects.size() - 1); JS memberName = (JS)objects.get(objects.size() - 2); JS struct = (JS)objects.get(objects.size() - 3); try { struct.put(memberName, memberValue); } catch (JSExn e) { throw new Error("this should never happen"); } objects.pop(); objects.pop(); case "data": int i; for(i=objects.size() - 1; objects.get(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 < args.size(); i++) { content.append(" \n"); appendObject(args.get(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(Encode.toBase64(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, JSU.B(true)); sb.append(" \n"); JSArray a = (JSArray)o; for(int i=0; i < a.size(); i++) appendObject(a.get(i), sb); sb.append(" \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, JSU.B(true)); JS j = (JS)o; sb.append(" \n"); Enumeration e = j.keys(); while (e.hasNext()) { Object key = e.next(); sb.append(" " + key + "\n"); appendObject(j.get((JS)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(JS a0, JS a1, JS a2, JS[] rest, int nargs) throws JSExn { JSArray args = new JSArray(); for(int i=0; i