/* * Copyright (C) 2011 Teleal GmbH, Switzerland * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.teleal.cling.support.contentdirectory; import org.teleal.cling.model.types.Datatype; import org.teleal.cling.model.types.InvalidValueException; import org.teleal.cling.support.model.DIDLAttribute; import org.teleal.cling.support.model.DIDLContent; import org.teleal.cling.support.model.DIDLObject; import org.teleal.cling.support.model.DescMeta; import org.teleal.cling.support.model.Person; import org.teleal.cling.support.model.PersonWithRole; import org.teleal.cling.support.model.ProtocolInfo; import org.teleal.cling.support.model.Res; import org.teleal.cling.support.model.StorageMedium; import org.teleal.cling.support.model.WriteStatus; import org.teleal.cling.support.model.container.Container; import org.teleal.cling.support.model.item.Item; import org.teleal.common.io.IO; import org.teleal.common.util.Exceptions; import org.teleal.common.xml.SAXParser; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.net.URI; import java.util.logging.Level; import java.util.logging.Logger; import static org.teleal.cling.model.XMLUtil.appendNewElement; import static org.teleal.cling.model.XMLUtil.appendNewElementIfNotNull; /** * DIDL parser based on SAX for reading and DOM for writing. * <p> * This parser requires Android platform level 8 (2.2). * </p> * <p> * Override the {@link #createDescMetaHandler(org.teleal.cling.support.model.DescMeta, org.teleal.common.xml.SAXParser.Handler)} * method to read vendor extension content of {@code <desc>} elements. You then should also override the * {@link #populateDescMetadata(org.w3c.dom.Element, org.teleal.cling.support.model.DescMeta)} method for writing. * </p> * <p> * Override the {@link #createItemHandler(org.teleal.cling.support.model.item.Item, org.teleal.common.xml.SAXParser.Handler)} * etc. methods to register custom handlers for vendor-specific elements and attributes within items, containers, * and so on. * </p> * * @author Christian Bauer * @author Mario Franco */ public class DIDLParser extends SAXParser { final private static Logger log = Logger.getLogger(DIDLParser.class.getName()); /** * Uses the current thread's context classloader to read and unmarshall the given resource. * * @param resource The resource on the classpath. * @return The unmarshalled DIDL content model. * @throws Exception */ public DIDLContent parseResource(String resource) throws Exception { InputStream is = null; try { is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource); return parse(IO.readLines(is)); } finally { if (is != null) is.close(); } } /** * Reads and unmarshalls an XML representation into a DIDL content model. * * @param xml The XML representation. * @return A DIDL content model. * @throws Exception */ public DIDLContent parse(String xml) throws Exception { if (xml == null || xml.length() == 0) { throw new RuntimeException("Null or empty XML"); } DIDLContent content = new DIDLContent(); createRootHandler(content, this); log.fine("Parsing DIDL XML content"); parse(new InputSource(new StringReader(xml))); return content; } protected RootHandler createRootHandler(DIDLContent instance, SAXParser parser) { return new RootHandler(instance, parser); } protected ContainerHandler createContainerHandler(Container instance, Handler parent) { return new ContainerHandler(instance, parent); } protected ItemHandler createItemHandler(Item instance, Handler parent) { return new ItemHandler(instance, parent); } protected ResHandler createResHandler(Res instance, Handler parent) { return new ResHandler(instance, parent); } protected DescMetaHandler createDescMetaHandler(DescMeta instance, Handler parent) { return new DescMetaHandler(instance, parent); } protected Container createContainer(Attributes attributes) { Container container = new Container(); container.setId(attributes.getValue("id")); container.setParentID(attributes.getValue("parentID")); if ((attributes.getValue("childCount") != null)) container.setChildCount(Integer.valueOf(attributes.getValue("childCount"))); try { Boolean value = (Boolean)Datatype.Builtin.BOOLEAN.getDatatype().valueOf( attributes.getValue("restricted") ); if (value != null) container.setRestricted(value); value = (Boolean)Datatype.Builtin.BOOLEAN.getDatatype().valueOf( attributes.getValue("searchable") ); if (value != null) container.setSearchable(value); } catch (Exception ex) { // Ignore } return container; } protected Item createItem(Attributes attributes) { Item item = new Item(); item.setId(attributes.getValue("id")); item.setParentID(attributes.getValue("parentID")); try { Boolean value = (Boolean)Datatype.Builtin.BOOLEAN.getDatatype().valueOf( attributes.getValue("restricted") ); if (value != null) item.setRestricted(value); } catch (Exception ex) { // Ignore } if ((attributes.getValue("refID") != null)) item.setRefID(attributes.getValue("refID")); return item; } protected Res createResource(Attributes attributes) { Res res = new Res(); if (attributes.getValue("importUri") != null) res.setImportUri(URI.create(attributes.getValue("importUri"))); try { res.setProtocolInfo( new ProtocolInfo(attributes.getValue("protocolInfo")) ); } catch (InvalidValueException ex) { log.warning("In DIDL content, invalid resource protocol info: " + Exceptions.unwrap(ex)); return null; } if (attributes.getValue("size") != null) res.setSize(Long.valueOf(attributes.getValue("size"))); if (attributes.getValue("duration") != null) res.setDuration(attributes.getValue("duration")); if (attributes.getValue("bitrate") != null) res.setBitrate(Long.valueOf(attributes.getValue("bitrate"))); if (attributes.getValue("sampleFrequency") != null) res.setSampleFrequency(Long.valueOf(attributes.getValue("sampleFrequency"))); if (attributes.getValue("bitsPerSample") != null) res.setBitsPerSample(Long.valueOf(attributes.getValue("bitsPerSample"))); if (attributes.getValue("nrAudioChannels") != null) res.setNrAudioChannels(Long.valueOf(attributes.getValue("nrAudioChannels"))); if (attributes.getValue("colorDepth") != null) res.setColorDepth(Long.valueOf(attributes.getValue("colorDepth"))); if (attributes.getValue("protection") != null) res.setProtection(attributes.getValue("protection")); if (attributes.getValue("resolution") != null) res.setResolution(attributes.getValue("resolution")); return res; } protected DescMeta createDescMeta(Attributes attributes) { DescMeta desc = new DescMeta(); desc.setId(attributes.getValue("id")); if ((attributes.getValue("type") != null)) desc.setType(attributes.getValue("type")); if ((attributes.getValue("nameSpace") != null)) desc.setNameSpace(URI.create(attributes.getValue("nameSpace"))); return desc; } /* ############################################################################################# */ /** * Generates a XML representation of the content model. * <p> * Items inside a container will <em>not</em> be represented in the XML, the containers * will be rendered flat without children. * </p> * * @param content The content model. * @return An XML representation. * @throws Exception */ public String generate(DIDLContent content) throws Exception { return generate(content, false); } /** * Generates an XML representation of the content model. * <p> * Optionally, items inside a container will be represented in the XML, * the container elements then have nested item elements. Although this * parser can read such a structure, it is unclear whether other DIDL * parsers should and actually do support this XML. * </p> * * @param content The content model. * @param nestedItems <code>true</code> if nested item elements should be rendered for containers. * @return An XML representation. * @throws Exception */ public String generate(DIDLContent content, boolean nestedItems) throws Exception { return documentToString(buildDOM(content, nestedItems), true); } // TODO: Yes, this only runs on Android 2.2 protected String documentToString(Document document, boolean omitProlog) throws Exception { TransformerFactory transFactory = TransformerFactory.newInstance(); // Indentation not supported on Android 2.2 //transFactory.setAttribute("indent-number", 4); Transformer transformer = transFactory.newTransformer(); if (omitProlog) { // TODO: UPNP VIOLATION: Terratec Noxon Webradio fails when DIDL content has a prolog // No XML prolog! This is allowed because it is UTF-8 encoded and required // because broken devices will stumble on SOAP messages that contain (even // encoded) XML prologs within a message body. transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); } // Again, Android 2.2 fails hard if you try this. //transformer.setOutputProperty(OutputKeys.INDENT, "yes"); StringWriter out = new StringWriter(); transformer.transform(new DOMSource(document), new StreamResult(out)); return out.toString(); } protected Document buildDOM(DIDLContent content, boolean nestedItems) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); Document d = factory.newDocumentBuilder().newDocument(); generateRoot(content, d, nestedItems); return d; } protected void generateRoot(DIDLContent content, Document descriptor, boolean nestedItems) { Element rootElement = descriptor.createElementNS(DIDLContent.NAMESPACE_URI, "DIDL-Lite"); descriptor.appendChild(rootElement); // rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:didl", DIDLContent.NAMESPACE_URI); rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:upnp", DIDLObject.Property.UPNP.NAMESPACE.URI); rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:dc", DIDLObject.Property.DC.NAMESPACE.URI); for (Container container : content.getContainers()) { if (container == null) continue; generateContainer(container, descriptor, rootElement, nestedItems); } for (Item item : content.getItems()) { if (item == null) continue; generateItem(item, descriptor, rootElement); } for (DescMeta descMeta : content.getDescMetadata()) { if (descMeta == null) continue; generateDescMetadata(descMeta, descriptor, rootElement); } } protected void generateContainer(Container container, Document descriptor, Element parent, boolean nestedItems) { if (container.getTitle() == null) { throw new RuntimeException("Missing 'dc:title' element for container: " + container.getId()); } if (container.getClazz() == null) { throw new RuntimeException("Missing 'upnp:class' element for container: " + container.getId()); } Element containerElement = appendNewElement(descriptor, parent, "container"); if (container.getId() == null) throw new NullPointerException("Missing id on container: " + container); containerElement.setAttribute("id", container.getId()); if (container.getParentID() == null) throw new NullPointerException("Missing parent id on container: " + container); containerElement.setAttribute("parentID", container.getParentID()); if (container.getChildCount() != null) { containerElement.setAttribute("childCount", Integer.toString(container.getChildCount())); } containerElement.setAttribute("restricted", Boolean.toString(container.isRestricted())); containerElement.setAttribute("searchable", Boolean.toString(container.isSearchable())); appendNewElementIfNotNull( descriptor, containerElement, "dc:title", container.getTitle(), DIDLObject.Property.DC.NAMESPACE.URI ); appendNewElementIfNotNull( descriptor, containerElement, "dc:creator", container.getCreator(), DIDLObject.Property.DC.NAMESPACE.URI ); appendNewElementIfNotNull( descriptor, containerElement, "upnp:writeStatus", container.getWriteStatus(), DIDLObject.Property.UPNP.NAMESPACE.URI ); appendClass(descriptor, containerElement, container.getClazz(), "upnp:class", false); for (DIDLObject.Class searchClass : container.getSearchClasses()) { appendClass(descriptor, containerElement, searchClass, "upnp:searchClass", true); } for (DIDLObject.Class createClass : container.getCreateClasses()) { appendClass(descriptor, containerElement, createClass, "upnp:createClass", true); } appendProperties(descriptor, containerElement, container, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI); appendProperties(descriptor, containerElement, container, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI); if (nestedItems) { for (Item item : container.getItems()) { if (item == null) continue; generateItem(item, descriptor, containerElement); } } for (Res resource : container.getResources()) { if (resource == null) continue; generateResource(resource, descriptor, containerElement); } for (DescMeta descMeta : container.getDescMetadata()) { if (descMeta == null) continue; generateDescMetadata(descMeta, descriptor, containerElement); } } protected void generateItem(Item item, Document descriptor, Element parent) { if (item.getTitle() == null) { throw new RuntimeException("Missing 'dc:title' element for item: " + item.getId()); } if (item.getClazz() == null) { throw new RuntimeException("Missing 'upnp:class' element for item: " + item.getId()); } Element itemElement = appendNewElement(descriptor, parent, "item"); if (item.getId() == null) throw new NullPointerException("Missing id on item: " + item); itemElement.setAttribute("id", item.getId()); if (item.getParentID() == null) throw new NullPointerException("Missing parent id on item: " + item); itemElement.setAttribute("parentID", item.getParentID()); if (item.getRefID() != null) itemElement.setAttribute("refID", item.getRefID()); itemElement.setAttribute("restricted", Boolean.toString(item.isRestricted())); appendNewElementIfNotNull( descriptor, itemElement, "dc:title", item.getTitle(), DIDLObject.Property.DC.NAMESPACE.URI ); appendNewElementIfNotNull( descriptor, itemElement, "dc:creator", item.getCreator(), DIDLObject.Property.DC.NAMESPACE.URI ); appendNewElementIfNotNull( descriptor, itemElement, "upnp:writeStatus", item.getWriteStatus(), DIDLObject.Property.UPNP.NAMESPACE.URI ); appendClass(descriptor, itemElement, item.getClazz(), "upnp:class", false); appendProperties(descriptor, itemElement, item, "upnp", DIDLObject.Property.UPNP.NAMESPACE.class, DIDLObject.Property.UPNP.NAMESPACE.URI); appendProperties(descriptor, itemElement, item, "dc", DIDLObject.Property.DC.NAMESPACE.class, DIDLObject.Property.DC.NAMESPACE.URI); for (Res resource : item.getResources()) { if (resource == null) continue; generateResource(resource, descriptor, itemElement); } for (DescMeta descMeta : item.getDescMetadata()) { if (descMeta == null) continue; generateDescMetadata(descMeta, descriptor, itemElement); } } protected void generateResource(Res resource, Document descriptor, Element parent) { if (resource.getValue() == null) { throw new RuntimeException("Missing resource URI value" + resource); } if (resource.getProtocolInfo() == null) { throw new RuntimeException("Missing resource protocol info: " + resource); } Element resourceElement = appendNewElement(descriptor, parent, "res", resource.getValue()); resourceElement.setAttribute("protocolInfo", resource.getProtocolInfo().toString()); if (resource.getImportUri() != null) resourceElement.setAttribute("importUri", resource.getImportUri().toString()); if (resource.getSize() != null) resourceElement.setAttribute("size", resource.getSize().toString()); if (resource.getDuration() != null) resourceElement.setAttribute("duration", resource.getDuration()); if (resource.getBitrate() != null) resourceElement.setAttribute("bitrate", resource.getBitrate().toString()); if (resource.getSampleFrequency() != null) resourceElement.setAttribute("sampleFrequency", resource.getSampleFrequency().toString()); if (resource.getBitsPerSample() != null) resourceElement.setAttribute("bitsPerSample", resource.getBitsPerSample().toString()); if (resource.getNrAudioChannels() != null) resourceElement.setAttribute("nrAudioChannels", resource.getNrAudioChannels().toString()); if (resource.getColorDepth() != null) resourceElement.setAttribute("colorDepth", resource.getColorDepth().toString()); if (resource.getProtection() != null) resourceElement.setAttribute("protection", resource.getProtection()); if (resource.getResolution() != null) resourceElement.setAttribute("resolution", resource.getResolution()); } protected void generateDescMetadata(DescMeta descMeta, Document descriptor, Element parent) { if (descMeta.getId() == null) { throw new RuntimeException("Missing id of description metadata: " + descMeta); } if (descMeta.getNameSpace() == null) { throw new RuntimeException("Missing namespace of description metadata: " + descMeta); } Element descElement = appendNewElement(descriptor, parent, "desc"); descElement.setAttribute("id", descMeta.getId()); descElement.setAttribute("nameSpace", descMeta.getNameSpace().toString()); if (descMeta.getType() != null) descElement.setAttribute("type", descMeta.getType()); populateDescMetadata(descElement, descMeta); } /** * Expects an <code>org.w3c.Document</code> as metadata, copies nodes of the document into the DIDL content. * <p> * This method will ignore the content and log a warning if it's of the wrong type. If you override * {@link #createDescMetaHandler(org.teleal.cling.support.model.DescMeta, org.teleal.common.xml.SAXParser.Handler)}, * you most likely also want to override this method. * </p> * * @param descElement The DIDL content {@code <desc>} element wrapping the final metadata. * @param descMeta The metadata with a <code>org.w3c.Document</code> payload. */ protected void populateDescMetadata(Element descElement, DescMeta descMeta) { if (descMeta.getMetadata() instanceof Document) { Document doc = (Document) descMeta.getMetadata(); NodeList nl = doc.getDocumentElement().getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node n = nl.item(i); if (n.getNodeType() != Node.ELEMENT_NODE) continue; Node clone = descElement.getOwnerDocument().importNode(n, true); descElement.appendChild(clone); } } else { log.warning("Unknown desc metadata content, please override populateDescMetadata(): " + descMeta.getMetadata()); } } protected void appendProperties(Document descriptor, Element parent, DIDLObject object, String prefix, Class<? extends DIDLObject.Property.NAMESPACE> namespace, String namespaceURI) { for (DIDLObject.Property<Object> property : object.getPropertiesByNamespace(namespace)) { Element el = descriptor.createElementNS(namespaceURI, prefix + ":" + property.getDescriptorName()); parent.appendChild(el); property.setOnElement(el); } } protected void appendClass(Document descriptor, Element parent, DIDLObject.Class clazz, String element, boolean appendDerivation) { Element classElement = appendNewElementIfNotNull( descriptor, parent, element, clazz.getValue(), DIDLObject.Property.UPNP.NAMESPACE.URI ); if (clazz.getFriendlyName() != null && clazz.getFriendlyName().length() > 0) classElement.setAttribute("name", clazz.getFriendlyName()); if (appendDerivation) classElement.setAttribute("includeDerived", Boolean.toString(clazz.isIncludeDerived())); } /** * Sends the given string to the log with <code>Level.FINE</code>, if that log level is enabled. * * @param s The string to send to the log. */ public void debugXML(String s) { if (log.isLoggable(Level.FINE)) { log.fine("-------------------------------------------------------------------------------------"); log.fine("\n" + s); log.fine("-------------------------------------------------------------------------------------"); } } /* ############################################################################################# */ public abstract class DIDLObjectHandler<I extends DIDLObject> extends Handler<I> { protected DIDLObjectHandler(I instance, Handler parent) { super(instance, parent); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if (DIDLObject.Property.DC.NAMESPACE.URI.equals(uri)) { if ("title".equals(localName)) { getInstance().setTitle(getCharacters()); } else if ("creator".equals(localName)) { getInstance().setCreator(getCharacters()); } else if ("description".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.DESCRIPTION(getCharacters())); } else if ("publisher".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.PUBLISHER(new Person(getCharacters()))); } else if ("contributor".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.CONTRIBUTOR(new Person(getCharacters()))); } else if ("date".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.DATE(getCharacters())); } else if ("language".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.LANGUAGE(getCharacters())); } else if ("rights".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.RIGHTS(getCharacters())); } else if ("relation".equals(localName)) { getInstance().addProperty(new DIDLObject.Property.DC.RELATION(URI.create(getCharacters()))); } } else if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) { if ("writeStatus".equals(localName)) { try { getInstance().setWriteStatus( WriteStatus.valueOf(getCharacters()) ); } catch (Exception ex) { log.info("Ignoring invalid writeStatus value: " + getCharacters()); } } else if ("class".equals(localName)) { getInstance().setClazz( new DIDLObject.Class( getCharacters(), getAttributes().getValue("name") ) ); } else if ("artist".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ARTIST( new PersonWithRole(getCharacters(), getAttributes().getValue("role")) ) ); } else if ("actor".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ACTOR( new PersonWithRole(getCharacters(), getAttributes().getValue("role")) ) ); } else if ("author".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.AUTHOR( new PersonWithRole(getCharacters(), getAttributes().getValue("role")) ) ); } else if ("producer".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.PRODUCER(new Person(getCharacters())) ); } else if ("director".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.DIRECTOR(new Person(getCharacters())) ); } else if ("longDescription".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.LONG_DESCRIPTION(getCharacters()) ); } else if ("storageUsed".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.STORAGE_USED(Long.valueOf(getCharacters())) ); } else if ("storageTotal".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.STORAGE_TOTAL(Long.valueOf(getCharacters())) ); } else if ("storageFree".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.STORAGE_FREE(Long.valueOf(getCharacters())) ); } else if ("storageMaxPartition".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.STORAGE_MAX_PARTITION(Long.valueOf(getCharacters())) ); } else if ("storageMedium".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.STORAGE_MEDIUM(StorageMedium.valueOrVendorSpecificOf(getCharacters())) ); } else if ("genre".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.GENRE(getCharacters()) ); } else if ("album".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ALBUM(getCharacters()) ); } else if ("playlist".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.PLAYLIST(getCharacters()) ); } else if ("region".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.REGION(getCharacters()) ); } else if ("rating".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.RATING(getCharacters()) ); } else if ("toc".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.TOC(getCharacters()) ); } else if ("albumArtURI".equals(localName)) { DIDLObject.Property albumArtURI = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(getCharacters())); Attributes albumArtURIAttributes = getAttributes(); for (int i = 0; i < albumArtURIAttributes.getLength(); i++) { if ("profileID".equals(albumArtURIAttributes.getLocalName(i))) { albumArtURI.addAttribute( new DIDLObject.Property.DLNA.PROFILE_ID( new DIDLAttribute( DIDLObject.Property.DLNA.NAMESPACE.URI, "dlna", albumArtURIAttributes.getValue(i)) )); } } getInstance().addProperty(albumArtURI); } else if ("artistDiscographyURI".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ARTIST_DISCO_URI(URI.create(getCharacters())) ); } else if ("lyricsURI".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.LYRICS_URI(URI.create(getCharacters())) ); } else if ("icon".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ICON(URI.create(getCharacters())) ); } else if ("radioCallSign".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.RADIO_CALL_SIGN(getCharacters()) ); } else if ("radioStationID".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.RADIO_STATION_ID(getCharacters()) ); } else if ("radioBand".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.RADIO_BAND(getCharacters()) ); } else if ("channelNr".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.CHANNEL_NR(Integer.valueOf(getCharacters())) ); } else if ("channelName".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.CHANNEL_NAME(getCharacters()) ); } else if ("scheduledStartTime".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.SCHEDULED_START_TIME(getCharacters()) ); } else if ("scheduledEndTime".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.SCHEDULED_END_TIME(getCharacters()) ); } else if ("DVDRegionCode".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.DVD_REGION_CODE(Integer.valueOf(getCharacters())) ); } else if ("originalTrackNumber".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.ORIGINAL_TRACK_NUMBER(Integer.valueOf(getCharacters())) ); } else if ("userAnnotation".equals(localName)) { getInstance().addProperty( new DIDLObject.Property.UPNP.USER_ANNOTATION(getCharacters()) ); } } } } public class RootHandler extends Handler<DIDLContent> { RootHandler(DIDLContent instance, SAXParser parser) { super(instance, parser); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (!DIDLContent.NAMESPACE_URI.equals(uri)) return; if (localName.equals("container")) { Container container = createContainer(attributes); getInstance().addContainer(container); createContainerHandler(container, this); } else if (localName.equals("item")) { Item item = createItem(attributes); getInstance().addItem(item); createItemHandler(item, this); } else if (localName.equals("desc")) { DescMeta desc = createDescMeta(attributes); getInstance().addDescMetadata(desc); createDescMetaHandler(desc, this); } } @Override protected boolean isLastElement(String uri, String localName, String qName) { if (DIDLContent.NAMESPACE_URI.equals(uri) && "DIDL-Lite".equals(localName)) { // Now transform all the generically typed Container and Item instances into // more specific Album, MusicTrack, etc. instances getInstance().replaceGenericContainerAndItems(); return true; } return false; } } public class ContainerHandler extends DIDLObjectHandler<Container> { public ContainerHandler(Container instance, Handler parent) { super(instance, parent); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (!DIDLContent.NAMESPACE_URI.equals(uri)) return; if (localName.equals("item")) { Item item = createItem(attributes); getInstance().addItem(item); createItemHandler(item, this); } else if (localName.equals("desc")) { DescMeta desc = createDescMeta(attributes); getInstance().addDescMetadata(desc); createDescMetaHandler(desc, this); } else if (localName.equals("res")) { Res res = createResource(attributes); if (res != null) { getInstance().addResource(res); createResHandler(res, this); } } // We do NOT support recursive container embedded in container! The schema allows it // but the spec doesn't: // // Section 2.8.3: Incremental navigation i.e. the full hierarchy is never returned // in one call since this is likely to flood the resources available to the control // point (memory, network bandwidth, etc.). } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if (DIDLObject.Property.UPNP.NAMESPACE.URI.equals(uri)) { if ("searchClass".equals(localName)) { getInstance().getSearchClasses().add( new DIDLObject.Class( getCharacters(), getAttributes().getValue("name"), "true".equals(getAttributes().getValue("includeDerived")) ) ); } else if ("createClass".equals(localName)) { getInstance().getCreateClasses().add( new DIDLObject.Class( getCharacters(), getAttributes().getValue("name"), "true".equals(getAttributes().getValue("includeDerived")) ) ); } } } @Override protected boolean isLastElement(String uri, String localName, String qName) { if (DIDLContent.NAMESPACE_URI.equals(uri) && "container".equals(localName)) { if (getInstance().getTitle() == null) { log.warning("In DIDL content, missing 'dc:title' element for container: " + getInstance().getId()); } if (getInstance().getClazz() == null) { log.warning("In DIDL content, missing 'upnp:class' element for container: " + getInstance().getId()); } return true; } return false; } } public class ItemHandler extends DIDLObjectHandler<Item> { public ItemHandler(Item instance, Handler parent) { super(instance, parent); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (!DIDLContent.NAMESPACE_URI.equals(uri)) return; if (localName.equals("res")) { Res res = createResource(attributes); if (res != null) { getInstance().addResource(res); createResHandler(res, this); } } else if (localName.equals("desc")) { DescMeta desc = createDescMeta(attributes); getInstance().addDescMetadata(desc); createDescMetaHandler(desc, this); } } @Override protected boolean isLastElement(String uri, String localName, String qName) { if (DIDLContent.NAMESPACE_URI.equals(uri) && "item".equals(localName)) { if (getInstance().getTitle() == null) { log.warning("In DIDL content, missing 'dc:title' element for item: " + getInstance().getId()); } if (getInstance().getClazz() == null) { log.warning("In DIDL content, missing 'upnp:class' element for item: " + getInstance().getId()); } return true; } return false; } } protected class ResHandler extends Handler<Res> { public ResHandler(Res instance, Handler parent) { super(instance, parent); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); getInstance().setValue(getCharacters()); } @Override protected boolean isLastElement(String uri, String localName, String qName) { return DIDLContent.NAMESPACE_URI.equals(uri) && "res".equals(localName); } } /** * Extracts an <code>org.w3c.Document</code> from the nested elements in the {@code <desc>} element. * <p> * The root element of this document is a wrapper in the namespace * {@link org.teleal.cling.support.model.DIDLContent#DESC_WRAPPER_NAMESPACE_URI}. * </p> */ public class DescMetaHandler extends Handler<DescMeta> { protected Element current; public DescMetaHandler(DescMeta instance, Handler parent) { super(instance, parent); instance.setMetadata(instance.createMetadataDocument()); current = getInstance().getMetadata().getDocumentElement(); } @Override public DescMeta<Document> getInstance() { return super.getInstance(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); Element newEl = getInstance().getMetadata().createElementNS(uri, qName); for (int i = 0; i < attributes.getLength(); i++) { newEl.setAttributeNS( attributes.getURI(i), attributes.getQName(i), attributes.getValue(i) ); } current.appendChild(newEl); current = newEl; } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if (isLastElement(uri, localName, qName)) return; // Ignore whitespace if (getCharacters().length() > 0 && !getCharacters().matches("[\\t\\n\\x0B\\f\\r\\s]+")) current.appendChild(getInstance().getMetadata().createTextNode(getCharacters())); current = (Element) current.getParentNode(); // Reset this so we can continue parsing child nodes with this handler characters = new StringBuilder(); attributes = null; } @Override protected boolean isLastElement(String uri, String localName, String qName) { return DIDLContent.NAMESPACE_URI.equals(uri) && "desc".equals(localName); } } }