commit 8fdeea99071b52c24354dd8ae273114b0b1b81fd
parent 5764dceb2a39ddf63425808c3dc46030be4587f6
Author: Brian Swetland <swetland@frotz.net>
Date: Sun, 24 Jul 2011 19:11:40 -0700
overhaul SoapRPC, update code
- provide a reasonable API for assembling message bodies
- reducing copies and cruft in the xmit/recv path
- automatically grow assembly buffers now
- don't use a static decoder in XML (not threadsafe)
- modify Sonos to use the new SoapRPC API
Signed-off-by: Brian Swetland <swetland@frotz.net>
Diffstat:
5 files changed, 259 insertions(+), 96 deletions(-)
diff --git a/net/frotz/sonos/SoapRPC.java b/net/frotz/sonos/SoapRPC.java
@@ -20,91 +20,159 @@ import java.net.InetAddress;
import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;
+
import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
class SoapRPC {
+ static Charset cs = Charset.forName("UTF-8");
+
public boolean trace_io;
public boolean trace_reply;
+ /* actual host to communicate with */
InetAddress addr;
int port;
- ByteBuffer reply;
+
+ /* XML object for reply */
XML xml;
+ /* io buffer */
+ ByteBuffer bb;
+
+ /* assembly buffers for rpc header and message */
+ StringBuilder hdr;
+ StringBuilder msg;
+
+ CharsetEncoder encoder;
+
+ /* hold remote information while assembling message */
+ String xmethod, xservice, xpath;
+
public SoapRPC(byte[] host, int port) {
+ init(host,port);
+ }
+
+ void init(byte[] host, int port) {
try {
addr = InetAddress.getByAddress(host);
} catch (Exception x) {
}
this.port = port;
- reply = ByteBuffer.wrap(new byte[32768]);
+ encoder = cs.newEncoder();
+
+ bb = ByteBuffer.wrap(new byte[32768]);
xml = new XML(32768);
+ hdr = new StringBuilder(2048);
+ msg = new StringBuilder(8192);
}
- void call(byte[] data) {
+
+
+ void call() {
+ CharBuffer hdrbuf;
+ CharBuffer msgbuf;
+ CoderResult cr;
+ int off, r;
+ byte[] buf;
try {
- reply.clear();
- byte[] buf = reply.array();
Socket s = new Socket(addr,port);
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
- out.write(data);
- int off = 0;
- for (;;) {
- int r = in.read(buf, off, buf.length - off);
- if (r <= 0) break;
- off += r;
+
+ if (trace_io) {
+ System.err.println("--------- xmit -----------");
+ System.err.print(hdr);
+ System.err.print(msg);
}
- reply.limit(off);
+
+ buf = bb.array();
+
+ /* to keep things simple, the headers must fit in one pass */
+ bb.clear();
+ encoder.reset();
+ hdrbuf = CharBuffer.wrap(hdr);
+ cr = encoder.encode(hdrbuf, bb, false);
+ if (cr != CoderResult.UNDERFLOW)
+ throw new Exception("encoder failed (1)");
+
+ msgbuf = CharBuffer.wrap(msg);
+ do {
+ cr = encoder.encode(msgbuf, bb, true);
+ if (cr.isError())
+ throw new Exception("encoder failed (2)");
+ out.write(buf, 0, bb.position());
+ bb.clear();
+ } while (cr == CoderResult.OVERFLOW);
+
+
+ /* read reply */
+ for (off = 0;;off += r) {
+ r = in.read(buf, off, buf.length-off);
+ if (r <= 0)
+ break;
+ }
+ bb.limit(off);
+
s.close();
if (trace_io) {
- System.out.println("--------- reply -----------");
- System.out.println(new String(buf, 0, off));
+ System.err.println("--------- recv -----------");
+ System.err.println(new String(buf, 0, off));
+ System.err.println("--------- done -----------");
}
} catch (Exception x) {
System.out.println("OOPS: " + x.getMessage());
x.printStackTrace();
}
}
- public XML call(Endpoint ept, String method, String payload) {
- StringBuilder msg = new StringBuilder();
- msg.append("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>");
- msg.append("<u:");
- msg.append(method);
+
+ public void prepare(Endpoint ept, String method) {
+ xmethod = method;
+ xservice = ept.service;
+ xpath = ept.path;
+
+ msg.setLength(0);
+
+ /* setup message envelope/prefix */
+ msg.append("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:");
+ msg.append(xmethod);
msg.append(" xmlns:u=\"urn:schemas-upnp-org:service:");
- msg.append(ept.service);
- msg.append("\">");
- msg.append(payload);
+ msg.append(xservice);
+ msg.append("\">\n");
+ }
+
+ public XML invoke() {
+ if (xmethod == null)
+ throw new RuntimeException("cannot invoke before prepare");
+
+ /* close the envelope */
msg.append("</u:");
- msg.append(method);
+ msg.append(xmethod);
msg.append("></s:Body></s:Envelope>\n");
- StringBuilder sb = new StringBuilder();
- sb.append("POST ");
- sb.append(ept.path);
- sb.append(" HTTP/1.0\r\n");
- sb.append("CONNECTION: close\r\n");
- sb.append("Content-Type: text/xml; charset=\"utf-8\"\r\n");
- sb.append("Content-Length: ");
- sb.append(msg.length());
- sb.append("\r\n");
- sb.append("SOAPACTION: \"urn:schemas-upnp-org:service:");
- sb.append(ept.service);
- sb.append("#");
- sb.append(method);
- sb.append("\"\r\n\r\n");
- sb.append(msg);
-
- if (trace_io) {
- System.out.println("--------- message -----------");
- System.out.println(sb);
- }
+ /* build HTTP headers */
+ hdr.append("POST ");
+ hdr.append(xpath);
+ hdr.append(" HTTP/1.0\r\n"+"CONNECTION: close\r\n"+
+ "Content-Type: text/xml; charset=\"utf-8\"\r\n"+
+ "Content-Length: ");
+ hdr.append(msg.length());
+ hdr.append("\r\n"+"SOAPACTION: \"urn:schemas-upnp-org:service:");
+ hdr.append(xservice);
+ hdr.append("#");
+ hdr.append(xmethod);
+ hdr.append("\"\r\n\r\n");
- byte[] data = sb.toString().getBytes();
+ xmethod = null;
- call(data);
+ call();
- xml.init(reply);
+ xml.init(bb);
try {
if (trace_reply) {
System.out.println("--------- reply -----------");
@@ -118,6 +186,58 @@ class SoapRPC {
return null;
}
}
+
+ public SoapRPC openTag(String name) {
+ msg.append('<');
+ msg.append(name);
+ msg.append('>');
+ return this;
+ }
+ public SoapRPC closeTag(String name) {
+ msg.append('<');
+ msg.append('/');
+ msg.append(name);
+ msg.append('>');
+ return this;
+ }
+ public SoapRPC simpleTag(String name, int value) {
+ openTag(name);
+ msg.append(value);
+ closeTag(name);
+ return this;
+ }
+ public SoapRPC simpleTag(String name, String value) {
+ openTag(name);
+ encode(value);
+ closeTag(name);
+ return this;
+ }
+ public SoapRPC encode(CharSequence csq) {
+ int n, max = csq.length();
+ char c;
+ for (n = 0; n < max; n++) {
+ switch((c = csq.charAt(n))) {
+ case '<':
+ msg.append("<");
+ break;
+ case '>':
+ msg.append(">");
+ break;
+ case '&':
+ msg.append("&");
+ break;
+ case '"':
+ msg.append(""");
+ break;
+ case '\'':
+ msg.append("'");
+ break;
+ default:
+ msg.append(c);
+ }
+ }
+ return this;
+ }
public static class Endpoint {
String service,path;
public Endpoint(String service, String path) {
@@ -126,3 +246,4 @@ class SoapRPC {
}
}
}
+
diff --git a/net/frotz/sonos/Sonos.java b/net/frotz/sonos/Sonos.java
@@ -16,14 +16,21 @@
package net.frotz.sonos;
+/* not thread-safe, not reentrant */
public class Sonos {
boolean trace_browse;
SoapRPC.Endpoint xport;
SoapRPC.Endpoint media;
SoapRPC.Endpoint render;
SoapRPC rpc;
+ XML result;
public Sonos(byte[] ip) {
+ init(ip);
+ }
+
+ void init(byte[] ip) {
+ result = new XML(32768);
rpc = new SoapRPC(ip, 1400);
xport = new SoapRPC.Endpoint(
@@ -41,75 +48,108 @@ public class Sonos {
public void trace_reply(boolean x) { rpc.trace_reply = x; }
public void trace_browse(boolean x) { trace_browse = x; }
+ /* volume controls */
public void volume() {
- rpc.call(render,"GetVolume",
- "<InstanceID>0</InstanceID>"+
- "<Channel>Master</Channel>" // Master | LF | RF
- );
+ rpc.prepare(render,"GetVolume");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("Channel", "Master"); // Master | LF | RF
+ rpc.invoke();
}
public void volume(int vol) { // 0-100
if ((vol < 0) || (vol > 100))
return;
- rpc.call(render,"SetVolume",
- "<InstanceID>0</InstanceID>"+
- "<Channel>Master</Channel>"+
- "<DesiredVolume>"+vol+"</DesiredVolume>"
- );
+ rpc.prepare(render,"SetVolume");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("Channel","Master");
+ rpc.simpleTag("DesiredVolume",vol);
+ rpc.invoke();
}
+
+ /* transport controls */
public void play() {
- rpc.call(xport,"Play","<InstanceID>0</InstanceID><Speed>1</Speed>");
+ rpc.prepare(xport,"Play");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("Speed",1);
+ rpc.invoke();
}
public void pause() {
- rpc.call(xport,"Pause","<InstanceID>0</InstanceID>");
+ rpc.prepare(xport,"Pause");
+ rpc.simpleTag("InstanceID",0);
+ rpc.invoke();
}
public void stop() {
- rpc.call(xport,"Stop","<InstanceID>0</InstanceID>");
+ rpc.prepare(xport,"Stop");
+ rpc.simpleTag("InstanceID",0);
+ rpc.invoke();
}
public void next() {
- rpc.call(xport,"Next","<InstanceID>0</InstanceID>");
+ rpc.prepare(xport,"Next");
+ rpc.simpleTag("InstanceID",0);
+ rpc.invoke();
}
- public void seekTrack(String nr) {
- rpc.call(xport,"Seek","<InstanceID>0</InstanceID><Unit>TRACK_NR</Unit><Target>"+nr+"</Target>");
+ public void prev() {
+ rpc.prepare(xport,"Previous");
+ rpc.simpleTag("InstanceID",0);
+ rpc.invoke();
+ }
+ public void seekTrack(int nr) {
+ if (nr < 1)
+ return;
+ rpc.prepare(xport,"Seek");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("Unit","TRACK_NR");
+ rpc.simpleTag("Target",nr);
+ rpc.invoke();
// does not start playing if not already in playback mode
}
- public void prev() {
- rpc.call(xport,"Previous","<InstanceID>0</InstanceID>");
+
+ /* queue management */
+ public void add(String uri) {
+ rpc.prepare(xport,"AddURIToQueue");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("EnqueuedURI",uri);
+ rpc.simpleTag("EnqueuedURIMetaData","");
+ rpc.simpleTag("DesiredFirstTrackNumberEnqueued",0);
+ rpc.simpleTag("EnqueueAsNext",0); // 0 = append, 1+ = insert
+ rpc.invoke();
}
public void remove(String id) {
- rpc.call(xport,"RemoveTrackFromQueue","<InstanceID>0</InstanceID><ObjectID>"+id+"</ObjectID>");
+ rpc.prepare(xport,"RemoveTrackFromQueue");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("ObjectID",id);
+ rpc.invoke();
}
public void removeAll() {
- rpc.call(xport,"RemoveAllTracksFromQueue","<InstanceID>0</InstanceID>");
- }
- public void add(String uri) {
- rpc.call(xport,"AddURIToQueue",
- "<InstanceID>0</InstanceID>"+
- "<EnqueuedURI>"+uri+"</EnqueuedURI>"+ // from <res> x-file-cifs... etc
- "<EnqueuedURIMetaData></EnqueuedURIMetaData>"+
- "<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>"+
- "<EnqueueAsNext>0</EnqueueAsNext>" // 0 = append, 1-n = insert
- );
+ rpc.prepare(xport,"RemoveAllTracksFromQueue");
+ rpc.simpleTag("InstanceID",0);
+ rpc.invoke();
}
- public void move(String from, String to) {
- rpc.call(xport,"ReorderTracksInQueue",
- "<InstanceID>0</InstanceID>"+
- "<StartingIndex>"+from+"</StartingIndex>"+
- "<NumberOfTracks>1</NumberOfTracks>"+
- "<InsertBefore>"+to+"</InsertBefore>"
- );
+ public void move(int from, int to) {
+ if ((from < 1) || (to < 1))
+ return;
+ rpc.prepare(xport,"ReorderTracksInQueue");
+ rpc.simpleTag("InstanceID",0);
+ rpc.simpleTag("StartingIndex",from);
+ rpc.simpleTag("NumberOfTracks",1);
+ rpc.simpleTag("InsertBefore",to);
+ rpc.invoke();
}
+
+ /* content service calls */
public void list(String _id, boolean d) {
int n = 0;
- XML result = new XML(32768);
- XML xml = rpc.call(media,"Browse",
- "<ObjectID>"+_id+"</ObjectID>"+
- (!d ? "<BrowseFlag>BrowseMetadata</BrowseFlag>" :
- "<BrowseFlag>BrowseDirectChildren</BrowseFlag>" )+
- "<Filter></Filter>"+
- "<StartingIndex>" + n + "</StartingIndex>"+
- "<RequestedCount>25</RequestedCount>"+
- "<SortCriteria></SortCriteria>"
- );
+ XML xml;
+
+ rpc.prepare(media,"Browse");
+ rpc.simpleTag("ObjectID",_id);
+ rpc.simpleTag("BrowseFlag",
+ (d ? "BrowseDirectChildren" : "BrowseMetadata"));
+ rpc.simpleTag("Filter","");
+ rpc.simpleTag("StartingIndex", n);
+ rpc.simpleTag("RequestedCount",25);
+ rpc.simpleTag("SortCriteria","");
+ xml = rpc.invoke();
+
try {
XMLSequence name = new XMLSequence();
XMLSequence value = new XMLSequence();
diff --git a/net/frotz/sonos/XML.java b/net/frotz/sonos/XML.java
@@ -43,10 +43,13 @@ public class XML {
Matcher mAttr;
boolean isOpen;
+ CharsetDecoder decoder;
+
/* used for io operations */
CharBuffer buf;
public XML(int size) {
+ decoder = cs.newDecoder();
seq = new XMLSequence();
tag = new XMLSequence();
tmp = new XMLSequence();
@@ -267,7 +270,6 @@ public class XML {
static Pattern pEntity = Pattern.compile("([^&]*)(&([^;]*);)");
static Charset cs = Charset.forName("UTF-8");
- static CharsetDecoder decoder = cs.newDecoder();
static public class Oops extends Exception {
public Oops(String msg) {
diff --git a/net/frotz/sonos/XMLSequence.java b/net/frotz/sonos/XMLSequence.java
@@ -95,7 +95,7 @@ class XMLSequence implements CharSequence {
return off;
}
- CharSequence copy() {
+ public CharSequence copy() {
XMLSequence s = new XMLSequence();
s.init(data, offset, offset + count);
return s;
diff --git a/net/frotz/sonos/app.java b/net/frotz/sonos/app.java
@@ -21,7 +21,7 @@ public class app {
Sonos sonos = new Sonos(new byte[] { 10, 0, 0, (byte) 199});
String cmd = args[0];
- //sonos.trace_io(true);
+ sonos.trace_io(true);
sonos.trace_reply(true);
sonos.trace_browse(true);
@@ -44,9 +44,9 @@ public class app {
} else if (cmd.equals("removeall")) {
sonos.removeAll();
} else if (cmd.equals("move")) {
- sonos.move(args[1],args[2]);
+ sonos.move(Integer.parseInt(args[1]),Integer.parseInt(args[2]));
} else if (cmd.equals("track")) {
- sonos.seekTrack(args[1]);
+ sonos.seekTrack(Integer.parseInt(args[1]));
} else if (cmd.equals("volume")) {
if (args.length == 1)
sonos.volume();