2 * Copyright (c) 2010-2023 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.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntry;
31 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryRes;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.xml.sax.Attributes;
35 import org.xml.sax.InputSource;
36 import org.xml.sax.SAXException;
37 import org.xml.sax.helpers.DefaultHandler;
41 * @author Mark Herwege - Initial contribution
42 * @author Karel Goderis - Based on UPnP logic in Sonos binding
45 public class UpnpXMLParser {
47 private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
49 private static final String METADATA_PATTERN = "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
50 + "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
51 + "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
52 + "<item id=\"{0}\" parentID=\"{1}\" restricted=\"true\"><dc:title>{2}</dc:title>"
53 + "<upnp:class>{3}</upnp:class><upnp:album>{4}</upnp:album>"
54 + "<upnp:albumArtURI>{5}</upnp:albumArtURI><dc:creator>{6}</dc:creator>"
55 + "<upnp:artist>{7}</upnp:artist><dc:publisher>{8}</dc:publisher>"
56 + "<upnp:genre>{9}</upnp:genre><upnp:originalTrackNumber>{10}</upnp:originalTrackNumber>"
57 + "</item></DIDL-Lite>";
59 private enum Element {
72 public static Map<String, @Nullable String> getRenderingControlFromXML(String xml) {
74 LOGGER.debug("Could not parse Rendering Control from empty xml");
75 return Collections.emptyMap();
77 RenderingControlEventHandler handler = new RenderingControlEventHandler();
79 SAXParserFactory factory = SAXParserFactory.newInstance();
80 SAXParser saxParser = factory.newSAXParser();
81 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
82 saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
83 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
84 saxParser.parse(new InputSource(new StringReader(xml)), handler);
85 } catch (IOException e) {
86 // This should never happen - we're not performing I/O!
87 LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
88 } catch (SAXException | ParserConfigurationException s) {
89 LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
91 return handler.getChanges();
94 private static class RenderingControlEventHandler extends DefaultHandler {
96 private final Map<String, @Nullable String> changes = new HashMap<>();
98 RenderingControlEventHandler() {
99 // shouldn't be used outside of this package.
103 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
104 @Nullable Attributes attributes) throws SAXException {
112 String channel = attributes == null ? null : attributes.getValue("channel");
113 String val = attributes == null ? null : attributes.getValue("val");
114 if (channel != null && val != null) {
115 changes.put(channel + qName, val);
119 if ((attributes != null) && (attributes.getValue("val") != null)) {
120 changes.put(qName, attributes.getValue("val"));
126 public Map<String, @Nullable String> getChanges() {
131 public static Map<String, String> getAVTransportFromXML(String xml) {
133 LOGGER.debug("Could not parse AV Transport from empty xml");
134 return Collections.emptyMap();
136 AVTransportEventHandler handler = new AVTransportEventHandler();
138 SAXParserFactory factory = SAXParserFactory.newInstance();
139 SAXParser saxParser = factory.newSAXParser();
140 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
141 saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
142 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
143 saxParser.parse(new InputSource(new StringReader(xml)), handler);
144 } catch (IOException e) {
145 // This should never happen - we're not performing I/O!
146 LOGGER.error("Could not parse AV Transport from string '{}'", xml, e);
147 } catch (SAXException | ParserConfigurationException s) {
148 LOGGER.debug("Could not parse AV Transport from string '{}'", xml, s);
150 return handler.getChanges();
153 private static class AVTransportEventHandler extends DefaultHandler {
155 private final Map<String, String> changes = new HashMap<String, String>();
157 AVTransportEventHandler() {
158 // shouldn't be used outside of this package.
162 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
163 @Nullable Attributes attributes) throws SAXException {
165 * The events are all of the form <qName val="value"/> so we can get all
166 * the info we need from here.
168 if ((qName != null) && (attributes != null) && (attributes.getValue("val") != null)) {
169 changes.put(qName, attributes.getValue("val"));
173 public Map<String, String> getChanges() {
178 public static List<UpnpEntry> getEntriesFromXML(String xml) {
180 LOGGER.debug("Could not parse Entries from empty xml");
181 return Collections.emptyList();
183 EntryHandler handler = new EntryHandler();
185 SAXParserFactory factory = SAXParserFactory.newInstance();
186 SAXParser saxParser = factory.newSAXParser();
187 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
188 saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
189 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
190 saxParser.parse(new InputSource(new StringReader(xml)), handler);
191 } catch (IOException e) {
192 // This should never happen - we're not performing I/O!
193 LOGGER.error("Could not parse Entries from string '{}'", xml, e);
194 } catch (SAXException | ParserConfigurationException s) {
195 LOGGER.debug("Could not parse Entries from string '{}'", xml, s);
197 return handler.getEntries();
200 private static class EntryHandler extends DefaultHandler {
202 // Maintain a set of elements it is not useful to complain about.
203 // This list will be initialized on the first failure case.
204 private static List<String> ignore = new ArrayList<String>();
206 private String id = "";
207 private String refId = "";
208 private String parentId = "";
209 private StringBuilder upnpClass = new StringBuilder();
210 private List<UpnpEntryRes> resList = new ArrayList<>();
211 private StringBuilder res = new StringBuilder();
212 private StringBuilder title = new StringBuilder();
213 private StringBuilder album = new StringBuilder();
214 private StringBuilder albumArtUri = new StringBuilder();
215 private StringBuilder creator = new StringBuilder();
216 private StringBuilder artist = new StringBuilder();
217 private List<String> artistList = new ArrayList<>();
218 private StringBuilder publisher = new StringBuilder();
219 private StringBuilder genre = new StringBuilder();
220 private StringBuilder trackNumber = new StringBuilder();
221 private @Nullable Element element = null;
223 private List<UpnpEntry> entries = new ArrayList<>();
226 // shouldn't be used outside of this package.
230 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
231 @Nullable Attributes attributes) throws SAXException {
239 if (attributes != null) {
240 if (attributes.getValue("id") != null) {
241 id = attributes.getValue("id");
243 if (attributes.getValue("refID") != null) {
244 refId = attributes.getValue("refID");
246 if (attributes.getValue("parentID") != null) {
247 parentId = attributes.getValue("parentID");
252 if (attributes != null) {
253 String protocolInfo = attributes.getValue("protocolInfo");
256 size = Long.parseLong(attributes.getValue("size"));
257 } catch (NumberFormatException e) {
260 String duration = attributes.getValue("duration");
261 String importUri = attributes.getValue("importUri");
262 resList.add(0, new UpnpEntryRes(protocolInfo, size, duration, importUri));
263 element = Element.RES;
267 element = Element.TITLE;
270 element = Element.CLASS;
273 element = Element.CREATOR;
276 element = Element.ARTIST;
279 element = Element.PUBLISHER;
282 element = Element.GENRE;
285 element = Element.ALBUM;
287 case "upnp:albumArtURI":
288 element = Element.ALBUM_ART_URI;
290 case "upnp:originalTrackNumber":
291 element = Element.TRACK_NUMBER;
294 if (ignore.isEmpty()) {
296 ignore.add("DIDL-Lite");
298 ignore.add("ordinal");
299 ignore.add("description");
300 ignore.add("writeStatus");
301 ignore.add("storageUsed");
302 ignore.add("supported");
303 ignore.add("pushSource");
305 ignore.add("playlist");
307 ignore.add("rating");
308 ignore.add("userrating");
309 ignore.add("episodeSeason");
310 ignore.add("childCountContainer");
311 ignore.add("modificationTime");
312 ignore.add("containerContent");
314 if (!ignore.contains(localName)) {
315 LOGGER.debug("Did not recognise element named {}", localName);
322 public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
323 Element el = element;
324 if (el == null || ch == null) {
329 title.append(ch, start, length);
332 upnpClass.append(ch, start, length);
335 res.append(ch, start, length);
338 album.append(ch, start, length);
341 albumArtUri.append(ch, start, length);
344 creator.append(ch, start, length);
347 artist.append(ch, start, length);
350 publisher.append(ch, start, length);
353 genre.append(ch, start, length);
356 trackNumber.append(ch, start, length);
362 public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
363 throws SAXException {
364 if ("container".equals(qName) || "item".equals(qName)) {
367 Integer trackNumberVal;
369 trackNumberVal = Integer.parseInt(trackNumber.toString());
370 } catch (NumberFormatException e) {
371 trackNumberVal = null;
374 entries.add(new UpnpEntry(id, refId, parentId, upnpClass.toString()).withTitle(title.toString())
375 .withAlbum(album.toString()).withAlbumArtUri(albumArtUri.toString())
376 .withCreator(creator.toString())
377 .withArtist(!artistList.isEmpty() ? artistList.get(0) : artist.toString())
378 .withPublisher(publisher.toString()).withGenre(genre.toString()).withTrackNumber(trackNumberVal)
379 .withResList(resList));
381 title = new StringBuilder();
382 upnpClass = new StringBuilder();
383 resList = new ArrayList<>();
384 album = new StringBuilder();
385 albumArtUri = new StringBuilder();
386 creator = new StringBuilder();
387 artistList = new ArrayList<>();
388 publisher = new StringBuilder();
389 genre = new StringBuilder();
390 trackNumber = new StringBuilder();
391 } else if ("res".equals(qName)) {
392 resList.get(0).setRes(res.toString());
393 res = new StringBuilder();
394 } else if ("upnp:artist".equals(qName)) {
395 artistList.add(artist.toString());
396 artist = new StringBuilder();
400 public List<UpnpEntry> getEntries() {
405 public static String compileMetadataString(UpnpEntry entry) {
406 String id = entry.getId();
407 String parentId = entry.getParentId();
408 String title = StringUtils.escapeXml(entry.getTitle());
409 String upnpClass = entry.getUpnpClass();
410 String album = StringUtils.escapeXml(entry.getAlbum());
411 String albumArtUri = entry.getAlbumArtUri();
412 String creator = StringUtils.escapeXml(entry.getCreator());
413 String artist = StringUtils.escapeXml(entry.getArtist());
414 String publisher = StringUtils.escapeXml(entry.getPublisher());
415 String genre = StringUtils.escapeXml(entry.getGenre());
416 Integer trackNumber = entry.getOriginalTrackNumber();
418 final MessageFormat messageFormat = new MessageFormat(METADATA_PATTERN);
419 String metadata = messageFormat.format(new Object[] { id, parentId, title, upnpClass, album, albumArtUri,
420 creator, artist, publisher, genre, trackNumber });