]> git.basschouten.com Git - openhab-addons.git/blob
b45ba75d2bd5ab8dfb2be2b2c122d753a4eeec3f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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.openhab.core.util.StringUtils;
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             factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
83             saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
84             factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
85             saxParser.parse(new InputSource(new StringReader(xml)), handler);
86         } catch (IOException e) {
87             // This should never happen - we're not performing I/O!
88             LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
89         } catch (SAXException | ParserConfigurationException s) {
90             LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
91         }
92         return handler.getChanges();
93     }
94
95     private static class RenderingControlEventHandler extends DefaultHandler {
96
97         private final Map<String, @Nullable String> changes = new HashMap<>();
98
99         RenderingControlEventHandler() {
100             // shouldn't be used outside of this package.
101         }
102
103         @Override
104         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
105                 @Nullable Attributes attributes) throws SAXException {
106             if (qName == null) {
107                 return;
108             }
109             switch (qName) {
110                 case "Volume":
111                 case "Mute":
112                 case "Loudness":
113                     String channel = attributes == null ? null : attributes.getValue("channel");
114                     String val = attributes == null ? null : attributes.getValue("val");
115                     if (channel != null && val != null) {
116                         changes.put(channel + qName, val);
117                     }
118                     break;
119                 default:
120                     if ((attributes != null) && (attributes.getValue("val") != null)) {
121                         changes.put(qName, attributes.getValue("val"));
122                     }
123                     break;
124             }
125         }
126
127         public Map<String, @Nullable String> getChanges() {
128             return changes;
129         }
130     }
131
132     public static Map<String, String> getAVTransportFromXML(String xml) {
133         if (xml.isEmpty()) {
134             LOGGER.debug("Could not parse AV Transport from empty xml");
135             return Collections.emptyMap();
136         }
137         AVTransportEventHandler handler = new AVTransportEventHandler();
138         try {
139             SAXParserFactory factory = SAXParserFactory.newInstance();
140             SAXParser saxParser = factory.newSAXParser();
141             factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
142             saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
143             factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
144             saxParser.parse(new InputSource(new StringReader(xml)), handler);
145         } catch (IOException e) {
146             // This should never happen - we're not performing I/O!
147             LOGGER.error("Could not parse AV Transport from string '{}'", xml, e);
148         } catch (SAXException | ParserConfigurationException s) {
149             LOGGER.debug("Could not parse AV Transport from string '{}'", xml, s);
150         }
151         return handler.getChanges();
152     }
153
154     private static class AVTransportEventHandler extends DefaultHandler {
155
156         private final Map<String, String> changes = new HashMap<String, String>();
157
158         AVTransportEventHandler() {
159             // shouldn't be used outside of this package.
160         }
161
162         @Override
163         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
164                 @Nullable Attributes attributes) throws SAXException {
165             /*
166              * The events are all of the form <qName val="value"/> so we can get all
167              * the info we need from here.
168              */
169             if ((qName != null) && (attributes != null) && (attributes.getValue("val") != null)) {
170                 changes.put(qName, attributes.getValue("val"));
171             }
172         }
173
174         public Map<String, String> getChanges() {
175             return changes;
176         }
177     }
178
179     public static List<UpnpEntry> getEntriesFromXML(String xml) {
180         if (xml.isEmpty()) {
181             LOGGER.debug("Could not parse Entries from empty xml");
182             return Collections.emptyList();
183         }
184         EntryHandler handler = new EntryHandler();
185         try {
186             SAXParserFactory factory = SAXParserFactory.newInstance();
187             SAXParser saxParser = factory.newSAXParser();
188             factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
189             saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
190             factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
191             saxParser.parse(new InputSource(new StringReader(xml)), handler);
192         } catch (IOException e) {
193             // This should never happen - we're not performing I/O!
194             LOGGER.error("Could not parse Entries from string '{}'", xml, e);
195         } catch (SAXException | ParserConfigurationException s) {
196             LOGGER.debug("Could not parse Entries from string '{}'", xml, s);
197         }
198         return handler.getEntries();
199     }
200
201     private static class EntryHandler extends DefaultHandler {
202
203         // Maintain a set of elements it is not useful to complain about.
204         // This list will be initialized on the first failure case.
205         private static List<String> ignore = new ArrayList<String>();
206
207         private String id = "";
208         private String refId = "";
209         private String parentId = "";
210         private StringBuilder upnpClass = new StringBuilder();
211         private List<UpnpEntryRes> resList = new ArrayList<>();
212         private StringBuilder res = new StringBuilder();
213         private StringBuilder title = new StringBuilder();
214         private StringBuilder album = new StringBuilder();
215         private StringBuilder albumArtUri = new StringBuilder();
216         private StringBuilder creator = new StringBuilder();
217         private StringBuilder artist = new StringBuilder();
218         private List<String> artistList = new ArrayList<>();
219         private StringBuilder publisher = new StringBuilder();
220         private StringBuilder genre = new StringBuilder();
221         private StringBuilder trackNumber = new StringBuilder();
222         private @Nullable Element element = null;
223
224         private List<UpnpEntry> entries = new ArrayList<>();
225
226         EntryHandler() {
227             // shouldn't be used outside of this package.
228         }
229
230         @Override
231         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
232                 @Nullable Attributes attributes) throws SAXException {
233             if (qName == null) {
234                 element = null;
235                 return;
236             }
237             switch (qName) {
238                 case "container":
239                 case "item":
240                     if (attributes != null) {
241                         if (attributes.getValue("id") != null) {
242                             id = attributes.getValue("id");
243                         }
244                         if (attributes.getValue("refID") != null) {
245                             refId = attributes.getValue("refID");
246                         }
247                         if (attributes.getValue("parentID") != null) {
248                             parentId = attributes.getValue("parentID");
249                         }
250                     }
251                     break;
252                 case "res":
253                     if (attributes != null) {
254                         String protocolInfo = attributes.getValue("protocolInfo");
255                         Long size;
256                         try {
257                             size = Long.parseLong(attributes.getValue("size"));
258                         } catch (NumberFormatException e) {
259                             size = null;
260                         }
261                         String duration = attributes.getValue("duration");
262                         String importUri = attributes.getValue("importUri");
263                         resList.add(0, new UpnpEntryRes(protocolInfo, size, duration, importUri));
264                         element = Element.RES;
265                     }
266                     break;
267                 case "dc:title":
268                     element = Element.TITLE;
269                     break;
270                 case "upnp:class":
271                     element = Element.CLASS;
272                     break;
273                 case "dc:creator":
274                     element = Element.CREATOR;
275                     break;
276                 case "upnp:artist":
277                     element = Element.ARTIST;
278                     break;
279                 case "dc:publisher":
280                     element = Element.PUBLISHER;
281                     break;
282                 case "upnp:genre":
283                     element = Element.GENRE;
284                     break;
285                 case "upnp:album":
286                     element = Element.ALBUM;
287                     break;
288                 case "upnp:albumArtURI":
289                     element = Element.ALBUM_ART_URI;
290                     break;
291                 case "upnp:originalTrackNumber":
292                     element = Element.TRACK_NUMBER;
293                     break;
294                 default:
295                     if (ignore.isEmpty()) {
296                         ignore.add("");
297                         ignore.add("DIDL-Lite");
298                         ignore.add("type");
299                         ignore.add("ordinal");
300                         ignore.add("description");
301                         ignore.add("writeStatus");
302                         ignore.add("storageUsed");
303                         ignore.add("supported");
304                         ignore.add("pushSource");
305                         ignore.add("icon");
306                         ignore.add("playlist");
307                         ignore.add("date");
308                         ignore.add("rating");
309                         ignore.add("userrating");
310                         ignore.add("episodeSeason");
311                         ignore.add("childCountContainer");
312                         ignore.add("modificationTime");
313                         ignore.add("containerContent");
314                     }
315                     if (!ignore.contains(localName)) {
316                         LOGGER.debug("Did not recognise element named {}", localName);
317                     }
318                     element = null;
319             }
320         }
321
322         @Override
323         public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
324             Element el = element;
325             if (el == null || ch == null) {
326                 return;
327             }
328             switch (el) {
329                 case TITLE:
330                     title.append(ch, start, length);
331                     break;
332                 case CLASS:
333                     upnpClass.append(ch, start, length);
334                     break;
335                 case RES:
336                     res.append(ch, start, length);
337                     break;
338                 case ALBUM:
339                     album.append(ch, start, length);
340                     break;
341                 case ALBUM_ART_URI:
342                     albumArtUri.append(ch, start, length);
343                     break;
344                 case CREATOR:
345                     creator.append(ch, start, length);
346                     break;
347                 case ARTIST:
348                     artist.append(ch, start, length);
349                     break;
350                 case PUBLISHER:
351                     publisher.append(ch, start, length);
352                     break;
353                 case GENRE:
354                     genre.append(ch, start, length);
355                     break;
356                 case TRACK_NUMBER:
357                     trackNumber.append(ch, start, length);
358                     break;
359             }
360         }
361
362         @Override
363         public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
364                 throws SAXException {
365             if ("container".equals(qName) || "item".equals(qName)) {
366                 element = null;
367
368                 Integer trackNumberVal;
369                 try {
370                     trackNumberVal = Integer.parseInt(trackNumber.toString());
371                 } catch (NumberFormatException e) {
372                     trackNumberVal = null;
373                 }
374
375                 entries.add(new UpnpEntry(id, refId, parentId, upnpClass.toString()).withTitle(title.toString())
376                         .withAlbum(album.toString()).withAlbumArtUri(albumArtUri.toString())
377                         .withCreator(creator.toString())
378                         .withArtist(!artistList.isEmpty() ? artistList.get(0) : artist.toString())
379                         .withPublisher(publisher.toString()).withGenre(genre.toString()).withTrackNumber(trackNumberVal)
380                         .withResList(resList));
381
382                 title = new StringBuilder();
383                 upnpClass = new StringBuilder();
384                 resList = new ArrayList<>();
385                 album = new StringBuilder();
386                 albumArtUri = new StringBuilder();
387                 creator = new StringBuilder();
388                 artistList = new ArrayList<>();
389                 publisher = new StringBuilder();
390                 genre = new StringBuilder();
391                 trackNumber = new StringBuilder();
392             } else if ("res".equals(qName)) {
393                 resList.get(0).setRes(res.toString());
394                 res = new StringBuilder();
395             } else if ("upnp:artist".equals(qName)) {
396                 artistList.add(artist.toString());
397                 artist = new StringBuilder();
398             }
399         }
400
401         public List<UpnpEntry> getEntries() {
402             return entries;
403         }
404     }
405
406     public static String compileMetadataString(UpnpEntry entry) {
407         String id = entry.getId();
408         String parentId = entry.getParentId();
409         String title = StringUtils.escapeXml(entry.getTitle());
410         String upnpClass = entry.getUpnpClass();
411         String album = StringUtils.escapeXml(entry.getAlbum());
412         String albumArtUri = entry.getAlbumArtUri();
413         String creator = StringUtils.escapeXml(entry.getCreator());
414         String artist = StringUtils.escapeXml(entry.getArtist());
415         String publisher = StringUtils.escapeXml(entry.getPublisher());
416         String genre = StringUtils.escapeXml(entry.getGenre());
417         Integer trackNumber = entry.getOriginalTrackNumber();
418
419         final MessageFormat messageFormat = new MessageFormat(METADATA_PATTERN);
420         String metadata = messageFormat.format(new Object[] { id, parentId, title, upnpClass, album, albumArtUri,
421                 creator, artist, publisher, genre, trackNumber });
422
423         return metadata;
424     }
425 }