sonos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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:
Mnet/frotz/sonos/SoapRPC.java | 213++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mnet/frotz/sonos/Sonos.java | 130++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mnet/frotz/sonos/XML.java | 4+++-
Mnet/frotz/sonos/XMLSequence.java | 2+-
Mnet/frotz/sonos/app.java | 6+++---
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("&lt;"); + break; + case '>': + msg.append("&gt;"); + break; + case '&': + msg.append("&amp;"); + break; + case '"': + msg.append("&quot;"); + break; + case '\'': + msg.append("&apos;"); + 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();