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.binding.xml;
17  
18  import static org.fourthline.cling.model.XMLUtil.appendNewElement;
19  import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull;
20  
21  import java.io.StringReader;
22  import java.net.URI;
23  import java.net.URL;
24  import java.util.logging.Logger;
25  
26  import javax.xml.parsers.DocumentBuilder;
27  import javax.xml.parsers.DocumentBuilderFactory;
28  
29  import org.fourthline.cling.binding.staging.MutableDevice;
30  import org.fourthline.cling.binding.staging.MutableIcon;
31  import org.fourthline.cling.binding.staging.MutableService;
32  import org.fourthline.cling.binding.xml.Descriptor.Device.ELEMENT;
33  import org.fourthline.cling.model.Namespace;
34  import org.fourthline.cling.model.ValidationException;
35  import org.fourthline.cling.model.XMLUtil;
36  import org.fourthline.cling.model.meta.Device;
37  import org.fourthline.cling.model.meta.DeviceDetails;
38  import org.fourthline.cling.model.meta.Icon;
39  import org.fourthline.cling.model.meta.LocalDevice;
40  import org.fourthline.cling.model.meta.LocalService;
41  import org.fourthline.cling.model.meta.RemoteDevice;
42  import org.fourthline.cling.model.meta.RemoteService;
43  import org.fourthline.cling.model.meta.Service;
44  import org.fourthline.cling.model.profile.RemoteClientInfo;
45  import org.fourthline.cling.model.types.DLNACaps;
46  import org.fourthline.cling.model.types.DLNADoc;
47  import org.fourthline.cling.model.types.InvalidValueException;
48  import org.fourthline.cling.model.types.ServiceId;
49  import org.fourthline.cling.model.types.ServiceType;
50  import org.fourthline.cling.model.types.UDN;
51  import org.seamless.util.Exceptions;
52  import org.seamless.util.MimeType;
53  import org.w3c.dom.Document;
54  import org.w3c.dom.Element;
55  import org.w3c.dom.Node;
56  import org.w3c.dom.NodeList;
57  import org.xml.sax.ErrorHandler;
58  import org.xml.sax.InputSource;
59  import org.xml.sax.SAXException;
60  import org.xml.sax.SAXParseException;
61  
62  /**
63   * Implementation based on JAXP DOM.
64   *
65   * @author Christian Bauer
66   */
67  public class UDA10DeviceDescriptorBinderImpl implements DeviceDescriptorBinder, ErrorHandler {
68  
69      private static Logger log = Logger.getLogger(DeviceDescriptorBinder.class.getName());
70  
71      public <D extends Device> D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {
72  
73          if (descriptorXml == null || descriptorXml.length() == 0) {
74              throw new DescriptorBindingException("Null or empty descriptor");
75          }
76  
77          try {
78              log.fine("Populating device from XML descriptor: " + undescribedDevice);
79              // We can not validate the XML document. There is no possible XML schema (maybe RELAX NG) that would properly
80              // constrain the UDA 1.0 device descriptor documents: Any unknown element or attribute must be ignored, order of elements
81              // is not guaranteed. Try to write a schema for that! No combination of <xsd:any namespace="##any"> and <xsd:choice>
82              // works with that... But hey, MSFT sure has great tech guys! So what we do here is just parsing out the known elements
83              // and ignoring the other shit. We'll also do some very very basic validation of required elements, but that's it.
84  
85              // And by the way... try this with JAXB instead of manual DOM processing! And you thought it couldn't get worse....
86  
87              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
88              factory.setNamespaceAware(true);
89              DocumentBuilder documentBuilder = factory.newDocumentBuilder();
90              documentBuilder.setErrorHandler(this);
91  
92              Document d = documentBuilder.parse(
93                      new InputSource(
94                              // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
95                              new StringReader(descriptorXml.trim())
96                      )
97              );
98  
99              return describe(undescribedDevice, d);
100 
101         } catch (ValidationException ex) {
102             throw ex;
103         } catch (Exception ex) {
104             throw new DescriptorBindingException("Could not parse device descriptor: " + ex.toString(), ex);
105         }
106     }
107 
108     public <D extends Device> D describe(D undescribedDevice, Document dom) throws DescriptorBindingException, ValidationException {
109         try {
110             log.fine("Populating device from DOM: " + undescribedDevice);
111 
112             // Read the XML into a mutable descriptor graph
113             MutableDevice descriptor = new MutableDevice();
114             Element rootElement = dom.getDocumentElement();
115             hydrateRoot(descriptor, rootElement);
116 
117             // Build the immutable descriptor graph
118             return buildInstance(undescribedDevice, descriptor);
119 
120         } catch (ValidationException ex) {
121             throw ex;
122         } catch (Exception ex) {
123             throw new DescriptorBindingException("Could not parse device DOM: " + ex.toString(), ex);
124         }
125     }
126 
127     public <D extends Device> D buildInstance(D undescribedDevice, MutableDevice descriptor) throws ValidationException {
128         return (D) descriptor.build(undescribedDevice);
129     }
130 
131     protected void hydrateRoot(MutableDevice descriptor, Element rootElement) throws DescriptorBindingException {
132 
133         if (rootElement.getNamespaceURI() == null || !rootElement.getNamespaceURI().equals(Descriptor.Device.NAMESPACE_URI)) {
134             log.warning("Wrong XML namespace declared on root element: " + rootElement.getNamespaceURI());
135         }
136 
137         if (!rootElement.getNodeName().equals(ELEMENT.root.name())) {
138             throw new DescriptorBindingException("Root element name is not <root>: " + rootElement.getNodeName());
139         }
140 
141         NodeList rootChildren = rootElement.getChildNodes();
142 
143         Node deviceNode = null;
144 
145         for (int i = 0; i < rootChildren.getLength(); i++) {
146             Node rootChild = rootChildren.item(i);
147 
148             if (rootChild.getNodeType() != Node.ELEMENT_NODE)
149                 continue;
150 
151             if (ELEMENT.specVersion.equals(rootChild)) {
152                 hydrateSpecVersion(descriptor, rootChild);
153             } else if (ELEMENT.URLBase.equals(rootChild)) {
154                 try {
155                     String urlString = XMLUtil.getTextContent(rootChild);
156                     if (urlString != null && urlString.length() > 0) {
157                         // We hope it's  RFC 2396 and RFC 2732 compliant
158                         descriptor.baseURL = new URL(urlString);
159                     }
160                 } catch (Exception ex) {
161                     throw new DescriptorBindingException("Invalid URLBase: " + ex.getMessage());
162                 }
163             } else if (ELEMENT.device.equals(rootChild)) {
164                 // Just sanity check here...
165                 if (deviceNode != null)
166                     throw new DescriptorBindingException("Found multiple <device> elements in <root>");
167                 deviceNode = rootChild;
168             } else {
169                 log.finer("Ignoring unknown element: " + rootChild.getNodeName());
170             }
171         }
172 
173         if (deviceNode == null) {
174             throw new DescriptorBindingException("No <device> element in <root>");
175         }
176         hydrateDevice(descriptor, deviceNode);
177     }
178 
179     public void hydrateSpecVersion(MutableDevice descriptor, Node specVersionNode) throws DescriptorBindingException {
180 
181         NodeList specVersionChildren = specVersionNode.getChildNodes();
182         for (int i = 0; i < specVersionChildren.getLength(); i++) {
183             Node specVersionChild = specVersionChildren.item(i);
184 
185             if (specVersionChild.getNodeType() != Node.ELEMENT_NODE)
186                 continue;
187 
188             if (ELEMENT.major.equals(specVersionChild)) {
189                 String version = XMLUtil.getTextContent(specVersionChild).trim();
190                 if (!version.equals("1")) {
191                     log.warning("Unsupported UDA major version, ignoring: " + version);
192                     version = "1";
193                 }
194                 descriptor.udaVersion.major = Integer.valueOf(version);
195             } else if (ELEMENT.minor.equals(specVersionChild)) {
196                 String version = XMLUtil.getTextContent(specVersionChild).trim();
197                 if (!version.equals("0")) {
198                     log.warning("Unsupported UDA minor version, ignoring: " + version);
199                     version = "0";
200                 }
201                 descriptor.udaVersion.minor = Integer.valueOf(version);
202             }
203 
204         }
205 
206     }
207 
208     public void hydrateDevice(MutableDevice descriptor, Node deviceNode) throws DescriptorBindingException {
209 
210         NodeList deviceNodeChildren = deviceNode.getChildNodes();
211         for (int i = 0; i < deviceNodeChildren.getLength(); i++) {
212             Node deviceNodeChild = deviceNodeChildren.item(i);
213 
214             if (deviceNodeChild.getNodeType() != Node.ELEMENT_NODE)
215                 continue;
216 
217             if (ELEMENT.deviceType.equals(deviceNodeChild)) {
218                 descriptor.deviceType = XMLUtil.getTextContent(deviceNodeChild);
219             } else if (ELEMENT.friendlyName.equals(deviceNodeChild)) {
220                 descriptor.friendlyName = XMLUtil.getTextContent(deviceNodeChild);
221             } else if (ELEMENT.manufacturer.equals(deviceNodeChild)) {
222                 descriptor.manufacturer = XMLUtil.getTextContent(deviceNodeChild);
223             } else if (ELEMENT.manufacturerURL.equals(deviceNodeChild)) {
224                 descriptor.manufacturerURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
225             } else if (ELEMENT.modelDescription.equals(deviceNodeChild)) {
226                 descriptor.modelDescription = XMLUtil.getTextContent(deviceNodeChild);
227             } else if (ELEMENT.modelName.equals(deviceNodeChild)) {
228                 descriptor.modelName = XMLUtil.getTextContent(deviceNodeChild);
229             } else if (ELEMENT.modelNumber.equals(deviceNodeChild)) {
230                 descriptor.modelNumber = XMLUtil.getTextContent(deviceNodeChild);
231             } else if (ELEMENT.modelURL.equals(deviceNodeChild)) {
232                 descriptor.modelURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
233             } else if (ELEMENT.presentationURL.equals(deviceNodeChild)) {
234                 descriptor.presentationURI = parseURI(XMLUtil.getTextContent(deviceNodeChild));
235             } else if (ELEMENT.UPC.equals(deviceNodeChild)) {
236                 descriptor.upc = XMLUtil.getTextContent(deviceNodeChild);
237             } else if (ELEMENT.serialNumber.equals(deviceNodeChild)) {
238                 descriptor.serialNumber = XMLUtil.getTextContent(deviceNodeChild);
239             } else if (ELEMENT.UDN.equals(deviceNodeChild)) {
240                 descriptor.udn = UDN.valueOf(XMLUtil.getTextContent(deviceNodeChild));
241             } else if (ELEMENT.iconList.equals(deviceNodeChild)) {
242                 hydrateIconList(descriptor, deviceNodeChild);
243             } else if (ELEMENT.serviceList.equals(deviceNodeChild)) {
244                 hydrateServiceList(descriptor, deviceNodeChild);
245             } else if (ELEMENT.deviceList.equals(deviceNodeChild)) {
246                 hydrateDeviceList(descriptor, deviceNodeChild);
247             } else if (ELEMENT.X_DLNADOC.equals(deviceNodeChild) &&
248                 Descriptor.Device.DLNA_PREFIX.equals(deviceNodeChild.getPrefix())) {
249                 String txt = XMLUtil.getTextContent(deviceNodeChild);
250                 try {
251                     descriptor.dlnaDocs.add(DLNADoc.valueOf(txt));
252                 } catch (InvalidValueException ex) {
253                     log.info("Invalid X_DLNADOC value, ignoring value: " + txt);
254                 }
255             } else if (ELEMENT.X_DLNACAP.equals(deviceNodeChild) &&
256                 Descriptor.Device.DLNA_PREFIX.equals(deviceNodeChild.getPrefix())) {
257                 descriptor.dlnaCaps = DLNACaps.valueOf(XMLUtil.getTextContent(deviceNodeChild));
258             }
259         }
260     }
261 
262     public void hydrateIconList(MutableDevice descriptor, Node iconListNode) throws DescriptorBindingException {
263 
264         NodeList iconListNodeChildren = iconListNode.getChildNodes();
265         for (int i = 0; i < iconListNodeChildren.getLength(); i++) {
266             Node iconListNodeChild = iconListNodeChildren.item(i);
267 
268             if (iconListNodeChild.getNodeType() != Node.ELEMENT_NODE)
269                 continue;
270 
271             if (ELEMENT.icon.equals(iconListNodeChild)) {
272 
273                 MutableIcon icon = new MutableIcon();
274 
275                 NodeList iconChildren = iconListNodeChild.getChildNodes();
276 
277                 for (int x = 0; x < iconChildren.getLength(); x++) {
278                     Node iconChild = iconChildren.item(x);
279 
280                     if (iconChild.getNodeType() != Node.ELEMENT_NODE)
281                         continue;
282 
283                     if (ELEMENT.width.equals(iconChild)) {
284                         icon.width = (Integer.valueOf(XMLUtil.getTextContent(iconChild)));
285                     } else if (ELEMENT.height.equals(iconChild)) {
286                         icon.height = (Integer.valueOf(XMLUtil.getTextContent(iconChild)));
287                     } else if (ELEMENT.depth.equals(iconChild)) {
288                         String depth = XMLUtil.getTextContent(iconChild);
289                         try {
290                             icon.depth = (Integer.valueOf(depth));
291                        	} catch(NumberFormatException ex) {
292                        		log.warning("Invalid icon depth '" + depth + "', using 16 as default: " + ex);
293                        		icon.depth = 16;
294                        	}
295                     } else if (ELEMENT.url.equals(iconChild)) {
296                         icon.uri = parseURI(XMLUtil.getTextContent(iconChild));
297                     } else if (ELEMENT.mimetype.equals(iconChild)) {
298                         try {
299                             icon.mimeType = XMLUtil.getTextContent(iconChild);
300                             MimeType.valueOf(icon.mimeType);
301                         } catch(IllegalArgumentException ex) {
302                             log.warning("Ignoring invalid icon mime type: " + icon.mimeType);
303                             icon.mimeType = "";
304                         }
305                     }
306 
307                 }
308 
309                 descriptor.icons.add(icon);
310             }
311         }
312     }
313 
314     public void hydrateServiceList(MutableDevice descriptor, Node serviceListNode) throws DescriptorBindingException {
315 
316         NodeList serviceListNodeChildren = serviceListNode.getChildNodes();
317         for (int i = 0; i < serviceListNodeChildren.getLength(); i++) {
318             Node serviceListNodeChild = serviceListNodeChildren.item(i);
319 
320             if (serviceListNodeChild.getNodeType() != Node.ELEMENT_NODE)
321                 continue;
322 
323             if (ELEMENT.service.equals(serviceListNodeChild)) {
324 
325                 NodeList serviceChildren = serviceListNodeChild.getChildNodes();
326 
327                 try {
328                     MutableService service = new MutableService();
329 
330                     for (int x = 0; x < serviceChildren.getLength(); x++) {
331                         Node serviceChild = serviceChildren.item(x);
332 
333                         if (serviceChild.getNodeType() != Node.ELEMENT_NODE)
334                             continue;
335 
336                         if (ELEMENT.serviceType.equals(serviceChild)) {
337                             service.serviceType = (ServiceType.valueOf(XMLUtil.getTextContent(serviceChild)));
338                         } else if (ELEMENT.serviceId.equals(serviceChild)) {
339                             service.serviceId = (ServiceId.valueOf(XMLUtil.getTextContent(serviceChild)));
340                         } else if (ELEMENT.SCPDURL.equals(serviceChild)) {
341                             service.descriptorURI = parseURI(XMLUtil.getTextContent(serviceChild));
342                         } else if (ELEMENT.controlURL.equals(serviceChild)) {
343                             service.controlURI = parseURI(XMLUtil.getTextContent(serviceChild));
344                         } else if (ELEMENT.eventSubURL.equals(serviceChild)) {
345                             service.eventSubscriptionURI = parseURI(XMLUtil.getTextContent(serviceChild));
346                         }
347 
348                     }
349 
350                     descriptor.services.add(service);
351                 } catch (InvalidValueException ex) {
352                     log.warning(
353                         "UPnP specification violation, skipping invalid service declaration. " + ex.getMessage()
354                     );
355                 }
356             }
357         }
358     }
359 
360     public void hydrateDeviceList(MutableDevice descriptor, Node deviceListNode) throws DescriptorBindingException {
361 
362         NodeList deviceListNodeChildren = deviceListNode.getChildNodes();
363         for (int i = 0; i < deviceListNodeChildren.getLength(); i++) {
364             Node deviceListNodeChild = deviceListNodeChildren.item(i);
365 
366             if (deviceListNodeChild.getNodeType() != Node.ELEMENT_NODE)
367                 continue;
368 
369             if (ELEMENT.device.equals(deviceListNodeChild)) {
370                 MutableDevice embeddedDevice = new MutableDevice();
371                 embeddedDevice.parentDevice = descriptor;
372                 descriptor.embeddedDevices.add(embeddedDevice);
373                 hydrateDevice(embeddedDevice, deviceListNodeChild);
374             }
375         }
376 
377     }
378 
379     public String generate(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException {
380         try {
381             log.fine("Generating XML descriptor from device model: " + deviceModel);
382 
383             return XMLUtil.documentToString(buildDOM(deviceModel, info, namespace));
384 
385         } catch (Exception ex) {
386             throw new DescriptorBindingException("Could not build DOM: " + ex.getMessage(), ex);
387         }
388     }
389 
390     public Document buildDOM(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException {
391 
392         try {
393             log.fine("Generating DOM from device model: " + deviceModel);
394 
395             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
396             factory.setNamespaceAware(true);
397 
398             Document d = factory.newDocumentBuilder().newDocument();
399             generateRoot(namespace, deviceModel, d, info);
400 
401             return d;
402 
403         } catch (Exception ex) {
404             throw new DescriptorBindingException("Could not generate device descriptor: " + ex.getMessage(), ex);
405         }
406     }
407 
408     protected void generateRoot(Namespace namespace, Device deviceModel, Document descriptor, RemoteClientInfo info) {
409 
410         Element rootElement = descriptor.createElementNS(Descriptor.Device.NAMESPACE_URI, ELEMENT.root.toString());
411         descriptor.appendChild(rootElement);
412 
413         generateSpecVersion(namespace, deviceModel, descriptor, rootElement);
414 
415         /* UDA 1.1 spec says: Don't use URLBase anymore
416         if (deviceModel.getBaseURL() != null) {
417             appendChildElementWithTextContent(descriptor, rootElement, "URLBase", deviceModel.getBaseURL());
418         }
419         */
420 
421         generateDevice(namespace, deviceModel, descriptor, rootElement, info);
422     }
423 
424     protected void generateSpecVersion(Namespace namespace, Device deviceModel, Document descriptor, Element rootElement) {
425         Element specVersionElement = appendNewElement(descriptor, rootElement, ELEMENT.specVersion);
426         appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.major, deviceModel.getVersion().getMajor());
427         appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.minor, deviceModel.getVersion().getMinor());
428     }
429 
430     protected void generateDevice(Namespace namespace, Device deviceModel, Document descriptor, Element rootElement, RemoteClientInfo info) {
431 
432         Element deviceElement = appendNewElement(descriptor, rootElement, ELEMENT.device);
433 
434         appendNewElementIfNotNull(descriptor, deviceElement, ELEMENT.deviceType, deviceModel.getType());
435 
436         DeviceDetails deviceModelDetails = deviceModel.getDetails(info);
437         appendNewElementIfNotNull(
438                 descriptor, deviceElement, ELEMENT.friendlyName,
439                 deviceModelDetails.getFriendlyName()
440         );
441         if (deviceModelDetails.getManufacturerDetails() != null) {
442             appendNewElementIfNotNull(
443                     descriptor, deviceElement, ELEMENT.manufacturer,
444                     deviceModelDetails.getManufacturerDetails().getManufacturer()
445             );
446             appendNewElementIfNotNull(
447                     descriptor, deviceElement, ELEMENT.manufacturerURL,
448                     deviceModelDetails.getManufacturerDetails().getManufacturerURI()
449             );
450         }
451         if (deviceModelDetails.getModelDetails() != null) {
452             appendNewElementIfNotNull(
453                     descriptor, deviceElement, ELEMENT.modelDescription,
454                     deviceModelDetails.getModelDetails().getModelDescription()
455             );
456             appendNewElementIfNotNull(
457                     descriptor, deviceElement, ELEMENT.modelName,
458                     deviceModelDetails.getModelDetails().getModelName()
459             );
460             appendNewElementIfNotNull(
461                     descriptor, deviceElement, ELEMENT.modelNumber,
462                     deviceModelDetails.getModelDetails().getModelNumber()
463             );
464             appendNewElementIfNotNull(
465                     descriptor, deviceElement, ELEMENT.modelURL,
466                     deviceModelDetails.getModelDetails().getModelURI()
467             );
468         }
469         appendNewElementIfNotNull(
470                 descriptor, deviceElement, ELEMENT.serialNumber,
471                 deviceModelDetails.getSerialNumber()
472         );
473         appendNewElementIfNotNull(descriptor, deviceElement, ELEMENT.UDN, deviceModel.getIdentity().getUdn());
474         appendNewElementIfNotNull(
475                 descriptor, deviceElement, ELEMENT.presentationURL,
476                 deviceModelDetails.getPresentationURI()
477         );
478         appendNewElementIfNotNull(
479                 descriptor, deviceElement, ELEMENT.UPC,
480                 deviceModelDetails.getUpc()
481         );
482 
483         if (deviceModelDetails.getDlnaDocs() != null) {
484             for (DLNADoc dlnaDoc : deviceModelDetails.getDlnaDocs()) {
485                 appendNewElementIfNotNull(
486                         descriptor, deviceElement, Descriptor.Device.DLNA_PREFIX + ":" + ELEMENT.X_DLNADOC,
487                         dlnaDoc, Descriptor.Device.DLNA_NAMESPACE_URI
488                 );
489             }
490         }
491         appendNewElementIfNotNull(
492                 descriptor, deviceElement, Descriptor.Device.DLNA_PREFIX + ":" + ELEMENT.X_DLNACAP,
493                 deviceModelDetails.getDlnaCaps(), Descriptor.Device.DLNA_NAMESPACE_URI
494         );
495         
496         appendNewElementIfNotNull(
497                 descriptor, deviceElement, Descriptor.Device.SEC_PREFIX + ":" + ELEMENT.ProductCap,
498                 deviceModelDetails.getSecProductCaps(), Descriptor.Device.SEC_NAMESPACE_URI
499         );
500         
501         appendNewElementIfNotNull(
502                 descriptor, deviceElement, Descriptor.Device.SEC_PREFIX + ":" + ELEMENT.X_ProductCap,
503                 deviceModelDetails.getSecProductCaps(), Descriptor.Device.SEC_NAMESPACE_URI
504         );
505 
506         generateIconList(namespace, deviceModel, descriptor, deviceElement);
507         generateServiceList(namespace, deviceModel, descriptor, deviceElement);
508         generateDeviceList(namespace, deviceModel, descriptor, deviceElement, info);
509     }
510 
511     protected void generateIconList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement) {
512         if (!deviceModel.hasIcons()) return;
513 
514         Element iconListElement = appendNewElement(descriptor, deviceElement, ELEMENT.iconList);
515 
516         for (Icon icon : deviceModel.getIcons()) {
517             Element iconElement = appendNewElement(descriptor, iconListElement, ELEMENT.icon);
518 
519             appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.mimetype, icon.getMimeType());
520             appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.width, icon.getWidth());
521             appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.height, icon.getHeight());
522             appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.depth, icon.getDepth());
523             if (deviceModel instanceof RemoteDevice) {
524             	appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.url,  icon.getUri());
525             } else if (deviceModel instanceof LocalDevice) {
526             	appendNewElementIfNotNull(descriptor, iconElement, ELEMENT.url,  namespace.getIconPath(icon));
527             }
528         }
529     }
530 
531     protected void generateServiceList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement) {
532         if (!deviceModel.hasServices()) return;
533 
534         Element serviceListElement = appendNewElement(descriptor, deviceElement, ELEMENT.serviceList);
535 
536         for (Service service : deviceModel.getServices()) {
537             Element serviceElement = appendNewElement(descriptor, serviceListElement, ELEMENT.service);
538 
539             appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.serviceType, service.getServiceType());
540             appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.serviceId, service.getServiceId());
541             if (service instanceof RemoteService) {
542                 RemoteService rs = (RemoteService) service;
543                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.SCPDURL, rs.getDescriptorURI());
544                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.controlURL, rs.getControlURI());
545                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.eventSubURL, rs.getEventSubscriptionURI());
546             } else if (service instanceof LocalService) {
547                 LocalService ls = (LocalService) service;
548                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.SCPDURL, namespace.getDescriptorPath(ls));
549                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.controlURL, namespace.getControlPath(ls));
550                 appendNewElementIfNotNull(descriptor, serviceElement, ELEMENT.eventSubURL, namespace.getEventSubscriptionPath(ls));
551             }
552         }
553     }
554 
555     protected void generateDeviceList(Namespace namespace, Device deviceModel, Document descriptor, Element deviceElement, RemoteClientInfo info) {
556         if (!deviceModel.hasEmbeddedDevices()) return;
557 
558         Element deviceListElement = appendNewElement(descriptor, deviceElement, ELEMENT.deviceList);
559 
560         for (Device device : deviceModel.getEmbeddedDevices()) {
561             generateDevice(namespace, device, descriptor, deviceListElement, info);
562         }
563     }
564 
565     public void warning(SAXParseException e) throws SAXException {
566         log.warning(e.toString());
567     }
568 
569     public void error(SAXParseException e) throws SAXException {
570         throw e;
571     }
572 
573     public void fatalError(SAXParseException e) throws SAXException {
574         throw e;
575     }
576 
577     static protected URI parseURI(String uri) {
578 
579         // TODO: UPNP VIOLATION: Netgear DG834 uses a non-URI: 'www.netgear.com'
580         if (uri.startsWith("www.")) {
581              uri = "http://" + uri;
582         }
583 
584         // TODO: UPNP VIOLATION: Plutinosoft uses unencoded relative URIs
585         // /var/mobile/Applications/71367E68-F30F-460B-A2D2-331509441D13/Windows Media Player Streamer.app/Icon-ps3.jpg
586         if (uri.contains(" ")) {
587             // We don't want to split/encode individual parts of the URI, too much work
588             // TODO: But we probably should do this? Because browsers do it, everyone
589             // seems to think that spaces in URLs are somehow OK...
590             uri = uri.replaceAll(" ", "%20");
591         }
592 
593         try {
594             return URI.create(uri);
595         } catch (Throwable ex) {
596             /*
597         	catch Throwable because on Android 2.2, parsing some invalid URI like "http://..."  gives:
598         	        	java.lang.NullPointerException
599         	        	 	at java.net.URI$Helper.isValidDomainName(URI.java:631)
600         	        	 	at java.net.URI$Helper.isValidHost(URI.java:595)
601         	        	 	at java.net.URI$Helper.parseAuthority(URI.java:544)
602         	        	 	at java.net.URI$Helper.parseURI(URI.java:404)
603         	        	 	at java.net.URI$Helper.access$100(URI.java:302)
604         	        	 	at java.net.URI.<init>(URI.java:87)
605         	        		at java.net.URI.create(URI.java:968)
606             */
607             log.fine("Illegal URI, trying with ./ prefix: " + Exceptions.unwrap(ex));
608             // Ignore
609         }
610         try {
611             // The java.net.URI class can't deal with "_urn:foobar" (yeah, great idea Intel UPnP tools guy), as
612             // explained in RFC 3986:
613             //
614             // A path segment that contains a colon character (e.g., "this:that") cannot be used as the first segment
615             // of a relative-path reference, as it would be mistaken for a scheme name. Such a segment must
616             // be preceded by a dot-segment (e.g., "./this:that") to make a relative-path reference.
617             //
618             return URI.create("./" + uri);
619         } catch (IllegalArgumentException ex) {
620             log.warning("Illegal URI '" + uri + "', ignoring value: " + Exceptions.unwrap(ex));
621             // Ignore
622         }
623         return null;
624     }
625 
626 }