]> git.basschouten.com Git - openhab-addons.git/blob
f983180296d8d57cd1d760d68e1acaa21435ee67
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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;
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.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;
37
38 /**
39  *
40  * @author Mark Herwege - Initial contribution
41  * @author Karel Goderis - Based on UPnP logic in Sonos binding
42  */
43 @NonNullByDefault
44 public class UpnpXMLParser {
45
46     private static final Logger LOGGER = LoggerFactory.getLogger(UpnpXMLParser.class);
47
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>");
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, String> getAVTransportFromXML(String xml) {
73         if (xml.isEmpty()) {
74             LOGGER.debug("Could not parse AV Transport from empty xml");
75             return Collections.emptyMap();
76         }
77         AVTransportEventHandler handler = new AVTransportEventHandler();
78         try {
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);
87         }
88         return handler.getChanges();
89     }
90
91     /**
92      * @param xml
93      * @return a list of Entries from the given xml string.
94      * @throws IOException
95      * @throws SAXException
96      */
97     public static List<UpnpEntry> getEntriesFromXML(String xml) {
98         if (xml.isEmpty()) {
99             LOGGER.debug("Could not parse Entries from empty xml");
100             return Collections.emptyList();
101         }
102         EntryHandler handler = new EntryHandler();
103         try {
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);
112         }
113         return handler.getEntries();
114     }
115
116     private static class AVTransportEventHandler extends DefaultHandler {
117
118         private final Map<String, String> changes = new HashMap<String, String>();
119
120         AVTransportEventHandler() {
121             // shouldn't be used outside of this package.
122         }
123
124         @Override
125         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
126                 @Nullable Attributes atts) throws SAXException {
127             /*
128              * The events are all of the form <qName val="value"/> so we can get all
129              * the info we need from here.
130              */
131             if ((qName != null) && (atts != null) && (atts.getValue("val") != null)) {
132                 changes.put(qName, atts.getValue("val"));
133             }
134         }
135
136         public Map<String, String> getChanges() {
137             return changes;
138         }
139     }
140
141     private static class EntryHandler extends DefaultHandler {
142
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>();
146
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;
163
164         private List<UpnpEntry> entries = new ArrayList<>();
165
166         EntryHandler() {
167             // shouldn't be used outside of this package.
168         }
169
170         @Override
171         public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
172                 @Nullable Attributes attributes) throws SAXException {
173             if (qName == null) {
174                 element = null;
175                 return;
176             }
177             switch (qName) {
178                 case "container":
179                 case "item":
180                     if (attributes != null) {
181                         if (attributes.getValue("id") != null) {
182                             id = attributes.getValue("id");
183                         }
184                         if (attributes.getValue("refID") != null) {
185                             refId = attributes.getValue("refID");
186                         }
187                         if (attributes.getValue("parentID") != null) {
188                             parentId = attributes.getValue("parentID");
189                         }
190                     }
191                     break;
192                 case "res":
193                     if (attributes != null) {
194                         String protocolInfo = attributes.getValue("protocolInfo");
195                         Long size;
196                         try {
197                             size = Long.parseLong(attributes.getValue("size"));
198                         } catch (NumberFormatException e) {
199                             size = null;
200                         }
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;
205                     }
206                     break;
207                 case "dc:title":
208                     element = Element.TITLE;
209                     break;
210                 case "upnp:class":
211                     element = Element.CLASS;
212                     break;
213                 case "dc:creator":
214                     element = Element.CREATOR;
215                     break;
216                 case "upnp:artist":
217                     element = Element.ARTIST;
218                     break;
219                 case "dc:publisher":
220                     element = Element.PUBLISHER;
221                     break;
222                 case "upnp:genre":
223                     element = Element.GENRE;
224                     break;
225                 case "upnp:album":
226                     element = Element.ALBUM;
227                     break;
228                 case "upnp:albumArtURI":
229                     element = Element.ALBUM_ART_URI;
230                     break;
231                 case "upnp:originalTrackNumber":
232                     element = Element.TRACK_NUMBER;
233                     break;
234                 default:
235                     if (ignore.isEmpty()) {
236                         ignore.add("");
237                         ignore.add("DIDL-Lite");
238                         ignore.add("type");
239                         ignore.add("ordinal");
240                         ignore.add("description");
241                         ignore.add("writeStatus");
242                         ignore.add("storageUsed");
243                         ignore.add("supported");
244                         ignore.add("pushSource");
245                         ignore.add("icon");
246                         ignore.add("playlist");
247                         ignore.add("date");
248                         ignore.add("rating");
249                         ignore.add("userrating");
250                         ignore.add("episodeSeason");
251                         ignore.add("childCountContainer");
252                         ignore.add("modificationTime");
253                         ignore.add("containerContent");
254                     }
255                     if (!ignore.contains(localName)) {
256                         LOGGER.debug("Did not recognise element named {}", localName);
257                     }
258                     element = null;
259             }
260         }
261
262         @Override
263         public void characters(char @Nullable [] ch, int start, int length) throws SAXException {
264             Element el = element;
265             if (el == null || ch == null) {
266                 return;
267             }
268             switch (el) {
269                 case TITLE:
270                     title.append(ch, start, length);
271                     break;
272                 case CLASS:
273                     upnpClass.append(ch, start, length);
274                     break;
275                 case RES:
276                     res.append(ch, start, length);
277                     break;
278                 case ALBUM:
279                     album.append(ch, start, length);
280                     break;
281                 case ALBUM_ART_URI:
282                     albumArtUri.append(ch, start, length);
283                     break;
284                 case CREATOR:
285                     creator.append(ch, start, length);
286                     break;
287                 case ARTIST:
288                     artist.append(ch, start, length);
289                     break;
290                 case PUBLISHER:
291                     publisher.append(ch, start, length);
292                     break;
293                 case GENRE:
294                     genre.append(ch, start, length);
295                     break;
296                 case TRACK_NUMBER:
297                     trackNumber.append(ch, start, length);
298                     break;
299             }
300         }
301
302         @Override
303         public void endElement(@Nullable String uri, @Nullable String localName, @Nullable String qName)
304                 throws SAXException {
305             if ("container".equals(qName) || "item".equals(qName)) {
306                 element = null;
307
308                 Integer trackNumberVal;
309                 try {
310                     trackNumberVal = Integer.parseInt(trackNumber.toString());
311                 } catch (NumberFormatException e) {
312                     trackNumberVal = null;
313                 }
314
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));
321
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();
338             }
339         }
340
341         public List<UpnpEntry> getEntries() {
342             return entries;
343         }
344     }
345
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();
358
359         String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, album, albumArtUri,
360                 creator, artist, publisher, genre, trackNumber });
361
362         return metadata;
363     }
364 }