/*
* This software copyright by various authors including the RPTools.net
* development team, and licensed under the LGPL Version 3 or, at your
* option, any later version.
*
* Portions of this software were originally covered under the Apache
* Software License, Version 1.1 or Version 2.0.
*
* See the file LICENSE elsewhere in this distribution for license details.
*/
package net.sbbi.upnp.devices;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import net.sbbi.upnp.JXPathParser;
import net.sbbi.upnp.services.UPNPService;
import org.apache.commons.jxpath.Container;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.xml.DocumentContainer;
import org.apache.log4j.Logger;
/**
* Root UPNP device that is contained in a device definition file. Slightly differs from a simple UPNPDevice object.
* This object will contains all the child devices, this is the top objet in the UPNP device devices hierarchy.
*
* @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
* @version 1.0
*/
public class UPNPRootDevice extends UPNPDevice {
private final static Logger log = Logger.getLogger(UPNPRootDevice.class);
private final int specVersionMajor;
private final int specVersionMinor;
private URL URLBase;
private long validityTime;
private long creationTime;
private final URL deviceDefLoc;
private String deviceDefLocData;
private String vendorFirmware;
private String discoveryUSN;
private String discoveryUDN;
private final DocumentContainer UPNPDevice;
/**
* Constructor for the root device, constructs itself from An xml device definition file provided by the UPNP device
* via http normally.
*
* @param deviceDefLoc
* the location of the XML device definition file using "the urn:schemas-upnp-org:device-1-0" namespace
* @param maxAge
* the maximum age of this UPNP device in secs before considered to be outdated
* @param vendorFirmware
* the vendor firmware
* @param discoveryUSN
* the discovery USN used to find and create this device
* @param discoveryUDN
* the discovery UDN used to find and create this device
* @throws MalformedURLException
* if the location URL is invalid and cannot be used to populate this root object and its child devices
* IllegalStateException if the device has an unsupported version, currently only version 1.0 is
* supported
*/
public UPNPRootDevice(URL deviceDefLoc, String maxAge, String vendorFirmware, String discoveryUSN, String discoveryUDN) throws MalformedURLException, IllegalStateException {
this(deviceDefLoc, maxAge);
this.vendorFirmware = vendorFirmware;
this.discoveryUSN = discoveryUSN;
this.discoveryUDN = discoveryUDN;
}
/**
* Constructor for the root device, constructs itself from An xml device definition file provided by the UPNP device
* via http normally.
*
* @param deviceDefLoc
* the location of the XML device definition file using "the urn:schemas-upnp-org:device-1-0" namespace
* @param maxAge
* the maximum age of this UPNP device in secs before considered to be outdated
* @param vendorFirmware
* the vendor firmware
* @throws MalformedURLException
* if the location URL is invalid and cannot be used to populate this root object and its child devices
* IllegalStateException if the device has an unsupported version, currently only version 1.0 is
* supported
*/
public UPNPRootDevice(URL deviceDefLoc, String maxAge, String vendorFirmware) throws MalformedURLException, IllegalStateException {
this(deviceDefLoc, maxAge);
this.vendorFirmware = vendorFirmware;
}
/**
* Constructor for the root device, constructs itself from An xml device definition file provided by the UPNP device
* via http normally.
*
* @param deviceDefLoc
* the location of the XML device definition file using "the urn:schemas-upnp-org:device-1-0" namespace
* @param maxAge
* the maximum age in secs of this UPNP device before considered to be outdated
* @throws MalformedURLException
* if the location URL is invalid and cannot be used to populate this root object and its child devices
* IllegalStateException if the device has an unsupported version, currently only version 1.0 is
* supported
*/
public UPNPRootDevice(URL deviceDefLoc, String maxAge) throws MalformedURLException, IllegalStateException {
this.deviceDefLoc = deviceDefLoc;
DocumentContainer.registerXMLParser(DocumentContainer.MODEL_DOM, new JXPathParser());
UPNPDevice = new DocumentContainer(deviceDefLoc, DocumentContainer.MODEL_DOM);
validityTime = Integer.parseInt(maxAge) * 1000;
creationTime = System.currentTimeMillis();
JXPathContext context = JXPathContext.newContext(this);
context.registerNamespace("upnp", "urn:schemas-upnp-org:device-1-0");
Pointer rootPtr = null;
try {
rootPtr = context.getPointer("UPNPDevice/upnp:root");
} catch (Exception ex) {
// handled below as rootPtr will be null
}
if (rootPtr == null)
throw new IllegalStateException("Unsupported device; no 'UPNPDevice/upnp:root' element");
JXPathContext rootCtx = context.getRelativeContext(rootPtr);
specVersionMajor = Integer.parseInt((String) rootCtx.getValue("upnp:specVersion/upnp:major"));
specVersionMinor = Integer.parseInt((String) rootCtx.getValue("upnp:specVersion/upnp:minor"));
if (!(specVersionMajor == 1 && specVersionMinor == 0)) {
throw new IllegalStateException("Unsupported device version (" + specVersionMajor + "." + specVersionMinor + ")");
}
boolean buildURLBase = true;
String base = null;
try {
base = (String) rootCtx.getValue("upnp:URLBase");
if (base != null && base.trim().length() > 0) {
URLBase = new URL(base);
if (log.isDebugEnabled())
log.debug("device URLBase " + URLBase);
buildURLBase = false;
}
} catch (JXPathException ex) {
// URLBase is not mandatory we assume we use the URL of the device
} catch (MalformedURLException malformedEx) {
// crappy urlbase provided
log.warn("Error occured during device baseURL " + base + " parsing, building it from device default location", malformedEx);
}
if (buildURLBase) {
String URL = deviceDefLoc.getProtocol() + "://" + deviceDefLoc.getHost() + ":" + deviceDefLoc.getPort();
String path = deviceDefLoc.getPath();
if (path != null) {
int lastSlash = path.lastIndexOf('/');
if (lastSlash != -1) {
URL += path.substring(0, lastSlash);
}
}
URLBase = new URL(URL);
}
Pointer devicePtr = rootCtx.getPointer("upnp:device");
JXPathContext deviceCtx = rootCtx.getRelativeContext(devicePtr);
fillUPNPDevice(this, null, deviceCtx, URLBase);
}
/**
* The validity time for this device in milliseconds,
*
* @return the number of milliseconds remaining before the device object that has been build is considered to be
* outdated, after this delay the UPNP device should resend an advertisement message or a negative value if
* the device is outdated
*/
public long getValidityTime() {
long elapsed = System.currentTimeMillis() - creationTime;
return validityTime - elapsed;
}
/**
* Resets the device validity time
*
* @param newMaxAge
* the maximum age in secs of this UPNP device before considered to be outdated
*/
public void resetValidityTime(String newMaxAge) {
validityTime = Integer.parseInt(newMaxAge) * 1000;
creationTime = System.currentTimeMillis();
}
/**
* Retreives the device description file location
*
* @return an URL
*/
public URL getDeviceDefLoc() {
return deviceDefLoc;
}
public int getSpecVersionMajor() {
return specVersionMajor;
}
public int getSpecVersionMinor() {
return specVersionMinor;
}
public String getVendorFirmware() {
return vendorFirmware;
}
public String getDiscoveryUSN() {
return discoveryUSN;
}
public String getDiscoveryUDN() {
return discoveryUDN;
}
/**
* URL base acces
*
* @return URL the URL base, or null if the device does not provide such information
*/
public URL getURLBase() {
return URLBase;
}
/**
* Parsing an UPNPdevice description element (<device>) in the description XML file
*
* @param device
* the device object that will be populated
* @param parent
* the device parent object
* @param deviceCtx
* an XPath context for object population
* @param baseURL
* the base URL of the UPNP device
* @throws MalformedURLException
* if some URL provided in the description file is invalid
*/
private void fillUPNPDevice(UPNPDevice device, UPNPDevice parent, JXPathContext deviceCtx, URL baseURL) throws MalformedURLException {
device.deviceType = getMandatoryData(deviceCtx, "upnp:deviceType");
if (log.isDebugEnabled())
log.debug("parsing device " + device.deviceType);
device.friendlyName = getMandatoryData(deviceCtx, "upnp:friendlyName");
device.manufacturer = getNonMandatoryData(deviceCtx, "upnp:manufacturer");
String base = getNonMandatoryData(deviceCtx, "upnp:manufacturerURL");
try {
if (base != null)
device.manufacturerURL = new URL(base);
} catch (java.net.MalformedURLException ex) {
// crappy data provided, keep the field null
}
try {
device.presentationURL = getURL(getNonMandatoryData(deviceCtx, "upnp:presentationURL"), URLBase);
} catch (java.net.MalformedURLException ex) {
// crappy data provided, keep the field null
}
device.modelDescription = getNonMandatoryData(deviceCtx, "upnp:modelDescription");
device.modelName = getMandatoryData(deviceCtx, "upnp:modelName");
device.modelNumber = getNonMandatoryData(deviceCtx, "upnp:modelNumber");
device.modelURL = getNonMandatoryData(deviceCtx, "upnp:modelURL");
device.serialNumber = getNonMandatoryData(deviceCtx, "upnp:serialNumber");
device.UDN = getMandatoryData(deviceCtx, "upnp:UDN");
device.USN = UDN.concat("::").concat(deviceType);
String tmp = getNonMandatoryData(deviceCtx, "upnp:UPC");
if (tmp != null) {
try {
device.UPC = Long.parseLong(tmp);
} catch (Exception ex) {
// non all numeric field provided, non upnp compliant device
}
}
device.parent = parent;
fillUPNPServicesList(device, deviceCtx);
fillUPNPDeviceIconsList(device, deviceCtx, URLBase);
Pointer deviceListPtr;
try {
deviceListPtr = deviceCtx.getPointer("upnp:deviceList");
} catch (JXPathException ex) {
// no pointers for this device list, this can happen
// if the device has no child devices, simply return
return;
}
JXPathContext deviceListCtx = deviceCtx.getRelativeContext(deviceListPtr);
Double arraySize = (Double) deviceListCtx.getValue("count( upnp:device )");
if (log.isDebugEnabled())
log.debug("child devices count is " + arraySize);
device.childDevices = new ArrayList<UPNPDevice>(arraySize.intValue());
for (int idx = 1; idx <= arraySize.intValue(); idx++) {
Pointer devicePtr = deviceListCtx.getPointer("upnp:device[" + idx + "]");
JXPathContext childDeviceCtx = deviceListCtx.getRelativeContext(devicePtr);
UPNPDevice childDevice = new UPNPDevice();
fillUPNPDevice(childDevice, device, childDeviceCtx, baseURL);
if (log.isDebugEnabled())
log.debug("adding child device " + childDevice.getDeviceType());
device.childDevices.add(childDevice);
}
}
private String getMandatoryData(JXPathContext ctx, String ctxFieldName) {
String value = (String) ctx.getValue(ctxFieldName);
if (value != null && value.length() == 0) {
throw new JXPathException("Mandatory field " + ctxFieldName + " not provided, uncompliant UPNP device !!");
}
return value;
}
private String getNonMandatoryData(JXPathContext ctx, String ctxFieldName) {
String value = null;
try {
value = (String) ctx.getValue(ctxFieldName);
if (value != null && value.length() == 0) {
value = null;
}
} catch (JXPathException ex) {
value = null;
}
return value;
}
/**
* Parsing an UPNPdevice services list element (<device/serviceList>) in the description XML file
*
* @param device
* the device object that will store the services list (UPNPService) objects
* @param deviceCtx
* an XPath context for object population
* @throws MalformedURLException
* if some URL provided in the description file for a service entry is invalid
*/
private void fillUPNPServicesList(UPNPDevice device, JXPathContext deviceCtx) throws MalformedURLException {
Pointer serviceListPtr = deviceCtx.getPointer("upnp:serviceList");
JXPathContext serviceListCtx = deviceCtx.getRelativeContext(serviceListPtr);
Double arraySize = (Double) serviceListCtx.getValue("count( upnp:service )");
if (log.isDebugEnabled())
log.debug("device services count is " + arraySize);
device.services = new ArrayList<UPNPService>(arraySize.intValue());
for (int idx = 1; idx <= arraySize.intValue(); idx++) {
Pointer servicePtr = serviceListCtx.getPointer("upnp:service[" + idx + "]");
JXPathContext serviceCtx = serviceListCtx.getRelativeContext(servicePtr);
// TODO possibility of bugs if deviceDefLoc contains a file name
URL base = URLBase != null ? URLBase : deviceDefLoc;
UPNPService service = new UPNPService(serviceCtx, base, this);
device.services.add(service);
}
}
/**
* Parsing an UPNPdevice icons list element (<device/iconList>) in the description XML file. This list can be null.
*
* @param device
* the device object that will store the icons list (DeviceIcon) objects
* @param deviceCtx
* an XPath context for object population
* @throws MalformedURLException
* if some URL provided in the description file for an icon URL
*/
private void fillUPNPDeviceIconsList(UPNPDevice device, JXPathContext deviceCtx, URL baseURL) throws MalformedURLException {
Pointer iconListPtr;
try {
iconListPtr = deviceCtx.getPointer("upnp:iconList");
} catch (JXPathException ex) {
// no pointers for icons list, this can happen; simply return
return;
}
JXPathContext iconListCtx = deviceCtx.getRelativeContext(iconListPtr);
Double arraySize = (Double) iconListCtx.getValue("count( upnp:icon )");
if (log.isDebugEnabled())
log.debug("device icons count is " + arraySize);
device.deviceIcons = new ArrayList<DeviceIcon>();
for (int idx = 1; idx <= arraySize.intValue(); idx++) {
DeviceIcon ico = new DeviceIcon();
if (true) {
JXPathContext elemCtx = deviceCtx.getRelativeContext(iconListCtx.getPointer("upnp:icon[" + idx + "]"));
ico.mimeType = (String) elemCtx.getValue("upnp:mimetype");
ico.width = Integer.parseInt((String) elemCtx.getValue("upnp:width"));
ico.height = Integer.parseInt((String) elemCtx.getValue("upnp:height"));
ico.depth = Integer.parseInt((String) elemCtx.getValue("upnp:depth"));
ico.url = getURL((String) elemCtx.getValue("upnp:url"), baseURL);
// } else {
// ico.mimeType = (String) iconListCtx.getValue("upnp:icon[" + i + "]/upnp:mimetype");
// ico.width = Integer.parseInt((String) iconListCtx.getValue("upnp:icon[" + i + "]/upnp:width"));
// ico.height = Integer.parseInt((String) iconListCtx.getValue("upnp:icon[" + i + "]/upnp:height"));
// ico.depth = Integer.parseInt((String) iconListCtx.getValue("upnp:icon[" + i + "]/upnp:depth"));
// ico.url = getURL((String) iconListCtx.getValue("upnp:icon[" + i + "]/upnp:url"), baseURL);
}
if (log.isDebugEnabled())
log.debug("icon URL is " + ico.url);
device.deviceIcons.add(ico);
}
}
/**
* Parsing an URL from the descriptionXML file
*
* @param url
* the string representation fo the URL
* @param baseURL
* the base device URL, needed if the url param is relative
* @return an URL object defining the url param
* @throws MalformedURLException
* if the url param or baseURL.toExternalForm() + url cannot be parsed to create an URL object
*/
public final static URL getURL(String url, URL baseURL) throws MalformedURLException {
URL rtrVal;
if (url == null || url.trim().length() == 0)
return null;
try {
rtrVal = new URL(url);
} catch (MalformedURLException malEx) {
// maybe that the url is relative, we add the baseURL and reparse it
// if relative then we take the device baser url root and add the url
if (baseURL != null) {
url = url.replace('\\', '/');
if (url.charAt(0) != '/') {
// the path is relative to the device baseURL
String externalForm = baseURL.toExternalForm();
if (!externalForm.endsWith("/")) {
externalForm += "/";
}
rtrVal = new URL(externalForm + url);
} else {
// the path is not relative
String URLRoot = baseURL.getProtocol() + "://" + baseURL.getHost() + ":" + baseURL.getPort();
rtrVal = new URL(URLRoot + url);
}
} else {
throw malEx;
}
}
return rtrVal;
}
/**
* Retrieves the device definition XML data
*
* @return the device definition XML data as a String
*/
public String getDeviceDefLocData() {
if (deviceDefLocData == null) {
try {
java.io.InputStream in = deviceDefLoc.openConnection().getInputStream();
int readen = 0;
byte[] buff = new byte[512];
StringBuffer strBuff = new StringBuffer();
while ((readen = in.read(buff)) != -1) {
strBuff.append(new String(buff, 0, readen));
}
in.close();
deviceDefLocData = strBuff.toString();
} catch (IOException ioEx) {
return null;
}
}
return deviceDefLocData;
}
/**
* Used for JXPath parsing, do not use this method
*
* @return a Container object for Xpath parsing capabilities
*/
public Container getUPNPDevice() {
return UPNPDevice;
}
}