]> git.basschouten.com Git - openhab-addons.git/blob
89983602f107e7444bede4318d56e429d9339a67
[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.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;
38
39 /**
40  *
41  * @author Mark Herwege - Initial contribution
42  * @author Karel Goderis - Based on UPnP logic in Sonos binding
43  */
44 @NonNullByDefault
45 public class UpnpXMLParser {
46
47     private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
48
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>";
58
59     private enum Element {
60         TITLE,
61         CLASS,
62         ALBUM,
63         ALBUM_ART_URI,
64         CREATOR,
65         ARTIST,
66         PUBLISHER,
67         GENRE,
68         TRACK_NUMBER,
69         RES
70     }
71
72     public static Map<String, @Nullable String> getRenderingControlFromXML(String xml) {
73         if (xml.isEmpty()) {
74             LOGGER.debug("Could not parse Rendering Control from empty xml");
75             return Collections.emptyMap();
76         }
77         RenderingControlEventHandler handler = new RenderingControlEventHandler();
78         try {
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);
90         }
91         return handler.getChanges();
92     }
93
94     private static class RenderingControlEventHandler extends DefaultHandler {
95
96         private final Map<String, @Nullable String> changes = new HashMap<>();
97
98         RenderingControlEventHandler() {
99             // shouldn't be used outside of this package.
100         }
101
102         @Override
103         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
104                 @Nullable Attributes attributes) throws SAXException {
105             if (qName == null) {
106                 return;
107             }
108             switch (qName) {
109                 case "Volume":
110                 case "Mute":
111                 case "Loudness":
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);
116                     }
117                     break;
118                 default:
119                     if ((attributes != null) && (attributes.getValue("val") != null)) {
120                         changes.put(qName, attributes.getValue("val"));
121                     }
122                     break;
123             }
124         }
125
126         public Map<String, @Nullable String> getChanges() {
127             return changes;
128         }
129     }
130
131     public static Map<String, String> getAVTransportFromXML(String xml) {
132         if (xml.isEmpty()) {
133             LOGGER.debug("Could not parse AV Transport from empty xml");
134             return Collections.emptyMap();
135         }
136         AVTransportEventHandler handler = new AVTransportEventHandler();
137         try {
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);
149         }
150         return handler.getChanges();
151     }
152
153     private static class AVTransportEventHandler extends DefaultHandler {
154
155         private final Map<String, String> changes = new HashMap<String, String>();
156
157         AVTransportEventHandler() {
158             // shouldn't be used outside of this package.
159         }
160
161         @Override
162         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
163                 @Nullable Attributes attributes) throws SAXException {
164             /*
165              * The events are all of the form <qName val="value"/> so we can get all
166              * the info we need from here.
167              */
168             if ((qName != null) && (attributes != null) && (attributes.getValue("val") != null)) {
169                 changes.put(qName, attributes.getValue("val"));
170             }
171         }
172
173         public Map<String, String> getChanges() {
174             return changes;
175         }
176     }
177
178     public static List<UpnpEntry> getEntriesFromXML(String xml) {
179         if (xml.isEmpty()) {
180             LOGGER.debug("Could not parse Entries from empty xml");
181             return Collections.emptyList();
182         }
183         EntryHandler handler = new EntryHandler();
184         try {
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);
196         }
197         return handler.getEntries();
198     }
199
200     private static class EntryHandler extends DefaultHandler {
201
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>();
205
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;
222
223         private List<UpnpEntry> entries = new ArrayList<>();
224
225         EntryHandler() {
226             // shouldn't be used outside of this package.
227         }
228
229         @Override
230         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
231                 @Nullable Attributes attributes) throws SAXException {
232             if (qName == null) {
233                 element = null;
234                 return;
235             }
236             switch (qName) {
237                 case "container":
238                 case "item":
239                     if (attributes != null) {
240                         if (attributes.getValue("id") != null) {
241                             id = attributes.getValue("id");
242                         }
243                         if (attributes.getValue("refID") != null) {
244                             refId = attributes.getValue("refID");
245                         }
246                         if (attributes.getValue("parentID") != null) {
247                             parentId = attributes.getValue("parentID");
248                         }
249                     }
250                     break;
251                 case "res":
252                     if (attributes != null) {
253                         String protocolInfo = attributes.getValue("protocolInfo");
254                         Long size;
255                         try {
256                             size = Long.parseLong(attributes.getValue("size"));
257                         } catch (NumberFormatException e) {
258                             size = null;
259                         }
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;
264                     }
265                     break;
266                 case "dc:title":
267                     element = Element.TITLE;
268                     break;
269                 case "upnp:class":
270                     element = Element.CLASS;
271                     break;
272                 case "dc:creator":
273                     element = Element.CREATOR;
274                     break;
275                 case "upnp:artist":
276                     element = Element.ARTIST;
277                     break;
278                 case "dc:publisher":
279                     element = Element.PUBLISHER;
280                     break;
281                 case "upnp:genre":
282                     element = Element.GENRE;
283                     break;
284                 case "upnp:album":
285                     element = Element.ALBUM;
286                     break;
287                 case "upnp:albumArtURI":
288                     element = Element.ALBUM_ART_URI;
289                     break;
290                 case "upnp:originalTrackNumber":
291                     element = Element.TRACK_NUMBER;
292                     break;
293                 default:
294                     if (ignore.isEmpty()) {
295                         ignore.add("");
296                         ignore.add("DIDL-Lite");
297                         ignore.add("type");
298                         ignore.add("ordinal");
299                         ignore.add("description");
300                         ignore.add("writeStatus");
301                         ignore.add("storageUsed");
302                         ignore.add("supported");
303                         ignore.add("pushSource");
304                         ignore.add("icon");
305                         ignore.add("playlist");
306                         ignore.add("date");
307                         ignore.add("rating");
308                         ignore.add("userrating");
309                         ignore.add("episodeSeason");
310                         ignore.add("childCountContainer");
311                         ignore.add("modificationTime");
312                         ignore.add("containerContent");
313                     }
314                     if (!ignore.contains(localName)) {
315                         LOGGER.debug("Did not recognise element named {}", localName);
316                     }
317                     element = null;
318             }
319         }
320
321         @Override
322         public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
323             Element el = element;
324             if (el == null || ch == null) {
325                 return;
326             }
327             switch (el) {
328                 case TITLE:
329                     title.append(ch, start, length);
330                     break;
331                 case CLASS:
332                     upnpClass.append(ch, start, length);
333                     break;
334                 case RES:
335                     res.append(ch, start, length);
336                     break;
337                 case ALBUM:
338                     album.append(ch, start, length);
339                     break;
340                 case ALBUM_ART_URI:
341                     albumArtUri.append(ch, start, length);
342                     break;
343                 case CREATOR:
344                     creator.append(ch, start, length);
345                     break;
346                 case ARTIST:
347                     artist.append(ch, start, length);
348                     break;
349                 case PUBLISHER:
350                     publisher.append(ch, start, length);
351                     break;
352                 case GENRE:
353                     genre.append(ch, start, length);
354                     break;
355                 case TRACK_NUMBER:
356                     trackNumber.append(ch, start, length);
357                     break;
358             }
359         }
360
361         @Override
362         public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
363                 throws SAXException {
364             if ("container".equals(qName) || "item".equals(qName)) {
365                 element = null;
366
367                 Integer trackNumberVal;
368                 try {
369                     trackNumberVal = Integer.parseInt(trackNumber.toString());
370                 } catch (NumberFormatException e) {
371                     trackNumberVal = null;
372                 }
373
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));
380
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();
397             }
398         }
399
400         public List<UpnpEntry> getEntries() {
401             return entries;
402         }
403     }
404
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();
417
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 });
421
422         return metadata;
423     }
424 }