/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ /** * Inspired by Copyright 2007 David Wheeler Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. * * */ package org.openhab.binding.sonos.internal; import java.io.IOException; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.joda.time.DateTime; import org.joda.time.LocalTime; import org.joda.time.Period; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.PeriodFormatter; import org.joda.time.format.PeriodFormatterBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.teleal.cling.model.meta.StateVariable; import org.teleal.cling.model.meta.StateVariableTypeDetails; import org.teleal.cling.model.state.StateVariableValue; import org.teleal.cling.model.types.StringDatatype; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; /** * XMLParser is a set of helper classes and methods to parse XML string that are returned by Sonos players in the * network, or that * are used to parse string returned after a call to a specific UPNP variable request * * @author Karel Goderis * @since 1.1.0 * */ public class SonosXMLParser { static final Logger logger = LoggerFactory.getLogger(SonosXMLParser.class); private static MessageFormat METADATA_FORMAT = new MessageFormat( "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" " + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" " + "xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" " + "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">" + "<item id=\"{0}\" parentID=\"{1}\" restricted=\"true\">" + "<dc:title>{2}</dc:title>" + "<upnp:class>{3}</upnp:class>" + "<desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">" + "{4}</desc>" + "</item></DIDL-Lite>"); private enum Element { TITLE, CLASS, ALBUM, ALBUM_ART_URI, CREATOR, RES, TRACK_NUMBER, RESMD, DESC } private enum CurrentElement { item, res, streamContent, albumArtURI, title, upnpClass, creator, album, albumArtist, desc; } /** * @param xml * @return a list of alarms from the given xml string. * @throws IOException * @throws SAXException */ public static List<SonosAlarm> getAlarmsFromStringResult(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); AlarmHandler handler = new AlarmHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Alarms from String {}", xml); } return handler.getAlarms(); } /** * @param xml * @return a list of Entrys from the given xml string. * @throws IOException * @throws SAXException */ public static List<SonosEntry> getEntriesFromString(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); EntryHandler handler = new EntryHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Entries from String {}", xml); } return handler.getArtists(); } /** * Returns the meta data which is needed to play Pandora * (and others?) favorites * * @param xml * @return The value of the desc xml tag * @throws SAXException */ public static SonosResourceMetaData getEmbededMetaDataFromResource(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); EmbededMetaDataHandler handler = new EmbededMetaDataHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Entries from String {}", xml); } return handler.getMetaData(); } /** * @param controller * @param xml * @return zone group from the given xml * @throws IOException * @throws SAXException */ public static List<SonosZoneGroup> getZoneGroupFromXML(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); ZoneGroupHandler handler = new ZoneGroupHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { // This should never happen - we're not performing I/O! logger.error("Could not parse ZoneGroup from String {}", xml); } return handler.getGroups(); } public static List<String> getRadioTimeFromXML(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); OpmlHandler handler = new OpmlHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { // This should never happen - we're not performing I/O! logger.error("Could not parse RadioTime from String {}", xml); } return handler.getTextFields(); } public static Map<String, StateVariableValue> getRenderingControlFromXML(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); RenderingControlEventHandler handler = new RenderingControlEventHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { // This should never happen - we're not performing I/O! logger.debug("Could not parse Rendering Control event: {}", e); } return handler.getChanges(); } public static Map<String, StateVariableValue> getAVTransportFromXML(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); AVTransportEventHandler handler = new AVTransportEventHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { // This should never happen - we're not performing I/O! logger.error("Could not parse AV Transport Event: {}", e); } return handler.getChanges(); } public static SonosMetaData getMetaDataFromXML(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); // logger.debug("getTrackFromXML {}",xml); MetaDataHandler handler = new MetaDataHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { // This should never happen - we're not performing I/O! logger.error("Could not parse AV Transport Event: {}", e); } return handler.getMetaData(); } static private class EntryHandler extends DefaultHandler { // Maintain a set of elements about which it is unuseful to complain about. // This list will be initialized on the first failure case private static List<String> ignore = null; private String id; private String parentId; private StringBuilder upnpClass = new StringBuilder(); private StringBuilder res = new StringBuilder(); private StringBuilder title = new StringBuilder(); private StringBuilder album = new StringBuilder(); private StringBuilder albumArtUri = new StringBuilder(); private StringBuilder creator = new StringBuilder(); private StringBuilder trackNumber = new StringBuilder(); private StringBuilder desc = new StringBuilder(); private Element element = null; private List<SonosEntry> artists = new ArrayList<SonosEntry>(); EntryHandler() { // shouldn't be used outside of this package. } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("container") || qName.equals("item")) { id = attributes.getValue("id"); parentId = attributes.getValue("parentID"); } else if (qName.equals("res")) { element = Element.RES; } else if (qName.equals("dc:title")) { element = Element.TITLE; } else if (qName.equals("upnp:class")) { element = Element.CLASS; } else if (qName.equals("dc:creator")) { element = Element.CREATOR; } else if (qName.equals("upnp:album")) { element = Element.ALBUM; } else if (qName.equals("upnp:albumArtURI")) { element = Element.ALBUM_ART_URI; } else if (qName.equals("upnp:originalTrackNumber")) { element = Element.TRACK_NUMBER; } else if (qName.equals("r:resMD")) { element = Element.RESMD; } else { if (ignore == null) { ignore = new ArrayList<String>(); ignore.add("DIDL-Lite"); ignore.add("type"); ignore.add("ordinal"); ignore.add("description"); } if (!ignore.contains(localName)) { logger.warn("Did not recognise element named {}", localName); } element = null; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (element == null) { return; } switch (element) { case TITLE: title.append(ch, start, length); break; case CLASS: upnpClass.append(ch, start, length); break; case RES: res.append(ch, start, length); break; case ALBUM: album.append(ch, start, length); break; case ALBUM_ART_URI: albumArtUri.append(ch, start, length); break; case CREATOR: creator.append(ch, start, length); break; case TRACK_NUMBER: trackNumber.append(ch, start, length); break; case RESMD: desc.append(ch, start, length); break; // no default } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("container") || qName.equals("item")) { element = null; int trackNumberVal = 0; try { trackNumberVal = Integer.parseInt(trackNumber.toString()); } catch (Exception e) { } SonosResourceMetaData md = null; // The resource description is needed for playing favorites on pandora try { md = getEmbededMetaDataFromResource(desc.toString()); } catch (SAXException ignore) { logger.debug("Failed to parse embedded", ignore); } artists.add(new SonosEntry(id, title.toString(), parentId, album.toString(), albumArtUri.toString(), creator.toString(), upnpClass.toString(), res.toString(), trackNumberVal, md)); title = new StringBuilder(); upnpClass = new StringBuilder(); res = new StringBuilder(); album = new StringBuilder(); albumArtUri = new StringBuilder(); creator = new StringBuilder(); trackNumber = new StringBuilder(); desc = new StringBuilder(); } } public List<SonosEntry> getArtists() { return artists; } } static private class EmbededMetaDataHandler extends DefaultHandler { private String id; private String parentId; private StringBuilder title = new StringBuilder(); private StringBuilder upnpClass = new StringBuilder(); private StringBuilder desc = new StringBuilder(); private Element element = null; private SonosResourceMetaData metaData = null; EmbededMetaDataHandler() { // shouldn't be used outside of this package. } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("container") || qName.equals("item")) { id = attributes.getValue("id"); parentId = attributes.getValue("parentID"); } else if (qName.equals("desc")) { element = Element.DESC; } else if (qName.equals("upnp:class")) { element = Element.CLASS; } else if (qName.equals("dc:title")) { element = Element.TITLE; } else { element = null; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (element == null) { return; } switch (element) { case TITLE: title.append(ch, start, length); break; case CLASS: upnpClass.append(ch, start, length); break; case DESC: desc.append(ch, start, length); ; break; default: break; } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("DIDL-Lite")) { metaData = new SonosResourceMetaData(id, parentId, title.toString(), upnpClass.toString(), desc.toString()); element = null; desc = new StringBuilder(); upnpClass = new StringBuilder(); title = new StringBuilder(); } } public SonosResourceMetaData getMetaData() { return metaData; } } static private class AlarmHandler extends DefaultHandler { private String id; private String startTime; private String duration; private String recurrence; private String enabled; private String roomUUID; private String programURI; private String programMetaData; private String playMode; private String volume; private String includeLinkedZones; private List<SonosAlarm> alarms = new ArrayList<SonosAlarm>(); AlarmHandler() { // shouldn't be used outside of this package. } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("Alarm")) { id = attributes.getValue("ID"); duration = attributes.getValue("Duration"); recurrence = attributes.getValue("Recurrence"); startTime = attributes.getValue("StartTime"); enabled = attributes.getValue("Enabled"); roomUUID = attributes.getValue("RoomUUID"); programURI = attributes.getValue("ProgramURI"); programMetaData = attributes.getValue("ProgramMetaData"); playMode = attributes.getValue("PlayMode"); volume = attributes.getValue("Volume"); includeLinkedZones = attributes.getValue("IncludeLinkedZones"); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("Alarm")) { int finalID = 0; int finalVolume = 0; boolean finalEnabled = false; boolean finalIncludeLinkedZones = false; DateTime finalStartTime = null; Period finalDuration = null; try { finalID = Integer.parseInt(id); finalVolume = Integer.parseInt(volume); if (enabled.equals("0")) { finalEnabled = false; } else { finalEnabled = true; } if (includeLinkedZones.equals("0")) { finalIncludeLinkedZones = false; } else { finalIncludeLinkedZones = true; } } catch (Exception e) { logger.debug("Error parsing Integer"); } try { DateTimeFormatter formatter = DateTimeFormat.forPattern("HH:mm:ss"); finalStartTime = formatter.parseDateTime(startTime); LocalTime localDateTime = finalStartTime.toLocalTime(); finalStartTime = localDateTime.toDateTimeToday(); PeriodFormatter pFormatter = new PeriodFormatterBuilder().appendHours().appendSeparator(":") .appendMinutes().appendSeparator(":").appendSeconds().toFormatter(); finalDuration = pFormatter.parsePeriod(duration); } catch (Exception e) { logger.error("Error parsing DateTime"); } alarms.add(new SonosAlarm(finalID, finalStartTime, finalDuration, recurrence, finalEnabled, roomUUID, programURI, programMetaData, playMode, finalVolume, finalIncludeLinkedZones)); } } public List<SonosAlarm> getAlarms() { return alarms; } } static private class ZoneGroupHandler extends DefaultHandler { private final List<SonosZoneGroup> groups = new ArrayList<SonosZoneGroup>(); private final List<String> currentGroupPlayers = new ArrayList<String>(); private String coordinator; private String groupId; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("ZoneGroup")) { groupId = attributes.getValue("ID"); coordinator = attributes.getValue("Coordinator"); } else if (qName.equals("ZoneGroupMember")) { currentGroupPlayers.add(attributes.getValue("UUID")); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("ZoneGroup")) { groups.add(new SonosZoneGroup(groupId, coordinator, currentGroupPlayers)); currentGroupPlayers.clear(); } } public List<SonosZoneGroup> getGroups() { return groups; } } static private class OpmlHandler extends DefaultHandler { // <opml version="1"> // <head> // <status>200</status> // // </head> // <body> // <outline type="text" text="Q-Music 103.3" guide_id="s2398" key="station" // image="http://radiotime-logos.s3.amazonaws.com/s87683q.png" preset_id="s2398"/> // <outline type="text" text="Bjorn Verhoeven" guide_id="p257265" seconds_remaining="2230" duration="7200" // key="show"/> // <outline type="text" text="Top 40-Pop"/> // <outline type="text" text="37m remaining"/> // <outline type="object" text="NowPlaying"> // <nowplaying> // <logo>http://radiotime-logos.s3.amazonaws.com/s87683.png</logo> // <twitter_id /> // </nowplaying> // </outline> // </body> // </opml> private final List<String> textFields = new ArrayList<String>(); private String textField; private String type; private String logo; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("outline")) { type = attributes.getValue("type"); if (type.equals("text")) { textField = attributes.getValue("text"); } else { textField = null; } } else if (qName.equals("logo")) { // logo = attributes.getValue("UUID"); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("outline")) { if (textField != null) { textFields.add(textField); } } } public List<String> getTextFields() { return textFields; } } static private class AVTransportEventHandler extends DefaultHandler { /* * <Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/"> * <InstanceID val="0"> * <TransportState val="PLAYING"/> * <CurrentPlayMode val="NORMAL"/> * <CurrentPlayMode val="0"/> * <NumberOfTracks val="29"/> * <CurrentTrack val="12"/> * <CurrentSection val="0"/> * <CurrentTrackURI val= * "x-file-cifs://192.168.1.1/Storage4/Sonos%20Music/Queens%20Of%20The%20Stone%20Age/Lullabies%20To%20Paralyze/Queens%20Of%20The%20Stone%20Age%20-%20Lullabies%20To%20Paralyze%20-%2012%20-%20Broken%20Box.wma" * /> * <CurrentTrackDuration val="0:03:02"/> * <CurrentTrackMetaData val= * "<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="x-file-cifs:*:audio/x-ms-wma:*" duration="0:03:02">x-file-cifs://192.168.1.1/Storage4/Sonos%20Music/Queens%20Of%20The%20Stone%20Age/Lullabies%20To%20Paralyze/Queens%20Of%20The%20Stone%20Age%20-%20Lullabies%20To%20Paralyze%20-%2012%20-%20Broken%20Box.wma</res><r:streamContent></r:streamContent><dc:title>Broken Box</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:creator>Queens Of The Stone Age</dc:creator><upnp:album>Lullabies To Paralyze</upnp:album><r:albumArtist>Queens Of The Stone Age</r:albumArtist></item></DIDL-Lite>" * /><r:NextTrackURI val= * "x-file-cifs://192.168.1.1/Storage4/Sonos%20Music/Queens%20Of%20The%20Stone%20Age/Lullabies%20To%20Paralyze/Queens%20Of%20The%20Stone%20Age%20-%20Lullabies%20To%20Paralyze%20-%2013%20-%20''You%20Got%20A%20Killer%20Scene%20There,%20Man...''.wma" * /><r:NextTrackMetaData val= * "<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="x-file-cifs:*:audio/x-ms-wma:*" duration="0:04:56">x-file-cifs://192.168.1.1/Storage4/Sonos%20Music/Queens%20Of%20The%20Stone%20Age/Lullabies%20To%20Paralyze/Queens%20Of%20The%20Stone%20Age%20-%20Lullabies%20To%20Paralyze%20-%2013%20-%20&apos;&apos;You%20Got%20A%20Killer%20Scene%20There,%20Man...&apos;&apos;.wma</res><dc:title>&apos;&apos;You Got A Killer Scene There, Man...&apos;&apos;</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:creator>Queens Of The Stone Age</dc:creator><upnp:album>Lullabies To Paralyze</upnp:album><r:albumArtist>Queens Of The Stone Age</r:albumArtist></item></DIDL-Lite>" * /><r:EnqueuedTransportURI * val="x-rincon-playlist:RINCON_000E582126EE01400#A:ALBUMARTIST/Queens%20Of%20The%20Stone%20Age"/><r: * EnqueuedTransportURIMetaData val= * "<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="A:ALBUMARTIST/Queens%20Of%20The%20Stone%20Age" parentID="A:ALBUMARTIST" restricted="true"><dc:title>Queens Of The Stone Age</dc:title><upnp:class>object.container</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>" * /> * <PlaybackStorageMedium val="NETWORK"/> * <AVTransportURI val="x-rincon-queue:RINCON_000E5812BC1801400#0"/> * <AVTransportURIMetaData val=""/> * <CurrentTransportActions val="Play, Stop, Pause, Seek, Next, Previous"/> * <TransportStatus val="OK"/> * <r:SleepTimerGeneration val="0"/> * <r:AlarmRunning val="0"/> * <r:SnoozeRunning val="0"/> * <r:RestartPending val="0"/> * <TransportPlaySpeed val="NOT_IMPLEMENTED"/> * <CurrentMediaDuration val="NOT_IMPLEMENTED"/> * <RecordStorageMedium val="NOT_IMPLEMENTED"/> * <PossiblePlaybackStorageMedia val="NONE, NETWORK"/> * <PossibleRecordStorageMedia val="NOT_IMPLEMENTED"/> * <RecordMediumWriteStatus val="NOT_IMPLEMENTED"/> * <CurrentRecordQualityMode val="NOT_IMPLEMENTED"/> * <PossibleRecordQualityModes val="NOT_IMPLEMENTED"/> * <NextAVTransportURI val="NOT_IMPLEMENTED"/> * <NextAVTransportURIMetaData val="NOT_IMPLEMENTED"/> * </InstanceID> * </Event> */ private final Map<String, StateVariableValue> changes = new HashMap<String, StateVariableValue>(); @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { /* * The events are all of the form <qName val="value"/> so we can get all * the info we need from here. */ try { if (atts.getValue("val") != null) { StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); changes.put(localName, stateVariableValue); } } catch (IllegalArgumentException e) { // this means that localName isn't defined in EventType, which is expected for some elements logger.info("{} is not defined in EventType. ", localName); } } public Map<String, StateVariableValue> getChanges() { return changes; } } static private class MetaDataHandler extends DefaultHandler { private CurrentElement currentElement = null; private String id = "-1"; private String parentId = "-1"; private StringBuilder resource = new StringBuilder(); private StringBuilder streamContent = new StringBuilder(); private StringBuilder albumArtUri = new StringBuilder(); private StringBuilder title = new StringBuilder(); private StringBuilder upnpClass = new StringBuilder(); private StringBuilder creator = new StringBuilder(); private StringBuilder album = new StringBuilder(); private StringBuilder albumArtist = new StringBuilder(); @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if ("item".equals(localName)) { currentElement = CurrentElement.item; id = atts.getValue("id"); parentId = atts.getValue("parentID"); } else if ("res".equals(localName)) { currentElement = CurrentElement.res; } else if ("streamContent".equals(localName)) { currentElement = CurrentElement.streamContent; } else if ("albumArtURI".equals(localName)) { currentElement = CurrentElement.albumArtURI; } else if ("title".equals(localName)) { currentElement = CurrentElement.title; } else if ("class".equals(localName)) { currentElement = CurrentElement.upnpClass; } else if ("creator".equals(localName)) { currentElement = CurrentElement.creator; } else if ("album".equals(localName)) { currentElement = CurrentElement.album; } else if ("albumArtist".equals(localName)) { currentElement = CurrentElement.albumArtist; } else { // unknown element currentElement = null; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (currentElement != null) { switch (currentElement) { case item: break; case res: resource.append(ch, start, length); break; case streamContent: streamContent.append(ch, start, length); break; case albumArtURI: albumArtUri.append(ch, start, length); break; case title: title.append(ch, start, length); break; case upnpClass: upnpClass.append(ch, start, length); break; case creator: creator.append(ch, start, length); break; case album: album.append(ch, start, length); break; case albumArtist: albumArtist.append(ch, start, length); break; } } } public SonosMetaData getMetaData() { return new SonosMetaData(id, parentId, resource.toString(), streamContent.toString(), albumArtUri.toString(), title.toString(), upnpClass.toString(), creator.toString(), album.toString(), albumArtist.toString()); } } static private class RenderingControlEventHandler extends DefaultHandler { private final Map<String, StateVariableValue> changes = new HashMap<String, StateVariableValue>(); private boolean getPresetName = false; private String presetName; @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); if ("Volume".equals(qName)) { changes.put(qName + atts.getValue("channel"), stateVariableValue); } else if ("Mute".equals(qName)) { changes.put(qName + atts.getValue("channel"), stateVariableValue); } else if ("Bass".equals(qName)) { changes.put(qName, stateVariableValue); } else if ("Treble".equals(qName)) { changes.put(qName, stateVariableValue); } else if ("Loudness".equals(qName)) { changes.put(qName + atts.getValue("channel"), stateVariableValue); } else if ("OutputFixed".equals(qName)) { changes.put(qName, stateVariableValue); } else if ("PresetNameList".equals(qName)) { getPresetName = true; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (getPresetName) { presetName = new String(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (getPresetName) { getPresetName = false; StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, presetName); changes.put(qName, stateVariableValue); } } public Map<String, StateVariableValue> getChanges() { return changes; } } public static String compileMetadataString(SonosEntry entry) { // if (upnpClass.startsWith("object.container")) { // upnpClass = "object.container"; // } /** * If the entry contains resource meta data we will override this with * that data. */ String id = entry.getId(); String parentId = entry.getParentId(); String title = entry.getTitle(); String upnpClass = entry.getUpnpClass(); /** * By default 'RINCON_AssociatedZPUDN' is used for most operations, * however when playing a favorite entry that is associated withh a * subscription like pandora we need to use the desc string asscoiated * with that item. */ String desc = "RINCON_AssociatedZPUDN"; /** * If resource meta data exists, use it over the parent data */ if (entry.getResourceMetaData() != null) { id = entry.getResourceMetaData().getId(); parentId = entry.getResourceMetaData().getParentId(); title = entry.getResourceMetaData().getTitle(); desc = entry.getResourceMetaData().getDesc(); upnpClass = entry.getResourceMetaData().getUpnpClass(); } String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, desc }); return metadata; } }