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 org.fourthline.cling.binding.staging.MutableAction;
19  import org.fourthline.cling.binding.staging.MutableActionArgument;
20  import org.fourthline.cling.binding.staging.MutableAllowedValueRange;
21  import org.fourthline.cling.binding.staging.MutableService;
22  import org.fourthline.cling.binding.staging.MutableStateVariable;
23  import org.fourthline.cling.model.ValidationException;
24  import org.fourthline.cling.model.XMLUtil;
25  import org.fourthline.cling.model.meta.Action;
26  import org.fourthline.cling.model.meta.ActionArgument;
27  import org.fourthline.cling.model.meta.QueryStateVariableAction;
28  import org.fourthline.cling.model.meta.RemoteService;
29  import org.fourthline.cling.model.meta.Service;
30  import org.fourthline.cling.model.meta.StateVariable;
31  import org.fourthline.cling.model.meta.StateVariableEventDetails;
32  import org.fourthline.cling.model.types.CustomDatatype;
33  import org.fourthline.cling.model.types.Datatype;
34  import org.w3c.dom.Document;
35  import org.w3c.dom.Element;
36  import org.w3c.dom.Node;
37  import org.w3c.dom.NodeList;
38  import org.xml.sax.ErrorHandler;
39  import org.xml.sax.InputSource;
40  import org.xml.sax.SAXException;
41  import org.xml.sax.SAXParseException;
42  
43  import javax.xml.parsers.DocumentBuilder;
44  import javax.xml.parsers.DocumentBuilderFactory;
45  import java.io.StringReader;
46  import java.util.ArrayList;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.logging.Logger;
50  
51  import static org.fourthline.cling.binding.xml.Descriptor.Service.ATTRIBUTE;
52  import static org.fourthline.cling.binding.xml.Descriptor.Service.ELEMENT;
53  import static org.fourthline.cling.model.XMLUtil.appendNewElement;
54  import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull;
55  
56  /**
57   * Implementation based on JAXP DOM.
58   *
59   * @author Christian Bauer
60   */
61  public class UDA10ServiceDescriptorBinderImpl implements ServiceDescriptorBinder, ErrorHandler {
62  
63      private static Logger log = Logger.getLogger(ServiceDescriptorBinder.class.getName());
64  
65      public <S extends Service> S describe(S undescribedService, String descriptorXml) throws DescriptorBindingException, ValidationException {
66          if (descriptorXml == null || descriptorXml.length() == 0) {
67              throw new DescriptorBindingException("Null or empty descriptor");
68          }
69  
70          try {
71              log.fine("Populating service from XML descriptor: " + undescribedService);
72  
73              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
74              factory.setNamespaceAware(true);
75              DocumentBuilder documentBuilder = factory.newDocumentBuilder();
76              documentBuilder.setErrorHandler(this);
77  
78              Document d = documentBuilder.parse(
79                  new InputSource(
80                      // TODO: UPNP VIOLATION: Virgin Media Superhub sends trailing spaces/newlines after last XML element, need to trim()
81                      new StringReader(descriptorXml.trim())
82                  )
83              );
84  
85              return describe(undescribedService, d);
86  
87          } catch (ValidationException ex) {
88              throw ex;
89          } catch (Exception ex) {
90              throw new DescriptorBindingException("Could not parse service descriptor: " + ex.toString(), ex);
91          }
92      }
93  
94      public <S extends Service> S describe(S undescribedService, Document dom) throws DescriptorBindingException, ValidationException {
95          try {
96              log.fine("Populating service from DOM: " + undescribedService);
97  
98              // Read the XML into a mutable descriptor graph
99              MutableService descriptor = new MutableService();
100 
101             hydrateBasic(descriptor, undescribedService);
102 
103             Element rootElement = dom.getDocumentElement();
104             hydrateRoot(descriptor, rootElement);
105 
106             // Build the immutable descriptor graph
107             return buildInstance(undescribedService, descriptor);
108 
109         } catch (ValidationException ex) {
110             throw ex;
111         } catch (Exception ex) {
112             throw new DescriptorBindingException("Could not parse service DOM: " + ex.toString(), ex);
113         }
114     }
115 
116     protected <S extends Service> S buildInstance(S undescribedService, MutableService descriptor) throws ValidationException {
117         return (S)descriptor.build(undescribedService.getDevice());
118     }
119 
120     protected void hydrateBasic(MutableService descriptor, Service undescribedService) {
121         descriptor.serviceId = undescribedService.getServiceId();
122         descriptor.serviceType = undescribedService.getServiceType();
123         if (undescribedService instanceof RemoteService) {
124             RemoteService rs = (RemoteService) undescribedService;
125             descriptor.controlURI = rs.getControlURI();
126             descriptor.eventSubscriptionURI = rs.getEventSubscriptionURI();
127             descriptor.descriptorURI = rs.getDescriptorURI();
128         }
129     }
130 
131     protected void hydrateRoot(MutableService descriptor, Element rootElement)
132             throws DescriptorBindingException {
133 
134         // We don't check the XMLNS, nobody bothers anyway...
135 
136         if (!ELEMENT.scpd.equals(rootElement)) {
137             throw new DescriptorBindingException("Root element name is not <scpd>: " + rootElement.getNodeName());
138         }
139 
140         NodeList rootChildren = rootElement.getChildNodes();
141 
142         for (int i = 0; i < rootChildren.getLength(); i++) {
143             Node rootChild = rootChildren.item(i);
144 
145             if (rootChild.getNodeType() != Node.ELEMENT_NODE)
146                 continue;
147 
148             if (ELEMENT.specVersion.equals(rootChild)) {
149                 // We don't care about UDA major/minor specVersion anymore - whoever had the brilliant idea that
150                 // the spec versions can be declared on devices _AND_ on their services should have their fingers
151                 // broken so they never touch a keyboard again.
152                 // hydrateSpecVersion(descriptor, rootChild);
153             } else if (ELEMENT.actionList.equals(rootChild)) {
154                 hydrateActionList(descriptor, rootChild);
155             } else if (ELEMENT.serviceStateTable.equals(rootChild)) {
156                 hydrateServiceStateTableList(descriptor, rootChild);
157             } else {
158                 log.finer("Ignoring unknown element: " + rootChild.getNodeName());
159             }
160         }
161 
162     }
163 
164     /*
165     public void hydrateSpecVersion(MutableService descriptor, Node specVersionNode)
166             throws DescriptorBindingException {
167 
168         NodeList specVersionChildren = specVersionNode.getChildNodes();
169         for (int i = 0; i < specVersionChildren.getLength(); i++) {
170             Node specVersionChild = specVersionChildren.item(i);
171 
172             if (specVersionChild.getNodeType() != Node.ELEMENT_NODE)
173                 continue;
174 
175             MutableUDAVersion version = new MutableUDAVersion();
176             if (ELEMENT.major.equals(specVersionChild)) {
177                 version.major = Integer.valueOf(XMLUtil.getTextContent(specVersionChild));
178             } else if (ELEMENT.minor.equals(specVersionChild)) {
179                 version.minor = Integer.valueOf(XMLUtil.getTextContent(specVersionChild));
180             }
181         }
182     }
183     */
184 
185     public void hydrateActionList(MutableService descriptor, Node actionListNode) throws DescriptorBindingException {
186 
187         NodeList actionListChildren = actionListNode.getChildNodes();
188         for (int i = 0; i < actionListChildren.getLength(); i++) {
189             Node actionListChild = actionListChildren.item(i);
190 
191             if (actionListChild.getNodeType() != Node.ELEMENT_NODE)
192                 continue;
193 
194             if (ELEMENT.action.equals(actionListChild)) {
195                 MutableAction action = new MutableAction();
196                 hydrateAction(action, actionListChild);
197                 descriptor.actions.add(action);
198             }
199         }
200     }
201 
202     public void hydrateAction(MutableAction action, Node actionNode) {
203 
204         NodeList actionNodeChildren = actionNode.getChildNodes();
205         for (int i = 0; i < actionNodeChildren.getLength(); i++) {
206             Node actionNodeChild = actionNodeChildren.item(i);
207 
208             if (actionNodeChild.getNodeType() != Node.ELEMENT_NODE)
209                 continue;
210 
211             if (ELEMENT.name.equals(actionNodeChild)) {
212                 action.name = XMLUtil.getTextContent(actionNodeChild);
213             } else if (ELEMENT.argumentList.equals(actionNodeChild)) {
214 
215 
216                 NodeList argumentChildren = actionNodeChild.getChildNodes();
217                 for (int j = 0; j < argumentChildren.getLength(); j++) {
218                     Node argumentChild = argumentChildren.item(j);
219 
220                     if (argumentChild.getNodeType() != Node.ELEMENT_NODE)
221                         continue;
222 
223                     MutableActionArgument actionArgument = new MutableActionArgument();
224                     hydrateActionArgument(actionArgument, argumentChild);
225                     action.arguments.add(actionArgument);
226                 }
227             }
228         }
229 
230     }
231 
232     public void hydrateActionArgument(MutableActionArgument actionArgument, Node actionArgumentNode) {
233 
234         NodeList argumentNodeChildren = actionArgumentNode.getChildNodes();
235         for (int i = 0; i < argumentNodeChildren.getLength(); i++) {
236             Node argumentNodeChild = argumentNodeChildren.item(i);
237 
238             if (argumentNodeChild.getNodeType() != Node.ELEMENT_NODE)
239                 continue;
240 
241             if (ELEMENT.name.equals(argumentNodeChild)) {
242                 actionArgument.name = XMLUtil.getTextContent(argumentNodeChild);
243             } else if (ELEMENT.direction.equals(argumentNodeChild)) {
244                 String directionString = XMLUtil.getTextContent(argumentNodeChild);
245                 try {
246                     actionArgument.direction = ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ROOT));
247                 } catch (IllegalArgumentException ex) {
248                     // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT
249                     log.warning("UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString);
250                     actionArgument.direction = ActionArgument.Direction.IN;
251                 }
252             } else if (ELEMENT.relatedStateVariable.equals(argumentNodeChild)) {
253                 actionArgument.relatedStateVariable = XMLUtil.getTextContent(argumentNodeChild);
254             } else if (ELEMENT.retval.equals(argumentNodeChild)) {
255                 actionArgument.retval = true;
256             }
257         }
258     }
259 
260     public void hydrateServiceStateTableList(MutableService descriptor, Node serviceStateTableNode) {
261 
262         NodeList serviceStateTableChildren = serviceStateTableNode.getChildNodes();
263         for (int i = 0; i < serviceStateTableChildren.getLength(); i++) {
264             Node serviceStateTableChild = serviceStateTableChildren.item(i);
265 
266             if (serviceStateTableChild.getNodeType() != Node.ELEMENT_NODE)
267                 continue;
268 
269             if (ELEMENT.stateVariable.equals(serviceStateTableChild)) {
270                 MutableStateVariable stateVariable = new MutableStateVariable();
271                 hydrateStateVariable(stateVariable, (Element) serviceStateTableChild);
272                 descriptor.stateVariables.add(stateVariable);
273             }
274         }
275     }
276 
277     public void hydrateStateVariable(MutableStateVariable stateVariable, Element stateVariableElement) {
278 
279         stateVariable.eventDetails = new StateVariableEventDetails(
280                 stateVariableElement.getAttribute("sendEvents") != null &&
281                         stateVariableElement.getAttribute(ATTRIBUTE.sendEvents.toString()).toUpperCase(Locale.ROOT).equals("YES")
282         );
283 
284         NodeList stateVariableChildren = stateVariableElement.getChildNodes();
285         for (int i = 0; i < stateVariableChildren.getLength(); i++) {
286             Node stateVariableChild = stateVariableChildren.item(i);
287 
288             if (stateVariableChild.getNodeType() != Node.ELEMENT_NODE)
289                 continue;
290 
291             if (ELEMENT.name.equals(stateVariableChild)) {
292                 stateVariable.name = XMLUtil.getTextContent(stateVariableChild);
293             } else if (ELEMENT.dataType.equals(stateVariableChild)) {
294                 String dtName = XMLUtil.getTextContent(stateVariableChild);
295                 Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(dtName);
296                 stateVariable.dataType = builtin != null ? builtin.getDatatype() : new CustomDatatype(dtName);
297             } else if (ELEMENT.defaultValue.equals(stateVariableChild)) {
298                 stateVariable.defaultValue = XMLUtil.getTextContent(stateVariableChild);
299             } else if (ELEMENT.allowedValueList.equals(stateVariableChild)) {
300 
301                 List<String> allowedValues = new ArrayList<>();
302 
303                 NodeList allowedValueListChildren = stateVariableChild.getChildNodes();
304                 for (int j = 0; j < allowedValueListChildren.getLength(); j++) {
305                     Node allowedValueListChild = allowedValueListChildren.item(j);
306 
307                     if (allowedValueListChild.getNodeType() != Node.ELEMENT_NODE)
308                         continue;
309 
310                     if (ELEMENT.allowedValue.equals(allowedValueListChild))
311                         allowedValues.add(XMLUtil.getTextContent(allowedValueListChild));
312                 }
313 
314                 stateVariable.allowedValues = allowedValues;
315 
316             } else if (ELEMENT.allowedValueRange.equals(stateVariableChild)) {
317 
318                 MutableAllowedValueRange range = new MutableAllowedValueRange();
319 
320                 NodeList allowedValueRangeChildren = stateVariableChild.getChildNodes();
321                 for (int j = 0; j < allowedValueRangeChildren.getLength(); j++) {
322                     Node allowedValueRangeChild = allowedValueRangeChildren.item(j);
323 
324                     if (allowedValueRangeChild.getNodeType() != Node.ELEMENT_NODE)
325                         continue;
326 
327                     if (ELEMENT.minimum.equals(allowedValueRangeChild)) {
328                         try {
329                             range.minimum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
330                         } catch (Exception ex) {
331                         }
332                     } else if (ELEMENT.maximum.equals(allowedValueRangeChild)) {
333                         try {
334                             range.maximum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
335                         } catch (Exception ex) {
336                         }
337                     } else if (ELEMENT.step.equals(allowedValueRangeChild)) {
338                         try {
339                             range.step = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
340                         } catch (Exception ex) {
341                         }
342                     }
343                 }
344 
345                 stateVariable.allowedValueRange = range;
346             }
347         }
348     }
349 
350     public String generate(Service service) throws DescriptorBindingException {
351         try {
352             log.fine("Generating XML descriptor from service model: " + service);
353 
354             return XMLUtil.documentToString(buildDOM(service));
355 
356         } catch (Exception ex) {
357             throw new DescriptorBindingException("Could not build DOM: " + ex.getMessage(), ex);
358         }
359     }
360 
361     public Document buildDOM(Service service) throws DescriptorBindingException {
362 
363         try {
364             log.fine("Generating XML descriptor from service model: " + service);
365 
366             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
367             factory.setNamespaceAware(true);
368 
369             Document d = factory.newDocumentBuilder().newDocument();
370             generateScpd(service, d);
371 
372             return d;
373 
374         } catch (Exception ex) {
375             throw new DescriptorBindingException("Could not generate service descriptor: " + ex.getMessage(), ex);
376         }
377     }
378 
379     private void generateScpd(Service serviceModel, Document descriptor) {
380 
381         Element scpdElement = descriptor.createElementNS(Descriptor.Service.NAMESPACE_URI, ELEMENT.scpd.toString());
382         descriptor.appendChild(scpdElement);
383 
384         generateSpecVersion(serviceModel, descriptor, scpdElement);
385         if (serviceModel.hasActions()) {
386             generateActionList(serviceModel, descriptor, scpdElement);
387         }
388         generateServiceStateTable(serviceModel, descriptor, scpdElement);
389     }
390 
391     private void generateSpecVersion(Service serviceModel, Document descriptor, Element rootElement) {
392         Element specVersionElement = appendNewElement(descriptor, rootElement, ELEMENT.specVersion);
393         appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.major, serviceModel.getDevice().getVersion().getMajor());
394         appendNewElementIfNotNull(descriptor, specVersionElement, ELEMENT.minor, serviceModel.getDevice().getVersion().getMinor());
395     }
396 
397     private void generateActionList(Service serviceModel, Document descriptor, Element scpdElement) {
398 
399         Element actionListElement = appendNewElement(descriptor, scpdElement, ELEMENT.actionList);
400 
401         for (Action action : serviceModel.getActions()) {
402             if (!action.getName().equals(QueryStateVariableAction.ACTION_NAME))
403                 generateAction(action, descriptor, actionListElement);
404         }
405     }
406 
407     private void generateAction(Action action, Document descriptor, Element actionListElement) {
408 
409         Element actionElement = appendNewElement(descriptor, actionListElement, ELEMENT.action);
410 
411         appendNewElementIfNotNull(descriptor, actionElement, ELEMENT.name, action.getName());
412 
413         if (action.hasArguments()) {
414             Element argumentListElement = appendNewElement(descriptor, actionElement, ELEMENT.argumentList);
415             for (ActionArgument actionArgument : action.getArguments()) {
416                 generateActionArgument(actionArgument, descriptor, argumentListElement);
417             }
418         }
419     }
420 
421     private void generateActionArgument(ActionArgument actionArgument, Document descriptor, Element actionElement) {
422 
423         Element actionArgumentElement = appendNewElement(descriptor, actionElement, ELEMENT.argument);
424 
425         appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.name, actionArgument.getName());
426         appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.direction, actionArgument.getDirection().toString().toLowerCase(Locale.ROOT));
427         if (actionArgument.isReturnValue()) {
428             // TODO: UPNP VIOLATION: WMP12 will discard RenderingControl service if it contains <retval> tags
429             log.warning("UPnP specification violation: Not producing <retval> element to be compatible with WMP12: " + actionArgument);
430             // appendNewElement(descriptor, actionArgumentElement, ELEMENT.retval);
431         }
432         appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.relatedStateVariable, actionArgument.getRelatedStateVariableName());
433     }
434 
435     private void generateServiceStateTable(Service serviceModel, Document descriptor, Element scpdElement) {
436 
437         Element serviceStateTableElement = appendNewElement(descriptor, scpdElement, ELEMENT.serviceStateTable);
438 
439         for (StateVariable stateVariable : serviceModel.getStateVariables()) {
440             generateStateVariable(stateVariable, descriptor, serviceStateTableElement);
441         }
442     }
443 
444     private void generateStateVariable(StateVariable stateVariable, Document descriptor, Element serviveStateTableElement) {
445 
446         Element stateVariableElement = appendNewElement(descriptor, serviveStateTableElement, ELEMENT.stateVariable);
447 
448         appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.name, stateVariable.getName());
449 
450         if (stateVariable.getTypeDetails().getDatatype() instanceof CustomDatatype) {
451             appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.dataType,
452                     ((CustomDatatype)stateVariable.getTypeDetails().getDatatype()).getName());
453         } else {
454             appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.dataType,
455                     stateVariable.getTypeDetails().getDatatype().getBuiltin().getDescriptorName());
456         }
457 
458         appendNewElementIfNotNull(descriptor, stateVariableElement, ELEMENT.defaultValue,
459                 stateVariable.getTypeDetails().getDefaultValue());
460 
461         // The default is 'yes' but we generate it anyway just to be sure
462         if (stateVariable.getEventDetails().isSendEvents()) {
463             stateVariableElement.setAttribute(ATTRIBUTE.sendEvents.toString(), "yes");
464         } else {
465             stateVariableElement.setAttribute(ATTRIBUTE.sendEvents.toString(), "no");
466         }
467 
468         if (stateVariable.getTypeDetails().getAllowedValues() != null) {
469             Element allowedValueListElement = appendNewElement(descriptor, stateVariableElement, ELEMENT.allowedValueList);
470             for (String allowedValue : stateVariable.getTypeDetails().getAllowedValues()) {
471                 appendNewElementIfNotNull(descriptor, allowedValueListElement, ELEMENT.allowedValue, allowedValue);
472             }
473         }
474 
475         if (stateVariable.getTypeDetails().getAllowedValueRange() != null) {
476             Element allowedValueRangeElement = appendNewElement(descriptor, stateVariableElement, ELEMENT.allowedValueRange);
477             appendNewElementIfNotNull(
478                     descriptor, allowedValueRangeElement, ELEMENT.minimum, stateVariable.getTypeDetails().getAllowedValueRange().getMinimum()
479             );
480             appendNewElementIfNotNull(
481                     descriptor, allowedValueRangeElement, ELEMENT.maximum, stateVariable.getTypeDetails().getAllowedValueRange().getMaximum()
482             );
483             if (stateVariable.getTypeDetails().getAllowedValueRange().getStep() >= 1l) {
484                 appendNewElementIfNotNull(
485                         descriptor, allowedValueRangeElement, ELEMENT.step, stateVariable.getTypeDetails().getAllowedValueRange().getStep()
486                 );
487             }
488         }
489 
490     }
491 
492     public void warning(SAXParseException e) throws SAXException {
493         log.warning(e.toString());
494     }
495 
496     public void error(SAXParseException e) throws SAXException {
497         throw e;
498     }
499 
500     public void fatalError(SAXParseException e) throws SAXException {
501         throw e;
502     }
503 }
504