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;
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.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.xml.sax.Attributes;
34 import org.xml.sax.InputSource;
35 import org.xml.sax.SAXException;
36 import org.xml.sax.helpers.DefaultHandler;
40 * @author Mark Herwege - Initial contribution
41 * @author Karel Goderis - Based on UPnP logic in Sonos binding
44 public class UpnpXMLParser {
46 private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
48 private static final MessageFormat METADATA_FORMAT = new MessageFormat(
49 "<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, String> getAVTransportFromXML(String xml) {
74 LOGGER.debug("Could not parse AV Transport from empty xml");
75 return Collections.emptyMap();
77 AVTransportEventHandler handler = new AVTransportEventHandler();
79 SAXParserFactory factory = SAXParserFactory.newInstance();
80 SAXParser saxParser = factory.newSAXParser();
81 saxParser.parse(new InputSource(new StringReader(xml)), handler);
82 } catch (IOException e) {
83 // This should never happen - we're not performing I/O!
84 LOGGER.error("Could not parse AV Transport from string '{}'", xml, e);
85 } catch (SAXException | ParserConfigurationException s) {
86 LOGGER.debug("Could not parse AV Transport from string '{}'", xml, s);
88 return handler.getChanges();
93 * @return a list of Entries from the given xml string.
95 * @throws SAXException
97 public static List<UpnpEntry> getEntriesFromXML(String xml) {
99 LOGGER.debug("Could not parse Entries from empty xml");
100 return Collections.emptyList();
102 EntryHandler handler = new EntryHandler();
104 SAXParserFactory factory = SAXParserFactory.newInstance();
105 SAXParser saxParser = factory.newSAXParser();
106 saxParser.parse(new InputSource(new StringReader(xml)), handler);
107 } catch (IOException e) {
108 // This should never happen - we're not performing I/O!
109 LOGGER.error("Could not parse Entries from string '{}'", xml, e);
110 } catch (SAXException | ParserConfigurationException s) {
111 LOGGER.debug("Could not parse Entries from string '{}'", xml, s);
113 return handler.getEntries();
116 private static class AVTransportEventHandler extends DefaultHandler {
118 private final Map<String, String> changes = new HashMap<String, String>();
120 AVTransportEventHandler() {
121 // shouldn't be used outside of this package.
125 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
126 @Nullable Attributes atts) throws SAXException {
128 * The events are all of the form <qName val="value"/> so we can get all
129 * the info we need from here.
131 if ((qName != null) && (atts != null) && (atts.getValue("val") != null)) {
132 changes.put(qName, atts.getValue("val"));
136 public Map<String, String> getChanges() {
141 private static class EntryHandler extends DefaultHandler {
143 // Maintain a set of elements it is not useful to complain about.
144 // This list will be initialized on the first failure case.
145 private static List<String> ignore = new ArrayList<String>();
147 private String id = "";
148 private String refId = "";
149 private String parentId = "";
150 private StringBuilder upnpClass = new StringBuilder();
151 private List<UpnpEntryRes> resList = new ArrayList<>();
152 private StringBuilder res = new StringBuilder();
153 private StringBuilder title = new StringBuilder();
154 private StringBuilder album = new StringBuilder();
155 private StringBuilder albumArtUri = new StringBuilder();
156 private StringBuilder creator = new StringBuilder();
157 private StringBuilder artist = new StringBuilder();
158 private List<String> artistList = new ArrayList<>();
159 private StringBuilder publisher = new StringBuilder();
160 private StringBuilder genre = new StringBuilder();
161 private StringBuilder trackNumber = new StringBuilder();
162 private @Nullable Element element = null;
164 private List<UpnpEntry> entries = new ArrayList<>();
167 // shouldn't be used outside of this package.
171 public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
172 @Nullable Attributes attributes) throws SAXException {
180 if (attributes != null) {
181 if (attributes.getValue("id") != null) {
182 id = attributes.getValue("id");
184 if (attributes.getValue("refID") != null) {
185 refId = attributes.getValue("refID");
187 if (attributes.getValue("parentID") != null) {
188 parentId = attributes.getValue("parentID");
193 if (attributes != null) {
194 String protocolInfo = attributes.getValue("protocolInfo");
197 size = Long.parseLong(attributes.getValue("size"));
198 } catch (NumberFormatException e) {
201 String duration = attributes.getValue("duration");
202 String importUri = attributes.getValue("importUri");
203 resList.add(0, new UpnpEntryRes(protocolInfo, size, duration, importUri));
204 element = Element.RES;
208 element = Element.TITLE;
211 element = Element.CLASS;
214 element = Element.CREATOR;
217 element = Element.ARTIST;
220 element = Element.PUBLISHER;
223 element = Element.GENRE;
226 element = Element.ALBUM;
228 case "upnp:albumArtURI":
229 element = Element.ALBUM_ART_URI;
231 case "upnp:originalTrackNumber":
232 element = Element.TRACK_NUMBER;
235 if (ignore.isEmpty()) {
237 ignore.add("DIDL-Lite");
239 ignore.add("ordinal");
240 ignore.add("description");
241 ignore.add("writeStatus");
242 ignore.add("storageUsed");
243 ignore.add("supported");
244 ignore.add("pushSource");
246 ignore.add("playlist");
248 ignore.add("rating");
249 ignore.add("userrating");
250 ignore.add("episodeSeason");
251 ignore.add("childCountContainer");
252 ignore.add("modificationTime");
253 ignore.add("containerContent");
255 if (!ignore.contains(localName)) {
256 LOGGER.debug("Did not recognise element named {}", localName);
263 public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
264 Element el = element;
270 title.append(ch, start, length);
273 upnpClass.append(ch, start, length);
276 res.append(ch, start, length);
279 album.append(ch, start, length);
282 albumArtUri.append(ch, start, length);
285 creator.append(ch, start, length);
288 artist.append(ch, start, length);
291 publisher.append(ch, start, length);
294 genre.append(ch, start, length);
297 trackNumber.append(ch, start, length);
303 public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
304 throws SAXException {
305 if ("container".equals(qName) || "item".equals(qName)) {
308 Integer trackNumberVal;
310 trackNumberVal = Integer.parseInt(trackNumber.toString());
311 } catch (NumberFormatException e) {
312 trackNumberVal = null;
315 entries.add(new UpnpEntry(id, refId, parentId, upnpClass.toString()).withTitle(title.toString())
316 .withAlbum(album.toString()).withAlbumArtUri(albumArtUri.toString())
317 .withCreator(creator.toString())
318 .withArtist(artistList.size() > 0 ? artistList.get(0) : artist.toString())
319 .withPublisher(publisher.toString()).withGenre(genre.toString()).withTrackNumber(trackNumberVal)
320 .withResList(resList));
322 title = new StringBuilder();
323 upnpClass = new StringBuilder();
324 resList = new ArrayList<>();
325 album = new StringBuilder();
326 albumArtUri = new StringBuilder();
327 creator = new StringBuilder();
328 artistList = new ArrayList<>();
329 publisher = new StringBuilder();
330 genre = new StringBuilder();
331 trackNumber = new StringBuilder();
332 } else if ("res".equals(qName)) {
333 resList.get(0).setRes(res.toString());
334 res = new StringBuilder();
335 } else if ("upnp:artist".equals(qName)) {
336 artistList.add(artist.toString());
337 artist = new StringBuilder();
341 public List<UpnpEntry> getEntries() {
346 public static String compileMetadataString(UpnpEntry entry) {
347 String id = entry.getId();
348 String parentId = entry.getParentId();
349 String title = StringEscapeUtils.escapeXml(entry.getTitle());
350 String upnpClass = entry.getUpnpClass();
351 String album = StringEscapeUtils.escapeXml(entry.getAlbum());
352 String albumArtUri = entry.getAlbumArtUri();
353 String creator = StringEscapeUtils.escapeXml(entry.getCreator());
354 String artist = StringEscapeUtils.escapeXml(entry.getArtist());
355 String publisher = StringEscapeUtils.escapeXml(entry.getPublisher());
356 String genre = StringEscapeUtils.escapeXml(entry.getGenre());
357 Integer trackNumber = entry.getOriginalTrackNumber();
359 String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, album, albumArtUri,
360 creator, artist, publisher, genre, trackNumber });