2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.upnpcontrol.internal.util;
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.text.MessageFormat;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.List;
24 import javax.xml.parsers.ParserConfigurationException;
25 import javax.xml.parsers.SAXParser;
26 import javax.xml.parsers.SAXParserFactory;
28 import org.apache.commons.lang.StringEscapeUtils;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntry;
32 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryRes;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.xml.sax.Attributes;
36 import org.xml.sax.InputSource;
37 import org.xml.sax.SAXException;
38 import org.xml.sax.helpers.DefaultHandler;
42 * @author Mark Herwege - Initial contribution
43 * @author Karel Goderis - Based on UPnP logic in Sonos binding
46 public class UpnpXMLParser {
48 private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
50 private static final String METADATA_PATTERN = "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
51 + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
52 + "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
53 + "<item id=\"{0}\" parentID=\"{1}\" restricted=\"true\"><dc:title>{2}</dc:title>"
54 + "<upnp:class>{3}</upnp:class><upnp:album>{4}</upnp:album>"
55 + "<upnp:albumArtURI>{5}</upnp:albumArtURI><dc:creator>{6}</dc:creator>"
56 + "<upnp:artist>{7}</upnp:artist><dc:publisher>{8}</dc:publisher>"
57 + "<upnp:genre>{9}</upnp:genre><upnp:originalTrackNumber>{10}</upnp:originalTrackNumber>"
58 + "</item></DIDL-Lite>";
60 private enum Element {
73 public static Map<String, @Nullable String> getRenderingControlFromXML(String xml) {
75 LOGGER.debug("Could not parse Rendering Control from empty xml");
76 return Collections.emptyMap();
78 RenderingControlEventHandler handler = new RenderingControlEventHandler();
80 SAXParserFactory factory = SAXParserFactory.newInstance();
81 SAXParser saxParser = factory.newSAXParser();
82 saxParser.parse(new InputSource(new StringReader(xml)), handler);
83 } catch (IOException e) {
84 // This should never happen - we're not performing I/O!
85 LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
86 } catch (SAXException | ParserConfigurationException s) {
87 LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
89 return handler.getChanges();
92 private static class RenderingControlEventHandler extends DefaultHandler {
94 private final Map<String, @Nullable String> changes = new HashMap<>();
96 RenderingControlEventHandler() {
97 // shouldn't be used outside of this package.
101 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
102 @Nullable Attributes attributes) throws SAXException {
110 String channel = attributes == null ? null : attributes.getValue("channel");
111 String val = attributes == null ? null : attributes.getValue("val");
112 if (channel != null && val != null) {
113 changes.put(channel + qName, val);
117 if ((attributes != null) && (attributes.getValue("val") != null)) {
118 changes.put(qName, attributes.getValue("val"));
124 public Map<String, @Nullable String> getChanges() {
129 public static Map<String, String> getAVTransportFromXML(String xml) {
131 LOGGER.debug("Could not parse AV Transport from empty xml");
132 return Collections.emptyMap();
134 AVTransportEventHandler handler = new AVTransportEventHandler();
136 SAXParserFactory factory = SAXParserFactory.newInstance();
137 SAXParser saxParser = factory.newSAXParser();
138 saxParser.parse(new InputSource(new StringReader(xml)), handler);
139 } catch (IOException e) {
140 // This should never happen - we're not performing I/O!
141 LOGGER.error("Could not parse AV Transport from string '{}'", xml, e);
142 } catch (SAXException | ParserConfigurationException s) {
143 LOGGER.debug("Could not parse AV Transport from string '{}'", xml, s);
145 return handler.getChanges();
148 private static class AVTransportEventHandler extends DefaultHandler {
150 private final Map<String, String> changes = new HashMap<String, String>();
152 AVTransportEventHandler() {
153 // shouldn't be used outside of this package.
157 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
158 @Nullable Attributes attributes) throws SAXException {
160 * The events are all of the form <qName val="value"/> so we can get all
161 * the info we need from here.
163 if ((qName != null) && (attributes != null) && (attributes.getValue("val") != null)) {
164 changes.put(qName, attributes.getValue("val"));
168 public Map<String, String> getChanges() {
173 public static List<UpnpEntry> getEntriesFromXML(String xml) {
175 LOGGER.debug("Could not parse Entries from empty xml");
176 return Collections.emptyList();
178 EntryHandler handler = new EntryHandler();
180 SAXParserFactory factory = SAXParserFactory.newInstance();
181 SAXParser saxParser = factory.newSAXParser();
182 saxParser.parse(new InputSource(new StringReader(xml)), handler);
183 } catch (IOException e) {
184 // This should never happen - we're not performing I/O!
185 LOGGER.error("Could not parse Entries from string '{}'", xml, e);
186 } catch (SAXException | ParserConfigurationException s) {
187 LOGGER.debug("Could not parse Entries from string '{}'", xml, s);
189 return handler.getEntries();
192 private static class EntryHandler extends DefaultHandler {
194 // Maintain a set of elements it is not useful to complain about.
195 // This list will be initialized on the first failure case.
196 private static List<String> ignore = new ArrayList<String>();
198 private String id = "";
199 private String refId = "";
200 private String parentId = "";
201 private StringBuilder upnpClass = new StringBuilder();
202 private List<UpnpEntryRes> resList = new ArrayList<>();
203 private StringBuilder res = new StringBuilder();
204 private StringBuilder title = new StringBuilder();
205 private StringBuilder album = new StringBuilder();
206 private StringBuilder albumArtUri = new StringBuilder();
207 private StringBuilder creator = new StringBuilder();
208 private StringBuilder artist = new StringBuilder();
209 private List<String> artistList = new ArrayList<>();
210 private StringBuilder publisher = new StringBuilder();
211 private StringBuilder genre = new StringBuilder();
212 private StringBuilder trackNumber = new StringBuilder();
213 private @Nullable Element element = null;
215 private List<UpnpEntry> entries = new ArrayList<>();
218 // shouldn't be used outside of this package.
222 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
223 @Nullable Attributes attributes) throws SAXException {
231 if (attributes != null) {
232 if (attributes.getValue("id") != null) {
233 id = attributes.getValue("id");
235 if (attributes.getValue("refID") != null) {
236 refId = attributes.getValue("refID");
238 if (attributes.getValue("parentID") != null) {
239 parentId = attributes.getValue("parentID");
244 if (attributes != null) {
245 String protocolInfo = attributes.getValue("protocolInfo");
248 size = Long.parseLong(attributes.getValue("size"));
249 } catch (NumberFormatException e) {
252 String duration = attributes.getValue("duration");
253 String importUri = attributes.getValue("importUri");
254 resList.add(0, new UpnpEntryRes(protocolInfo, size, duration, importUri));
255 element = Element.RES;
259 element = Element.TITLE;
262 element = Element.CLASS;
265 element = Element.CREATOR;
268 element = Element.ARTIST;
271 element = Element.PUBLISHER;
274 element = Element.GENRE;
277 element = Element.ALBUM;
279 case "upnp:albumArtURI":
280 element = Element.ALBUM_ART_URI;
282 case "upnp:originalTrackNumber":
283 element = Element.TRACK_NUMBER;
286 if (ignore.isEmpty()) {
288 ignore.add("DIDL-Lite");
290 ignore.add("ordinal");
291 ignore.add("description");
292 ignore.add("writeStatus");
293 ignore.add("storageUsed");
294 ignore.add("supported");
295 ignore.add("pushSource");
297 ignore.add("playlist");
299 ignore.add("rating");
300 ignore.add("userrating");
301 ignore.add("episodeSeason");
302 ignore.add("childCountContainer");
303 ignore.add("modificationTime");
304 ignore.add("containerContent");
306 if (!ignore.contains(localName)) {
307 LOGGER.debug("Did not recognise element named {}", localName);
314 public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
315 Element el = element;
316 if (el == null || ch == null) {
321 title.append(ch, start, length);
324 upnpClass.append(ch, start, length);
327 res.append(ch, start, length);
330 album.append(ch, start, length);
333 albumArtUri.append(ch, start, length);
336 creator.append(ch, start, length);
339 artist.append(ch, start, length);
342 publisher.append(ch, start, length);
345 genre.append(ch, start, length);
348 trackNumber.append(ch, start, length);
354 public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
355 throws SAXException {
356 if ("container".equals(qName) || "item".equals(qName)) {
359 Integer trackNumberVal;
361 trackNumberVal = Integer.parseInt(trackNumber.toString());
362 } catch (NumberFormatException e) {
363 trackNumberVal = null;
366 entries.add(new UpnpEntry(id, refId, parentId, upnpClass.toString()).withTitle(title.toString())
367 .withAlbum(album.toString()).withAlbumArtUri(albumArtUri.toString())
368 .withCreator(creator.toString())
369 .withArtist(artistList.size() > 0 ? artistList.get(0) : artist.toString())
370 .withPublisher(publisher.toString()).withGenre(genre.toString()).withTrackNumber(trackNumberVal)
371 .withResList(resList));
373 title = new StringBuilder();
374 upnpClass = new StringBuilder();
375 resList = new ArrayList<>();
376 album = new StringBuilder();
377 albumArtUri = new StringBuilder();
378 creator = new StringBuilder();
379 artistList = new ArrayList<>();
380 publisher = new StringBuilder();
381 genre = new StringBuilder();
382 trackNumber = new StringBuilder();
383 } else if ("res".equals(qName)) {
384 resList.get(0).setRes(res.toString());
385 res = new StringBuilder();
386 } else if ("upnp:artist".equals(qName)) {
387 artistList.add(artist.toString());
388 artist = new StringBuilder();
392 public List<UpnpEntry> getEntries() {
397 public static String compileMetadataString(UpnpEntry entry) {
398 String id = entry.getId();
399 String parentId = entry.getParentId();
400 String title = StringEscapeUtils.escapeXml(entry.getTitle());
401 String upnpClass = entry.getUpnpClass();
402 String album = StringEscapeUtils.escapeXml(entry.getAlbum());
403 String albumArtUri = entry.getAlbumArtUri();
404 String creator = StringEscapeUtils.escapeXml(entry.getCreator());
405 String artist = StringEscapeUtils.escapeXml(entry.getArtist());
406 String publisher = StringEscapeUtils.escapeXml(entry.getPublisher());
407 String genre = StringEscapeUtils.escapeXml(entry.getGenre());
408 Integer trackNumber = entry.getOriginalTrackNumber();
410 final MessageFormat messageFormat = new MessageFormat(METADATA_PATTERN);
411 String metadata = messageFormat.format(new Object[] { id, parentId, title, upnpClass, album, albumArtUri,
412 creator, artist, publisher, genre, trackNumber });