/*
* 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);
}
}
}