]> git.basschouten.com Git - openhab-addons.git/blob
2fa703dfb058367992e6c20a90d1ab4f12a9ecfc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.upnpcontrol.internal.util;
14
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;
22 import java.util.Map;
23
24 import javax.xml.parsers.ParserConfigurationException;
25 import javax.xml.parsers.SAXParser;
26 import javax.xml.parsers.SAXParserFactory;
27
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;
39
40 /**
41  *
42  * @author Mark Herwege - Initial contribution
43  * @author Karel Goderis - Based on UPnP logic in Sonos binding
44  */
45 @NonNullByDefault
46 public class UpnpXMLParser {
47
48     private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
49
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>";
59
60     private enum Element {
61         TITLE,
62         CLASS,
63         ALBUM,
64         ALBUM_ART_URI,
65         CREATOR,
66         ARTIST,
67         PUBLISHER,
68         GENRE,
69         TRACK_NUMBER,
70         RES
71     }
72
73     public static Map<String, @Nullable String> getRenderingControlFromXML(String xml) {
74         if (xml.isEmpty()) {
75             LOGGER.debug("Could not parse Rendering Control from empty xml");
76             return Collections.emptyMap();
77         }
78         RenderingControlEventHandler handler = new RenderingControlEventHandler();
79         try {
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);
88         }
89         return handler.getChanges();
90     }
91
92     private static class RenderingControlEventHandler extends DefaultHandler {
93
94         private final Map<String, @Nullable String> changes = new HashMap<>();
95
96         RenderingControlEventHandler() {
97             // shouldn't be used outside of this package.
98         }
99
100         @Override
101         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
102                 @Nullable Attributes attributes) throws SAXException {
103             if (qName == null) {
104                 return;
105             }
106             switch (qName) {
107                 case "Volume":
108                 case "Mute":
109                 case "Loudness":
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);
114                     }
115                     break;
116                 default:
117                     if ((attributes != null) && (attributes.getValue("val") != null)) {
118                         changes.put(qName, attributes.getValue("val"));
119                     }
120                     break;
121             }
122         }
123
124         public Map<String, @Nullable String> getChanges() {
125             return changes;
126         }
127     }
128
129     public static Map<String, String> getAVTransportFromXML(String xml) {
130         if (xml.isEmpty()) {
131             LOGGER.debug("Could not parse AV Transport from empty xml");
132             return Collections.emptyMap();
133         }
134         AVTransportEventHandler handler = new AVTransportEventHandler();
135         try {
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);
144         }
145         return handler.getChanges();
146     }
147
148     private static class AVTransportEventHandler extends DefaultHandler {
149
150         private final Map<String, String> changes = new HashMap<String, String>();
151
152         AVTransportEventHandler() {
153             // shouldn't be used outside of this package.
154         }
155
156         @Override
157         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
158                 @Nullable Attributes attributes) throws SAXException {
159             /*
160              * The events are all of the form <qName val="value"/> so we can get all
161              * the info we need from here.
162              */
163             if ((qName != null) && (attributes != null) && (attributes.getValue("val") != null)) {
164                 changes.put(qName, attributes.getValue("val"));
165             }
166         }
167
168         public Map<String, String> getChanges() {
169             return changes;
170         }
171     }
172
173     public static List<UpnpEntry> getEntriesFromXML(String xml) {
174         if (xml.isEmpty()) {
175             LOGGER.debug("Could not parse Entries from empty xml");
176             return Collections.emptyList();
177         }
178         EntryHandler handler = new EntryHandler();
179         try {
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);
188         }
189         return handler.getEntries();
190     }
191
192     private static class EntryHandler extends DefaultHandler {
193
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>();
197
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;
214
215         private List<UpnpEntry> entries = new ArrayList<>();
216
217         EntryHandler() {
218             // shouldn't be used outside of this package.
219         }
220
221         @Override
222         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
223                 @Nullable Attributes attributes) throws SAXException {
224             if (qName == null) {
225                 element = null;
226                 return;
227             }
228             switch (qName) {
229                 case "container":
230                 case "item":
231                     if (attributes != null) {
232                         if (attributes.getValue("id") != null) {
233                             id = attributes.getValue("id");
234                         }
235                         if (attributes.getValue("refID") != null) {
236                             refId = attributes.getValue("refID");
237                         }
238                         if (attributes.getValue("parentID") != null) {
239                             parentId = attributes.getValue("parentID");
240                         }
241                     }
242                     break;
243                 case "res":
244                     if (attributes != null) {
245                         String protocolInfo = attributes.getValue("protocolInfo");
246                         Long size;
247                         try {
248                             size = Long.parseLong(attributes.getValue("size"));
249                         } catch (NumberFormatException e) {
250                             size = null;
251                         }
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;
256                     }
257                     break;
258                 case "dc:title":
259                     element = Element.TITLE;
260                     break;
261                 case "upnp:class":
262                     element = Element.CLASS;
263                     break;
264                 case "dc:creator":
265                     element = Element.CREATOR;
266                     break;
267                 case "upnp:artist":
268                     element = Element.ARTIST;
269                     break;
270                 case "dc:publisher":
271                     element = Element.PUBLISHER;
272                     break;
273                 case "upnp:genre":
274                     element = Element.GENRE;
275                     break;
276                 case "upnp:album":
277                     element = Element.ALBUM;
278                     break;
279                 case "upnp:albumArtURI":
280                     element = Element.ALBUM_ART_URI;
281                     break;
282                 case "upnp:originalTrackNumber":
283                     element = Element.TRACK_NUMBER;
284                     break;
285                 default:
286                     if (ignore.isEmpty()) {
287                         ignore.add("");
288                         ignore.add("DIDL-Lite");
289                         ignore.add("type");
290                         ignore.add("ordinal");
291                         ignore.add("description");
292                         ignore.add("writeStatus");
293                         ignore.add("storageUsed");
294                         ignore.add("supported");
295                         ignore.add("pushSource");
296                         ignore.add("icon");
297                         ignore.add("playlist");
298                         ignore.add("date");
299                         ignore.add("rating");
300                         ignore.add("userrating");
301                         ignore.add("episodeSeason");
302                         ignore.add("childCountContainer");
303                         ignore.add("modificationTime");
304                         ignore.add("containerContent");
305                     }
306                     if (!ignore.contains(localName)) {
307                         LOGGER.debug("Did not recognise element named {}", localName);
308                     }
309                     element = null;
310             }
311         }
312
313         @Override
314         public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
315             Element el = element;
316             if (el == null || ch == null) {
317                 return;
318             }
319             switch (el) {
320                 case TITLE:
321                     title.append(ch, start, length);
322                     break;
323                 case CLASS:
324                     upnpClass.append(ch, start, length);
325                     break;
326                 case RES:
327                     res.append(ch, start, length);
328                     break;
329                 case ALBUM:
330                     album.append(ch, start, length);
331                     break;
332                 case ALBUM_ART_URI:
333                     albumArtUri.append(ch, start, length);
334                     break;
335                 case CREATOR:
336                     creator.append(ch, start, length);
337                     break;
338                 case ARTIST:
339                     artist.append(ch, start, length);
340                     break;
341                 case PUBLISHER:
342                     publisher.append(ch, start, length);
343                     break;
344                 case GENRE:
345                     genre.append(ch, start, length);
346                     break;
347                 case TRACK_NUMBER:
348                     trackNumber.append(ch, start, length);
349                     break;
350             }
351         }
352
353         @Override
354         public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
355                 throws SAXException {
356             if ("container".equals(qName) || "item".equals(qName)) {
357                 element = null;
358
359                 Integer trackNumberVal;
360                 try {
361                     trackNumberVal = Integer.parseInt(trackNumber.toString());
362                 } catch (NumberFormatException e) {
363                     trackNumberVal = null;
364                 }
365
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));
372
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();
389             }
390         }
391
392         public List<UpnpEntry> getEntries() {
393             return entries;
394         }
395     }
396
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();
409
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 });
413
414         return metadata;
415     }
416 }