View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.support.contentdirectory;
17  
18  import org.fourthline.cling.model.types.Datatype;
19  import org.fourthline.cling.model.types.InvalidValueException;
20  import org.fourthline.cling.support.model.DIDLAttribute;
21  import org.fourthline.cling.support.model.DIDLContent;
22  import org.fourthline.cling.support.model.DIDLObject;
23  import org.fourthline.cling.support.model.DescMeta;
24  import org.fourthline.cling.support.model.Person;
25  import org.fourthline.cling.support.model.PersonWithRole;
26  import org.fourthline.cling.support.model.ProtocolInfo;
27  import org.fourthline.cling.support.model.Res;
28  import org.fourthline.cling.support.model.StorageMedium;
29  import org.fourthline.cling.support.model.WriteStatus;
30  import org.fourthline.cling.support.model.container.Container;
31  import org.fourthline.cling.support.model.item.Item;
32  import org.seamless.util.io.IO;
33  import org.seamless.util.Exceptions;
34  import org.seamless.xml.SAXParser;
35  import org.w3c.dom.Document;
36  import org.w3c.dom.Element;
37  import org.w3c.dom.Node;
38  import org.w3c.dom.NodeList;
39  import org.xml.sax.Attributes;
40  import org.xml.sax.InputSource;
41  import org.xml.sax.SAXException;
42  
43  import javax.xml.parsers.DocumentBuilderFactory;
44  import javax.xml.transform.OutputKeys;
45  import javax.xml.transform.Transformer;
46  import javax.xml.transform.TransformerFactory;
47  import javax.xml.transform.dom.DOMSource;
48  import javax.xml.transform.stream.StreamResult;
49  import java.io.InputStream;
50  import java.io.StringReader;
51  import java.io.StringWriter;
52  import java.net.URI;
53  import java.util.logging.Level;
54  import java.util.logging.Logger;
55  
56  import static org.fourthline.cling.model.XMLUtil.appendNewElement;
57  import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull;
58  
59  /**
60   * DIDL parser based on SAX for reading and DOM for writing.
61   * <p>
62   * This parser requires Android platform level 8 (2.2).
63   * </p>
64   * <p>
65   * Override the {@link #createDescMetaHandler(org.fourthline.cling.support.model.DescMeta, org.seamless.xml.SAXParser.Handler)}
66   * method to read vendor extension content of {@code <desc>} elements. You then should also override the
67   * {@link #populateDescMetadata(org.w3c.dom.Element, org.fourthline.cling.support.model.DescMeta)} method for writing.
68   * </p>
69   * <p>
70   * Override the {@link #createItemHandler(org.fourthline.cling.support.model.item.Item, org.seamless.xml.SAXParser.Handler)}
71   * etc. methods to register custom handlers for vendor-specific elements and attributes within items, containers,
72   * and so on.
73   * </p>
74   *
75   * @author Christian Bauer
76   * @author Mario Franco
77   */
78  public class DIDLParser extends SAXParser {
79  
80      final private static Logger log = Logger.getLogger(DIDLParser.class.getName());
81  
82      public static final String UNKNOWN_TITLE = "Unknown Title";
83  
84      /**
85       * Uses the current thread's context classloader to read and unmarshall the given resource.
86       *
87       * @param resource The resource on the classpath.
88       * @return The unmarshalled DIDL content model.
89       * @throws Exception
90       */
91      public DIDLContent parseResource(String resource) throws Exception {
92          InputStream is = null;
93          try {
94              is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
95              return parse(IO.readLines(is));
96          } finally {
97              if (is != null) is.close();
98          }
99      }
100 
101     /**
102      * Reads and unmarshalls an XML representation into a DIDL content model.
103      *
104      * @param xml The XML representation.
105      * @return A DIDL content model.
106      * @throws Exception
107      */
108     public DIDLContent parse(String xml) throws Exception {
109 
110         if (xml == null || xml.length() == 0) {
111             throw new RuntimeException("Null or empty XML");
112         }
113 
114         DIDLContent content = new DIDLContent();
115         createRootHandler(content, this);
116 
117         log.fine("Parsing DIDL XML content");
118         parse(new InputSource(new StringReader(xml)));
119         return content;
120     }
121 
122     protected RootHandler createRootHandler(DIDLContent instance, SAXParser parser) {
123         return new RootHandler(instance, parser);
124     }
125 
126     protected ContainerHandler createContainerHandler(Container instance, Handler parent) {
127         return new ContainerHandler(instance, parent);
128     }
129 
130     protected ItemHandler createItemHandler(Item instance, Handler parent) {
131         return new ItemHandler(instance, parent);
132     }
133 
134     protected ResHandler createResHandler(Res instance, Handler parent) {
135         return new ResHandler(instance, parent);
136     }
137 
138     protected DescMetaHandler createDescMetaHandler(DescMeta instance, Handler parent) {
139         return new DescMetaHandler(instance, parent);
140     }
141 
142 
143     protected Container createContainer(Attributes attributes) {
144         Container container = new Container();
145 
146         container.setId(attributes.getValue("id"));
147         container.setParentID(attributes.getValue("parentID"));
148 
149         if ((attributes.getValue("childCount") != null))
150             container.setChildCount(Integer.valueOf(attributes.getValue("childCount")));
151 
152         try {
153             Boolean value = (Boolean) Datatype.Builtin.BOOLEAN.getDatatype().valueOf(
154                 attributes.getValue("restricted")
155             );
156             if (value != null)
157                 container.setRestricted(value);
158 
159             value = (Boolean) Datatype.Builtin.BOOLEAN.getDatatype().valueOf(
160                 attributes.getValue("searchable")
161             );
162             if (value != null)
163                 container.setSearchable(value);
164         } catch (Exception ex) {
165             // Ignore
166         }
167 
168         return container;
169     }
170 
171     protected Item createItem(Attributes attributes) {
172         Item item = new Item();
173 
174         item.setId(attributes.getValue("id"));
175         item.setParentID(attributes.getValue("parentID"));
176 
177         try {
178             Boolean value = (Boolean)Datatype.Builtin.BOOLEAN.getDatatype().valueOf(
179                     attributes.getValue("restricted")
180             );
181             if (value != null)
182                 item.setRestricted(value);
183 
184         } catch (Exception ex) {
185             // Ignore
186         }
187 
188         if ((attributes.getValue("refID") != null))
189             item.setRefID(attributes.getValue("refID"));
190 
191         return item;
192     }
193 
194     protected Res createResource(Attributes attributes) {
195         Res res = new Res();
196 
197         if (attributes.getValue("importUri") != null)
198             res.setImportUri(URI.create(attributes.getValue("importUri")));
199 
200         try {
201             res.setProtocolInfo(
202                     new ProtocolInfo(attributes.getValue("protocolInfo"))
203             );
204         } catch (InvalidValueException ex) {
205             log.warning("In DIDL content, invalid resource protocol info: " + Exceptions.unwrap(ex));
206             return null;
207         }
208 
209         if (attributes.getValue("size") != null)
210             res.setSize(toLongOrNull(attributes.getValue("size")));
211 
212         if (attributes.getValue("duration") != null)
213             res.setDuration(attributes.getValue("duration"));
214 
215         if (attributes.getValue("bitrate") != null)
216             res.setBitrate(toLongOrNull(attributes.getValue("bitrate")));
217 
218         if (attributes.getValue("sampleFrequency") != null)
219             res.setSampleFrequency(toLongOrNull(attributes.getValue("sampleFrequency")));
220 
221         if (attributes.getValue("bitsPerSample") != null)
222             res.setBitsPerSample(toLongOrNull(attributes.getValue("bitsPerSample")));
223 
224         if (attributes.getValue("nrAudioChannels") != null)
225             res.setNrAudioChannels(toLongOrNull(attributes.getValue("nrAudioChannels")));
226 
227         if (attributes.getValue("colorDepth") != null)
228             res.setColorDepth(toLongOrNull(attributes.getValue("colorDepth")));
229 
230         if (attributes.getValue("protection") != null)
231             res.setProtection(attributes.getValue("protection"));
232 
233         if (attributes.getValue("resolution") != null)
234             res.setResolution(attributes.getValue("resolution"));
235 
236         return res;
237     }
238 
239     private Long toLongOrNull(String value) {
240         try {
241             return Long.valueOf(value);
242         } catch (NumberFormatException x) {
243             return null;
244         }
245     }
246 
247     protected DescMeta createDescMeta(Attributes attributes) {
248         DescMeta desc = new DescMeta();
249 
250         desc.setId(attributes.getValue("id"));
251 
252         if ((attributes.getValue("type") != null))
253             desc.setType(attributes.getValue("type"));
254 
255         if ((attributes.getValue("nameSpace") != null))
256             desc.setNameSpace(URI.create(attributes.getValue("nameSpace")));
257 
258         return desc;
259     }
260 
261 
262     /* ############################################################################################# */
263 
264 
265     /**
266      * Generates a XML representation of the content model.
267      * <p>
268      * Items inside a container will <em>not</em> be represented in the XML, the containers
269      * will be rendered flat without children.
270      * </p>
271      *
272      * @param content The content model.
273      * @return An XML representation.
274      * @throws Exception
275      */
276     public String generate(DIDLContent content) throws Exception {
277         return generate(content, false);
278     }
279 
280     /**
281      * Generates an XML representation of the content model.
282      * <p>
283      * Optionally, items inside a container will be represented in the XML,
284      * the container elements then have nested item elements. Although this
285      * parser can read such a structure, it is unclear whether other DIDL
286      * parsers should and actually do support this XML.
287      * </p>
288      *
289      * @param content     The content model.
290      * @param nestedItems <code>true</code> if nested item elements should be rendered for containers.
291      * @return An XML representation.
292      * @throws Exception
293      */
294     public String generate(DIDLContent content, boolean nestedItems) throws Exception {
295         return documentToString(buildDOM(content, nestedItems), true);
296     }
297 
298     // TODO: Yes, this only runs on Android 2.2
299 
300     protected String documentToString(Document document, boolean omitProlog) throws Exception {
301         TransformerFactory transFactory = TransformerFactory.newInstance();
302 
303         // Indentation not supported on Android 2.2
304         //transFactory.setAttribute("indent-number", 4);
305 
306         Transformer transformer = transFactory.newTransformer();
307 
308         if (omitProlog) {
309             // TODO: UPNP VIOLATION: Terratec Noxon Webradio fails when DIDL content has a prolog
310             // No XML prolog! This is allowed because it is UTF-8 encoded and required
311             // because broken devices will stumble on SOAP messages that contain (even
312             // encoded) XML prologs within a message body.
313             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
314         }
315 
316         // Again, Android 2.2 fails hard if you try this.
317         //transformer.setOutputProperty(OutputKeys.INDENT, "yes");
318 
319         StringWriter out = new StringWriter();
320         transformer.transform(new DOMSource(document), new StreamResult(out));
321         return out.toString();
322     }
323 
324     protected Document buildDOM(DIDLContent content, boolean nestedItems) throws Exception {
325 
326         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
327         factory.setNamespaceAware(true);
328 
329         Document d = factory.newDocumentBuilder().newDocument();
330 
331         generateRoot(content, d, nestedItems);
332 
333         return d;
334     }
335 
336     protected void generateRoot(DIDLContent content, Document descriptor, boolean nestedItems) {
337         Element rootElement = descriptor.createElementNS(DIDLContent.NAMESPACE_URI, "DIDL-Lite");
338         descriptor.appendChild(rootElement);
339 
340         // rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:didl", DIDLContent.NAMESPACE_URI);
341         rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:upnp", DIDLObject.Property.UPNP.NAMESPACE.URI);
342         rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dc", DIDLObject.Property.DC.NAMESPACE.URI);
343         rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:sec", DIDLObject.Property.SEC.NAMESPACE.URI);
344 
345         for (Container container : content.getContainers()) {
346             if (container == null) continue;
347             generateContainer(container, descriptor, rootElement, nestedItems);
348         }
349 
350         for (Item item : content.getItems()) {
351             if (item == null) continue;
352             generateItem(item, descriptor, rootElement);
353         }
354 
355         for (DescMeta descMeta : content.getDescMetadata()) {
356             if (descMeta == null) continue;
357             generateDescMetadata(descMeta, descriptor, rootElement);
358         }
359     }
360 
361     protected void generateContainer(Container container, Document descriptor, Element parent, boolean nestedItems) {
362 
363         if (container.getClazz() == null) {
364             throw new RuntimeException("Missing 'upnp:class' element for container: " + container.getId());
365         }
366 
367         Element containerElement = appendNewElement(descriptor, parent, "container");
368 
369         if (container.getId() == null)
370             throw new NullPointerException("Missing id on container: " + container);
371         containerElement.setAttribute("id", container.getId());
372 
373         if (container.getParentID() == null)
374             throw new NullPointerException("Missing parent id on container: " + container);
375         containerElement.setAttribute("parentID", container.getParentID());
376 
377         if (container.getChildCount() != null) {
378             containerElement.setAttribute("childCount", Integer.toString(container.getChildCount()));
379         }
380 
381         containerElement.setAttribute("restricted", booleanToInt(container.isRestricted()));
382         containerElement.setAttribute("searchable", booleanToInt(container.isSearchable()));
383 
384         String title = container.getTitle();
385         if (title == null) {
386             log.warning("Missing 'dc:title' element for container: " + container.getId());
387             title = UNKNOWN_TITLE;
388         }
389 
390         appendNewElementIfNotNull(
391             descriptor,
392             containerElement,
393             "dc:title",
394             title,
395             DIDLObject.Property.DC.NAMESPACE.URI
396         );
397 
398         appendNewElementIfNotNull(
399             descriptor,
400             containerElement,
401             "dc:creator",
402             container.getCreator(),
403             DIDLObject.Property.DC.NAMESPACE.URI
404         );
405 
406         appendNewElementIfNotNull(
407             descriptor,
408             containerElement,
409             "upnp:writeStatus",
410             container.getWriteStatus(),
411             DIDLObject.Property.UPNP.NAMESPACE.URI
412         );
413 
414         appendClass(descriptor, containerElement, container.getClazz(), "upnp:class", false);
415 
416         for (DIDLObject.Class searchClass : container.getSearchClasses()) {
417             appendClass(descriptor, containerElement, searchClass, "upnp:searchClass", true);
418         }
419 
420         for (DIDLObject.Class createClass : container.getCreateClasses()) {
421             appendClass(descriptor, containerElement, createClass, "upnp:createClass", true);
422         }
423 
424         appendProperties(descriptor, containerElement, container, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI);
425         appendProperties(descriptor, containerElement, container, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI);
426 
427         if (nestedItems) {
428             for (Item item : container.getItems()) {
429                 if (item == null) continue;
430                 generateItem(item, descriptor, containerElement);
431             }
432         }
433 
434         for (Res resource : container.getResources()) {
435             if (resource == null) continue;
436             generateResource(resource, descriptor, containerElement);
437         }
438 
439         for (DescMeta descMeta : container.getDescMetadata()) {
440             if (descMeta == null) continue;
441             generateDescMetadata(descMeta, descriptor, containerElement);
442         }
443     }
444 
445     protected void generateItem(Item item, Document descriptor, Element parent) {
446 
447         if (item.getClazz() == null) {
448             throw new RuntimeException("Missing 'upnp:class' element for item: " + item.getId());
449         }
450 
451         Element itemElement = appendNewElement(descriptor, parent, "item");
452 
453         if (item.getId() == null)
454             throw new NullPointerException("Missing id on item: " + item);
455         itemElement.setAttribute("id", item.getId());
456 
457         if (item.getParentID() == null)
458             throw new NullPointerException("Missing parent id on item: " + item);
459         itemElement.setAttribute("parentID", item.getParentID());
460 
461         if (item.getRefID() != null)
462             itemElement.setAttribute("refID", item.getRefID());
463         itemElement.setAttribute("restricted", booleanToInt(item.isRestricted()));
464 
465         String title = item.getTitle();
466         if (title == null) {
467             log.warning("Missing 'dc:title' element for item: " + item.getId());
468             title = UNKNOWN_TITLE;
469         }
470 
471         appendNewElementIfNotNull(
472             descriptor,
473             itemElement,
474             "dc:title",
475             title,
476             DIDLObject.Property.DC.NAMESPACE.URI
477         );
478 
479         appendNewElementIfNotNull(
480             descriptor,
481             itemElement,
482             "dc:creator",
483             item.getCreator(),
484             DIDLObject.Property.DC.NAMESPACE.URI
485         );
486 
487         appendNewElementIfNotNull(
488             descriptor,
489             itemElement,
490             "upnp:writeStatus",
491             item.getWriteStatus(),
492             DIDLObject.Property.UPNP.NAMESPACE.URI
493         );
494 
495         appendClass(descriptor, itemElement, item.getClazz(), "upnp:class", false);
496 
497         appendProperties(descriptor, itemElement, item, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI);
498         appendProperties(descriptor, itemElement, item, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI);
499         appendProperties(descriptor, itemElement, item, "sec", DIDLObject.Property.SEC.NAMESPACE.class, DIDLObject.Property.SEC.NAMESPACE.URI);
500 
501         for (Res resource : item.getResources()) {
502             if (resource == null) continue;
503             generateResource(resource, descriptor, itemElement);
504         }
505 
506         for (DescMeta descMeta : item.getDescMetadata()) {
507             if (descMeta == null) continue;
508             generateDescMetadata(descMeta, descriptor, itemElement);
509         }
510     }
511 
512     protected void generateResource(Res resource, Document descriptor, Element parent) {
513 
514         if (resource.getValue() == null) {
515             throw new RuntimeException("Missing resource URI value" + resource);
516         }
517         if (resource.getProtocolInfo() == null) {
518             throw new RuntimeException("Missing resource protocol info: " + resource);
519         }
520 
521         Element resourceElement = appendNewElement(descriptor, parent, "res", resource.getValue());
522         resourceElement.setAttribute("protocolInfo", resource.getProtocolInfo().toString());
523         if (resource.getImportUri() != null)
524             resourceElement.setAttribute("importUri", resource.getImportUri().toString());
525         if (resource.getSize() != null)
526             resourceElement.setAttribute("size", resource.getSize().toString());
527         if (resource.getDuration() != null)
528             resourceElement.setAttribute("duration", resource.getDuration());
529         if (resource.getBitrate() != null)
530             resourceElement.setAttribute("bitrate", resource.getBitrate().toString());
531         if (resource.getSampleFrequency() != null)
532             resourceElement.setAttribute("sampleFrequency", resource.getSampleFrequency().toString());
533         if (resource.getBitsPerSample() != null)
534             resourceElement.setAttribute("bitsPerSample", resource.getBitsPerSample().toString());
535         if (resource.getNrAudioChannels() != null)
536             resourceElement.setAttribute("nrAudioChannels", resource.getNrAudioChannels().toString());
537         if (resource.getColorDepth() != null)
538             resourceElement.setAttribute("colorDepth", resource.getColorDepth().toString());
539         if (resource.getProtection() != null)
540             resourceElement.setAttribute("protection", resource.getProtection());
541         if (resource.getResolution() != null)
542             resourceElement.setAttribute("resolution", resource.getResolution());
543     }
544 
545     protected void generateDescMetadata(DescMeta descMeta, Document descriptor, Element parent) {
546 
547         if (descMeta.getId() == null) {
548             throw new RuntimeException("Missing id of description metadata: " + descMeta);
549         }
550         if (descMeta.getNameSpace() == null) {
551             throw new RuntimeException("Missing namespace of description metadata: " + descMeta);
552         }
553 
554         Element descElement = appendNewElement(descriptor, parent, "desc");
555         descElement.setAttribute("id", descMeta.getId());
556         descElement.setAttribute("nameSpace", descMeta.getNameSpace().toString());
557         if (descMeta.getType() != null)
558             descElement.setAttribute("type", descMeta.getType());
559         populateDescMetadata(descElement, descMeta);
560     }
561 
562     /**
563      * Expects an <code>org.w3c.Document</code> as metadata, copies nodes of the document into the DIDL content.
564      * <p>
565      * This method will ignore the content and log a warning if it's of the wrong type. If you override
566      * {@link #createDescMetaHandler(org.fourthline.cling.support.model.DescMeta, org.seamless.xml.SAXParser.Handler)},
567      * you most likely also want to override this method.
568      * </p>
569      *
570      * @param descElement The DIDL content {@code <desc>} element wrapping the final metadata.
571      * @param descMeta    The metadata with a <code>org.w3c.Document</code> payload.
572      */
573     protected void populateDescMetadata(Element descElement, DescMeta descMeta) {
574         if (descMeta.getMetadata() instanceof Document) {
575             Document doc = (Document) descMeta.getMetadata();
576 
577             NodeList nl = doc.getDocumentElement().getChildNodes();
578             for (int i = 0; i < nl.getLength(); i++) {
579                 Node n = nl.item(i);
580                 if (n.getNodeType() != Node.ELEMENT_NODE)
581                     continue;
582 
583                 Node clone = descElement.getOwnerDocument().importNode(n, true);
584                 descElement.appendChild(clone);
585             }
586 
587         } else {
588             log.warning("Unknown desc metadata content, please override populateDescMetadata(): " + descMeta.getMetadata());
589         }
590     }
591 
592     protected void appendProperties(Document descriptor, Element parent, DIDLObject object, String prefix,
593                                     Class<? extends DIDLObject.Property.NAMESPACE> namespace,
594                                     String namespaceURI) {
595         for (DIDLObject.Property<Object> property : object.getPropertiesByNamespace(namespace)) {
596             Element el = descriptor.createElementNS(namespaceURI, prefix + ":" + property.getDescriptorName());
597             parent.appendChild(el);
598             property.setOnElement(el);
599         }
600     }
601 
602     protected void appendClass(Document descriptor, Element parent, DIDLObject.Class clazz, String element, boolean appendDerivation) {
603         Element classElement = appendNewElementIfNotNull(
604             descriptor,
605             parent,
606             element,
607             clazz.getValue(),
608             DIDLObject.Property.UPNP.NAMESPACE.URI
609         );
610         if (clazz.getFriendlyName() != null && clazz.getFriendlyName().length() > 0)
611             classElement.setAttribute("name", clazz.getFriendlyName());
612         if (appendDerivation)
613             classElement.setAttribute("includeDerived", Boolean.toString(clazz.isIncludeDerived()));
614     }
615 
616     protected String booleanToInt(boolean b) {
617         return b ? "1" : "0";
618     }
619 
620     /**
621      * Sends the given string to the log with <code>Level.FINE</code>, if that log level is enabled.
622      *
623      * @param s The string to send to the log.
624      */
625     public void debugXML(String s) {
626         if (log.isLoggable(Level.FINE)) {
627             log.fine("-------------------------------------------------------------------------------------");
628             log.fine("\n" + s);
629             log.fine("-------------------------------------------------------------------------------------");
630         }
631     }
632 
633 
634     /* ############################################################################################# */
635 
636 
637     public abstract class DIDLObjectHandler<I extends DIDLObject> extends Handler<I> {
638 
639         protected DIDLObjectHandler(I instance, Handler parent) {
640             super(instance, parent);
641         }
642 
643         @Override
644         public void endElement(String uri, String localName, String qName) throws SAXException {
645             super.endElement(uri, localName, qName);
646 
647             if (DIDLObject.Property.DC.NAMESPACE.URI.equals(uri)) {
648 
649                 if ("title".equals(localName)) {
650                     getInstance().setTitle(getCharacters());
651                 } else if ("creator".equals(localName)) {
652                     getInstance().setCreator(getCharacters());
653                 } else if ("description".equals(localName)) {
654                     getInstance().addProperty(new DIDLObject.Property.DC.DESCRIPTION(getCharacters()));
655                 } else if ("publisher".equals(localName)) {
656                     getInstance().addProperty(new DIDLObject.Property.DC.PUBLISHER(new Person(getCharacters())));
657                 } else if ("contributor".equals(localName)) {
658                     getInstance().addProperty(new DIDLObject.Property.DC.CONTRIBUTOR(new Person(getCharacters())));
659                 } else if ("date".equals(localName)) {
660                     getInstance().addProperty(new DIDLObject.Property.DC.DATE(getCharacters()));
661                 } else if ("language".equals(localName)) {
662                     getInstance().addProperty(new DIDLObject.Property.DC.LANGUAGE(getCharacters()));
663                 } else if ("rights".equals(localName)) {
664                     getInstance().addProperty(new DIDLObject.Property.DC.RIGHTS(getCharacters()));
665                 } else if ("relation".equals(localName)) {
666                     getInstance().addProperty(new DIDLObject.Property.DC.RELATION(URI.create(getCharacters())));
667                 }
668 
669             } else if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) {
670 
671                 if ("writeStatus".equals(localName)) {
672                     try {
673                         getInstance().setWriteStatus(
674                             WriteStatus.valueOf(getCharacters())
675                         );
676                     } catch (Exception ex) {
677                         log.info("Ignoring invalid writeStatus value: " + getCharacters());
678                     }
679                 } else if ("class".equals(localName)) {
680                     getInstance().setClazz(
681                         new DIDLObject.Class(
682                             getCharacters(),
683                             getAttributes().getValue("name")
684                         )
685                     );
686                 } else if ("artist".equals(localName)) {
687                     getInstance().addProperty(
688                         new DIDLObject.Property.UPNP.ARTIST(
689                             new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
690                         )
691                     );
692                 } else if ("actor".equals(localName)) {
693                     getInstance().addProperty(
694                         new DIDLObject.Property.UPNP.ACTOR(
695                             new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
696                         )
697                     );
698                 } else if ("author".equals(localName)) {
699                     getInstance().addProperty(
700                         new DIDLObject.Property.UPNP.AUTHOR(
701                             new PersonWithRole(getCharacters(), getAttributes().getValue("role"))
702                         )
703                     );
704                 } else if ("producer".equals(localName)) {
705                     getInstance().addProperty(
706                         new DIDLObject.Property.UPNP.PRODUCER(new Person(getCharacters()))
707                     );
708                 } else if ("director".equals(localName)) {
709                     getInstance().addProperty(
710                         new DIDLObject.Property.UPNP.DIRECTOR(new Person(getCharacters()))
711                     );
712                 } else if ("longDescription".equals(localName)) {
713                     getInstance().addProperty(
714                         new DIDLObject.Property.UPNP.LONG_DESCRIPTION(getCharacters())
715                     );
716                 } else if ("storageUsed".equals(localName)) {
717                     getInstance().addProperty(
718                         new DIDLObject.Property.UPNP.STORAGE_USED(Long.valueOf(getCharacters()))
719                     );
720                 } else if ("storageTotal".equals(localName)) {
721                     getInstance().addProperty(
722                         new DIDLObject.Property.UPNP.STORAGE_TOTAL(Long.valueOf(getCharacters()))
723                     );
724                 } else if ("storageFree".equals(localName)) {
725                     getInstance().addProperty(
726                         new DIDLObject.Property.UPNP.STORAGE_FREE(Long.valueOf(getCharacters()))
727                     );
728                 } else if ("storageMaxPartition".equals(localName)) {
729                     getInstance().addProperty(
730                         new DIDLObject.Property.UPNP.STORAGE_MAX_PARTITION(Long.valueOf(getCharacters()))
731                     );
732                 } else if ("storageMedium".equals(localName)) {
733                     getInstance().addProperty(
734                         new DIDLObject.Property.UPNP.STORAGE_MEDIUM(StorageMedium.valueOrVendorSpecificOf(getCharacters()))
735                     );
736                 } else if ("genre".equals(localName)) {
737                     getInstance().addProperty(
738                         new DIDLObject.Property.UPNP.GENRE(getCharacters())
739                     );
740                 } else if ("album".equals(localName)) {
741                     getInstance().addProperty(
742                         new DIDLObject.Property.UPNP.ALBUM(getCharacters())
743                     );
744                 } else if ("playlist".equals(localName)) {
745                     getInstance().addProperty(
746                         new DIDLObject.Property.UPNP.PLAYLIST(getCharacters())
747                     );
748                 } else if ("region".equals(localName)) {
749                     getInstance().addProperty(
750                         new DIDLObject.Property.UPNP.REGION(getCharacters())
751                     );
752                 } else if ("rating".equals(localName)) {
753                     getInstance().addProperty(
754                         new DIDLObject.Property.UPNP.RATING(getCharacters())
755                     );
756                 } else if ("toc".equals(localName)) {
757                     getInstance().addProperty(
758                         new DIDLObject.Property.UPNP.TOC(getCharacters())
759                     );
760                 } else if ("albumArtURI".equals(localName)) {
761                     DIDLObject.Property albumArtURI = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(getCharacters()));
762 
763                     Attributes albumArtURIAttributes = getAttributes();
764                     for (int i = 0; i < albumArtURIAttributes.getLength(); i++) {
765                         if ("profileID".equals(albumArtURIAttributes.getLocalName(i))) {
766                             albumArtURI.addAttribute(
767                                 new DIDLObject.Property.DLNA.PROFILE_ID(
768                                     new DIDLAttribute(
769                                         DIDLObject.Property.DLNA.NAMESPACE.URI,
770                                         "dlna",
771                                         albumArtURIAttributes.getValue(i))
772                                 ));
773                         }
774                     }
775 
776                     getInstance().addProperty(albumArtURI);
777                 } else if ("artistDiscographyURI".equals(localName)) {
778                     getInstance().addProperty(
779                         new DIDLObject.Property.UPNP.ARTIST_DISCO_URI(URI.create(getCharacters()))
780                     );
781                 } else if ("lyricsURI".equals(localName)) {
782                     getInstance().addProperty(
783                         new DIDLObject.Property.UPNP.LYRICS_URI(URI.create(getCharacters()))
784                     );
785                 } else if ("icon".equals(localName)) {
786                     getInstance().addProperty(
787                         new DIDLObject.Property.UPNP.ICON(URI.create(getCharacters()))
788                     );
789                 } else if ("radioCallSign".equals(localName)) {
790                     getInstance().addProperty(
791                         new DIDLObject.Property.UPNP.RADIO_CALL_SIGN(getCharacters())
792                     );
793                 } else if ("radioStationID".equals(localName)) {
794                     getInstance().addProperty(
795                         new DIDLObject.Property.UPNP.RADIO_STATION_ID(getCharacters())
796                     );
797                 } else if ("radioBand".equals(localName)) {
798                     getInstance().addProperty(
799                         new DIDLObject.Property.UPNP.RADIO_BAND(getCharacters())
800                     );
801                 } else if ("channelNr".equals(localName)) {
802                     getInstance().addProperty(
803                         new DIDLObject.Property.UPNP.CHANNEL_NR(Integer.valueOf(getCharacters()))
804                     );
805                 } else if ("channelName".equals(localName)) {
806                     getInstance().addProperty(
807                         new DIDLObject.Property.UPNP.CHANNEL_NAME(getCharacters())
808                     );
809                 } else if ("scheduledStartTime".equals(localName)) {
810                     getInstance().addProperty(
811                         new DIDLObject.Property.UPNP.SCHEDULED_START_TIME(getCharacters())
812                     );
813                 } else if ("scheduledEndTime".equals(localName)) {
814                     getInstance().addProperty(
815                         new DIDLObject.Property.UPNP.SCHEDULED_END_TIME(getCharacters())
816                     );
817                 } else if ("DVDRegionCode".equals(localName)) {
818                     getInstance().addProperty(
819                         new DIDLObject.Property.UPNP.DVD_REGION_CODE(Integer.valueOf(getCharacters()))
820                     );
821                 } else if ("originalTrackNumber".equals(localName)) {
822                     getInstance().addProperty(
823                         new DIDLObject.Property.UPNP.ORIGINAL_TRACK_NUMBER(Integer.valueOf(getCharacters()))
824                     );
825                 } else if ("userAnnotation".equals(localName)) {
826                     getInstance().addProperty(
827                         new DIDLObject.Property.UPNP.USER_ANNOTATION(getCharacters())
828                     );
829                 }
830             }
831         }
832     }
833 
834     public class RootHandler extends Handler<DIDLContent> {
835 
836         RootHandler(DIDLContent instance, SAXParser parser) {
837             super(instance, parser);
838         }
839 
840         @Override
841         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
842             super.startElement(uri, localName, qName, attributes);
843 
844             if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;
845 
846             if (localName.equals("container")) {
847 
848                 Container container = createContainer(attributes);
849                 getInstance().addContainer(container);
850                 createContainerHandler(container, this);
851 
852             } else if (localName.equals("item")) {
853 
854                 Item item = createItem(attributes);
855                 getInstance().addItem(item);
856                 createItemHandler(item, this);
857 
858             } else if (localName.equals("desc")) {
859 
860                 DescMeta desc = createDescMeta(attributes);
861                 getInstance().addDescMetadata(desc);
862                 createDescMetaHandler(desc, this);
863 
864             }
865         }
866 
867         @Override
868         protected boolean isLastElement(String uri, String localName, String qName) {
869             if (DIDLContent.NAMESPACE_URI.equals(uri) && "DIDL-Lite".equals(localName)) {
870 
871                 // Now transform all the generically typed Container and Item instances into
872                 // more specific Album, MusicTrack, etc. instances
873                 getInstance().replaceGenericContainerAndItems();
874 
875                 return true;
876             }
877             return false;
878         }
879     }
880 
881     public class ContainerHandler extends DIDLObjectHandler<Container> {
882         public ContainerHandler(Container instance, Handler parent) {
883             super(instance, parent);
884         }
885 
886         @Override
887         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
888             super.startElement(uri, localName, qName, attributes);
889 
890             if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;
891 
892             if (localName.equals("item")) {
893 
894                 Item item = createItem(attributes);
895                 getInstance().addItem(item);
896                 createItemHandler(item, this);
897 
898             } else if (localName.equals("desc")) {
899 
900                 DescMeta desc = createDescMeta(attributes);
901                 getInstance().addDescMetadata(desc);
902                 createDescMetaHandler(desc, this);
903 
904             } else if (localName.equals("res")) {
905 
906                 Res res = createResource(attributes);
907                 if (res != null) {
908                     getInstance().addResource(res);
909                     createResHandler(res, this);
910                 }
911 
912             }
913 
914             // We do NOT support recursive container embedded in container! The schema allows it
915             // but the spec doesn't:
916             //
917             // Section 2.8.3: Incremental navigation i.e. the full hierarchy is never returned
918             // in one call since this is likely to flood the resources available to the control
919             // point (memory, network bandwidth, etc.).
920         }
921 
922         @Override
923         public void endElement(String uri, String localName, String qName) throws SAXException {
924             super.endElement(uri, localName, qName);
925 
926             if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) {
927 
928                 if ("searchClass".equals(localName)) {
929                     getInstance().getSearchClasses().add(
930                         new DIDLObject.Class(
931                             getCharacters(),
932                             getAttributes().getValue("name"),
933                             "true".equals(getAttributes().getValue("includeDerived"))
934                         )
935                     );
936                 } else if ("createClass".equals(localName)) {
937                     getInstance().getCreateClasses().add(
938                         new DIDLObject.Class(
939                             getCharacters(),
940                             getAttributes().getValue("name"),
941                             "true".equals(getAttributes().getValue("includeDerived"))
942                         )
943                     );
944                 }
945             }
946         }
947 
948         @Override
949         protected boolean isLastElement(String uri, String localName, String qName) {
950             if (DIDLContent.NAMESPACE_URI.equals(uri) && "container".equals(localName)) {
951                 if (getInstance().getTitle() == null) {
952                     log.warning("In DIDL content, missing 'dc:title' element for container: " + getInstance().getId());
953                 }
954                 if (getInstance().getClazz() == null) {
955                     log.warning("In DIDL content, missing 'upnp:class' element for container: " + getInstance().getId());
956                 }
957                 return true;
958             }
959             return false;
960         }
961     }
962 
963     public class ItemHandler extends DIDLObjectHandler<Item> {
964         public ItemHandler(Item instance, Handler parent) {
965             super(instance, parent);
966         }
967 
968         @Override
969         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
970             super.startElement(uri, localName, qName, attributes);
971 
972             if (!DIDLContent.NAMESPACE_URI.equals(uri)) return;
973 
974             if (localName.equals("res")) {
975 
976                 Res res = createResource(attributes);
977                 if (res != null) {
978                     getInstance().addResource(res);
979                     createResHandler(res, this);
980                 }
981 
982             } else if (localName.equals("desc")) {
983 
984                 DescMeta desc = createDescMeta(attributes);
985                 getInstance().addDescMetadata(desc);
986                 createDescMetaHandler(desc, this);
987 
988             }
989         }
990 
991         @Override
992         protected boolean isLastElement(String uri, String localName, String qName) {
993             if (DIDLContent.NAMESPACE_URI.equals(uri) && "item".equals(localName)) {
994                 if (getInstance().getTitle() == null) {
995                     log.warning("In DIDL content, missing 'dc:title' element for item: " + getInstance().getId());
996                 }
997                 if (getInstance().getClazz() == null) {
998                     log.warning("In DIDL content, missing 'upnp:class' element for item: " + getInstance().getId());
999                 }
1000                 return true;
1001             }
1002             return false;
1003         }
1004     }
1005 
1006     protected class ResHandler extends Handler<Res> {
1007         public ResHandler(Res instance, Handler parent) {
1008             super(instance, parent);
1009         }
1010 
1011         @Override
1012         public void endElement(String uri, String localName, String qName) throws SAXException {
1013             super.endElement(uri, localName, qName);
1014             getInstance().setValue(getCharacters());
1015         }
1016 
1017         @Override
1018         protected boolean isLastElement(String uri, String localName, String qName) {
1019             return DIDLContent.NAMESPACE_URI.equals(uri) && "res".equals(localName);
1020         }
1021     }
1022 
1023     /**
1024      * Extracts an <code>org.w3c.Document</code> from the nested elements in the {@code <desc>} element.
1025      * <p>
1026      * The root element of this document is a wrapper in the namespace
1027      * {@link org.fourthline.cling.support.model.DIDLContent#DESC_WRAPPER_NAMESPACE_URI}.
1028      * </p>
1029      */
1030     public class DescMetaHandler extends Handler<DescMeta> {
1031 
1032         protected Element current;
1033 
1034         public DescMetaHandler(DescMeta instance, Handler parent) {
1035             super(instance, parent);
1036             instance.setMetadata(instance.createMetadataDocument());
1037             current = getInstance().getMetadata().getDocumentElement();
1038         }
1039 
1040         @Override
1041         public DescMeta<Document> getInstance() {
1042             return super.getInstance();
1043         }
1044 
1045         @Override
1046         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
1047             super.startElement(uri, localName, qName, attributes);
1048 
1049             Element newEl = getInstance().getMetadata().createElementNS(uri, qName);
1050             for (int i = 0; i < attributes.getLength(); i++) {
1051                 newEl.setAttributeNS(
1052                     attributes.getURI(i),
1053                     attributes.getQName(i),
1054                     attributes.getValue(i)
1055                 );
1056             }
1057             current.appendChild(newEl);
1058             current = newEl;
1059         }
1060 
1061         @Override
1062         public void endElement(String uri, String localName, String qName) throws SAXException {
1063             super.endElement(uri, localName, qName);
1064             if (isLastElement(uri, localName, qName)) return;
1065 
1066             // Ignore whitespace
1067             if (getCharacters().length() > 0 && !getCharacters().matches("[\\t\\n\\x0B\\f\\r\\s]+"))
1068                 current.appendChild(getInstance().getMetadata().createTextNode(getCharacters()));
1069 
1070             current = (Element) current.getParentNode();
1071 
1072             // Reset this so we can continue parsing child nodes with this handler
1073             characters = new StringBuilder();
1074             attributes = null;
1075         }
1076 
1077         @Override
1078         protected boolean isLastElement(String uri, String localName, String qName) {
1079             return DIDLContent.NAMESPACE_URI.equals(uri) && "desc".equals(localName);
1080         }
1081     }
1082 }