/** * Copyright (c) 2014-2017 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 */ package org.eclipse.smarthome.binding.sonos.internal; import java.io.IOException; import java.io.StringReader; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; /** * The {@link SonosXMLParser} is a class of helper functions * to parse XML data returned by the Zone Players * * @author Karel Goderis - Initial contribution */ 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) { AlarmHandler handler = new AlarmHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Alarms from string '{}'", xml); } catch (SAXException s) { logger.error("Could not parse Alarms from string '{}'", xml); } return handler.getAlarms(); } /** * @param xml * @return a list of Entries from the given xml string. * @throws IOException * @throws SAXException */ public static List<SonosEntry> getEntriesFromString(String xml) { EntryHandler handler = new EntryHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Entries from string '{}'", xml); } catch (SAXException s) { 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 getResourceMetaData(String xml) throws SAXException { XMLReader reader = XMLReaderFactory.createXMLReader(); ResourceMetaDataHandler handler = new ResourceMetaDataHandler(); reader.setContentHandler(handler); try { reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { logger.error("Could not parse Resource MetaData from String '{}'", xml); } catch (SAXException s) { logger.error("Could not parse Resource MetaData 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) { ZoneGroupHandler handler = new ZoneGroupHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); 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); } catch (SAXException s) { logger.error("Could not parse ZoneGroup from string '{}'", xml); } return handler.getGroups(); } public static List<String> getRadioTimeFromXML(String xml) { OpmlHandler handler = new OpmlHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); 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); } catch (SAXException s) { logger.error("Could not parse RadioTime from string '{}'", xml); } return handler.getTextFields(); } public static Map<String, String> getRenderingControlFromXML(String xml) { RenderingControlEventHandler handler = new RenderingControlEventHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); 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 Rendering Control from string '{}'", xml); } catch (SAXException s) { logger.error("Could not parse Rendering Control from string '{}'", xml); } return handler.getChanges(); } public static Map<String, String> getAVTransportFromXML(String xml) { AVTransportEventHandler handler = new AVTransportEventHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); 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 from string '{}'", xml); } catch (SAXException s) { logger.error("Could not parse AV Transport from string '{}'", xml); } return handler.getChanges(); } public static SonosMetaData getMetaDataFromXML(String xml) { MetaDataHandler handler = new MetaDataHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(handler); 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 MetaData from string '{}'", xml); } catch (SAXException s) { logger.error("Could not parse MetaData from string '{}'", xml); } 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.debug("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; case DESC: break; } } @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 if (!desc.toString().isEmpty()) { try { md = getResourceMetaData(desc.toString()); } catch (SAXException ignore) { logger.debug("Failed to parse embeded", 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 ResourceMetaDataHandler 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; ResourceMetaDataHandler() { // 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; 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"); } alarms.add(new SonosAlarm(finalID, startTime, duration, 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 final List<String> currentGroupPlayerZones = 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")); String zoneName = attributes.getValue("ZoneName"); if (zoneName != null) { currentGroupPlayerZones.add(zoneName); } String htInfoSet = attributes.getValue("HTSatChanMapSet"); if (htInfoSet != null) { currentGroupPlayers.addAll(getAllHomeTheaterMembers(htInfoSet)); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("ZoneGroup")) { groups.add(new SonosZoneGroup(groupId, coordinator, currentGroupPlayers, currentGroupPlayerZones)); currentGroupPlayers.clear(); currentGroupPlayerZones.clear(); } } public List<SonosZoneGroup> getGroups() { return groups; } private Set<String> getAllHomeTheaterMembers(String homeTheaterDescription) { Set<String> homeTheaterMembers = new HashSet<String>(); Matcher matcher = Pattern.compile("(RINCON_\\w+)").matcher(homeTheaterDescription); while (matcher.find()) { String member = matcher.group(); homeTheaterMembers.add(member); } return homeTheaterMembers; } } 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, String> changes = new HashMap<String, String>(); @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) { changes.put(localName, atts.getValue("val")); } } 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, String> 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; case desc: 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, String> changes = new HashMap<String, String>(); private boolean getPresetName = false; private String presetName; @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if ("Volume".equals(qName)) { changes.put(qName + atts.getValue("channel"), atts.getValue("val")); } else if ("Mute".equals(qName)) { changes.put(qName + atts.getValue("channel"), atts.getValue("val")); } else if ("Bass".equals(qName)) { changes.put(qName, atts.getValue("val")); } else if ("Treble".equals(qName)) { changes.put(qName, atts.getValue("val")); } else if ("Loudness".equals(qName)) { changes.put(qName + atts.getValue("channel"), atts.getValue("val")); } else if ("OutputFixed".equals(qName)) { changes.put(qName, atts.getValue("val")); } 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; changes.put(qName, presetName); } } public Map<String, String> getChanges() { return changes; } } public static String getRoomName(String descriptorXML) { RoomNameHandler roomNameHandler = new RoomNameHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(roomNameHandler); URL url = new URL(descriptorXML); reader.parse(new InputSource(url.openStream())); } catch (IOException | SAXException e) { logger.error("Could not parse Sonos room name from string '{}'", descriptorXML); } return roomNameHandler.getRoomName(); } static private class RoomNameHandler extends DefaultHandler { private String roomName; private boolean roomNameTag; @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if ("roomName".equalsIgnoreCase(localName)) { roomNameTag = true; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (roomNameTag) { roomName = new String(ch, start, length); roomNameTag = false; } } public String getRoomName() { return roomName; } } public static String parseModelDescription(URL descriptorURL) { ModelNameHandler modelNameHandler = new ModelNameHandler(); try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(modelNameHandler); URL url = new URL(descriptorURL.toString()); reader.parse(new InputSource(url.openStream())); } catch (IOException | SAXException e) { logger.error("Could not parse Sonos model name from string '{}'", descriptorURL.toString()); } return modelNameHandler.getModelName(); } static private class ModelNameHandler extends DefaultHandler { private String modelName; private boolean modelNameTag; @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if ("modelName".equalsIgnoreCase(localName)) { modelNameTag = true; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (modelNameTag) { modelName = new String(ch, start, length); modelNameTag = false; } } public String getModelName() { return modelName; } } /** * The model name provided by upnp is formated like in the example form "Sonos PLAY:1" or "Sonos PLAYBAR" * * @param sonosModelName Sonos model name provided via upnp device * @return the extracted players model name without column (:) character used for ThingType creation */ public static String extractModelName(String sonosModelName) { // Matcher matcher = Pattern.compile("\\s(.*)").matcher(sonosModelName); if (matcher.find()) { sonosModelName = matcher.group(1); } if (sonosModelName.contains(":")) { sonosModelName = sonosModelName.replace(":", ""); } return sonosModelName; } public static String compileMetadataString(SonosEntry entry) { /** * 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; } }