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.transport.impl;
17  
18  import org.fourthline.cling.model.Constants;
19  import org.fourthline.cling.model.XMLUtil;
20  import org.fourthline.cling.model.action.ActionArgumentValue;
21  import org.fourthline.cling.model.action.ActionException;
22  import org.fourthline.cling.model.action.ActionInvocation;
23  import org.fourthline.cling.model.message.control.ActionMessage;
24  import org.fourthline.cling.model.message.control.ActionRequestMessage;
25  import org.fourthline.cling.model.message.control.ActionResponseMessage;
26  import org.fourthline.cling.model.meta.ActionArgument;
27  import org.fourthline.cling.model.types.ErrorCode;
28  import org.fourthline.cling.model.types.InvalidValueException;
29  import org.fourthline.cling.transport.spi.SOAPActionProcessor;
30  import org.fourthline.cling.model.UnsupportedDataException;
31  import org.w3c.dom.Attr;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.Element;
34  import org.w3c.dom.Node;
35  import org.w3c.dom.NodeList;
36  import org.xml.sax.ErrorHandler;
37  import org.xml.sax.InputSource;
38  import org.xml.sax.SAXException;
39  import org.xml.sax.SAXParseException;
40  
41  import javax.xml.parsers.DocumentBuilder;
42  import javax.xml.parsers.DocumentBuilderFactory;
43  import javax.xml.parsers.FactoryConfigurationError;
44  
45  import java.io.StringReader;
46  import java.util.ArrayList;
47  import java.util.Arrays;
48  import java.util.List;
49  import java.util.logging.Level;
50  import java.util.logging.Logger;
51  
52  /**
53   * Default implementation based on the <em>W3C DOM</em> XML processing API.
54   *
55   * @author Christian Bauer
56   */
57  public class SOAPActionProcessorImpl implements SOAPActionProcessor, ErrorHandler {
58  
59      private static Logger log = Logger.getLogger(SOAPActionProcessor.class.getName());
60      
61      protected DocumentBuilderFactory createDocumentBuilderFactory() throws FactoryConfigurationError {
62      	return DocumentBuilderFactory.newInstance();
63      }
64  
65      public void writeBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
66  
67          log.fine("Writing body of " + requestMessage + " for: " + actionInvocation);
68  
69          try {
70  
71              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
72              factory.setNamespaceAware(true);
73              Document d = factory.newDocumentBuilder().newDocument();
74              Element body = writeBodyElement(d);
75  
76              writeBodyRequest(d, body, requestMessage, actionInvocation);
77  
78              if (log.isLoggable(Level.FINER)) {
79                  log.finer("===================================== SOAP BODY BEGIN ============================================");
80                  log.finer(requestMessage.getBodyString());
81                  log.finer("-===================================== SOAP BODY END ============================================");
82              }
83  
84          } catch (Exception ex) {
85              throw new UnsupportedDataException("Can't transform message payload: " + ex, ex);
86          }
87      }
88  
89      public void writeBody(ActionResponseMessage responseMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
90  
91          log.fine("Writing body of " + responseMessage + " for: " + actionInvocation);
92  
93          try {
94  
95              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
96              factory.setNamespaceAware(true);
97              Document d = factory.newDocumentBuilder().newDocument();
98              Element body = writeBodyElement(d);
99  
100             if (actionInvocation.getFailure() != null) {
101                 writeBodyFailure(d, body, responseMessage, actionInvocation);
102             } else {
103                 writeBodyResponse(d, body, responseMessage, actionInvocation);
104             }
105 
106             if (log.isLoggable(Level.FINER)) {
107                 log.finer("===================================== SOAP BODY BEGIN ============================================");
108                 log.finer(responseMessage.getBodyString());
109                 log.finer("-===================================== SOAP BODY END ============================================");
110             }
111 
112         } catch (Exception ex) {
113             throw new UnsupportedDataException("Can't transform message payload: " + ex, ex);
114         }
115     }
116 
117     public void readBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
118 
119         log.fine("Reading body of " + requestMessage + " for: " + actionInvocation);
120         if (log.isLoggable(Level.FINER)) {
121             log.finer("===================================== SOAP BODY BEGIN ============================================");
122             log.finer(requestMessage.getBodyString());
123             log.finer("-===================================== SOAP BODY END ============================================");
124         }
125 
126         String body = getMessageBody(requestMessage);
127         try {
128 
129             DocumentBuilderFactory factory = createDocumentBuilderFactory();
130             factory.setNamespaceAware(true);
131             DocumentBuilder documentBuilder = factory.newDocumentBuilder();
132             documentBuilder.setErrorHandler(this);
133 
134             Document d = documentBuilder.parse(new InputSource(new StringReader(body)));
135 
136             Element bodyElement = readBodyElement(d);
137 
138             readBodyRequest(d, bodyElement, requestMessage, actionInvocation);
139 
140         } catch (Exception ex) {
141             throw new UnsupportedDataException("Can't transform message payload: " + ex, ex, body);
142         }
143     }
144 
145     public void readBody(ActionResponseMessage responseMsg, ActionInvocation actionInvocation) throws UnsupportedDataException {
146 
147         log.fine("Reading body of " + responseMsg + " for: " + actionInvocation);
148         if (log.isLoggable(Level.FINER)) {
149             log.finer("===================================== SOAP BODY BEGIN ============================================");
150             log.finer(responseMsg.getBodyString());
151             log.finer("-===================================== SOAP BODY END ============================================");
152         }
153 
154         String body = getMessageBody(responseMsg);
155         try {
156 
157             DocumentBuilderFactory factory = createDocumentBuilderFactory();
158             factory.setNamespaceAware(true);
159             DocumentBuilder documentBuilder = factory.newDocumentBuilder();
160             documentBuilder.setErrorHandler(this);
161 
162             Document d = documentBuilder.parse(new InputSource(new StringReader(body)));
163 
164             Element bodyElement = readBodyElement(d);
165 
166             ActionException failure = readBodyFailure(d, bodyElement);
167 
168             if (failure == null) {
169                 readBodyResponse(d, bodyElement, responseMsg, actionInvocation);
170             } else {
171                 actionInvocation.setFailure(failure);
172             }
173 
174         } catch (Exception ex) {
175     		throw new UnsupportedDataException("Can't transform message payload: " + ex, ex, body);
176         }
177     }
178 
179     /* ##################################################################################################### */
180 
181     protected void writeBodyFailure(Document d,
182                                     Element bodyElement,
183                                     ActionResponseMessage message,
184                                     ActionInvocation actionInvocation) throws Exception {
185 
186         writeFaultElement(d, bodyElement, actionInvocation);
187         message.setBody(toString(d));
188     }
189 
190     protected void writeBodyRequest(Document d,
191                                     Element bodyElement,
192                                     ActionRequestMessage message,
193                                     ActionInvocation actionInvocation) throws Exception {
194 
195         Element actionRequestElement = writeActionRequestElement(d, bodyElement, message, actionInvocation);
196         writeActionInputArguments(d, actionRequestElement, actionInvocation);
197         message.setBody(toString(d));
198 
199     }
200 
201     protected void writeBodyResponse(Document d,
202                                      Element bodyElement,
203                                      ActionResponseMessage message,
204                                      ActionInvocation actionInvocation) throws Exception {
205 
206         Element actionResponseElement = writeActionResponseElement(d, bodyElement, message, actionInvocation);
207         writeActionOutputArguments(d, actionResponseElement, actionInvocation);
208         message.setBody(toString(d));
209     }
210 
211     protected ActionException readBodyFailure(Document d, Element bodyElement) throws Exception {
212         return readFaultElement(bodyElement);
213     }
214 
215     protected void readBodyRequest(Document d,
216                                    Element bodyElement,
217                                    ActionRequestMessage message,
218                                    ActionInvocation actionInvocation) throws Exception {
219 
220         Element actionRequestElement = readActionRequestElement(bodyElement, message, actionInvocation);
221         readActionInputArguments(actionRequestElement, actionInvocation);
222     }
223 
224     protected void readBodyResponse(Document d,
225                                     Element bodyElement,
226                                     ActionResponseMessage message,
227                                     ActionInvocation actionInvocation) throws Exception {
228 
229         Element actionResponse = readActionResponseElement(bodyElement, actionInvocation);
230         readActionOutputArguments(actionResponse, actionInvocation);
231     }
232 
233     /* ##################################################################################################### */
234 
235     protected Element writeBodyElement(Document d) {
236 
237         Element envelopeElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Envelope");
238         Attr encodingStyleAttr = d.createAttributeNS(Constants.SOAP_NS_ENVELOPE, "s:encodingStyle");
239         encodingStyleAttr.setValue(Constants.SOAP_URI_ENCODING_STYLE);
240         envelopeElement.setAttributeNode(encodingStyleAttr);
241         d.appendChild(envelopeElement);
242 
243         Element bodyElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Body");
244         envelopeElement.appendChild(bodyElement);
245 
246         return bodyElement;
247     }
248 
249     protected Element readBodyElement(Document d) {
250 
251         Element envelopeElement = d.getDocumentElement();
252         
253         if (envelopeElement == null || !getUnprefixedNodeName(envelopeElement).equals("Envelope")) {
254             throw new RuntimeException("Response root element was not 'Envelope'");
255         }
256 
257         NodeList envelopeElementChildren = envelopeElement.getChildNodes();
258         for (int i = 0; i < envelopeElementChildren.getLength(); i++) {
259             Node envelopeChild = envelopeElementChildren.item(i);
260 
261             if (envelopeChild.getNodeType() != Node.ELEMENT_NODE)
262                 continue;
263 
264             if (getUnprefixedNodeName(envelopeChild).equals("Body")) {
265                 return (Element) envelopeChild;
266             }
267         }
268 
269         throw new RuntimeException("Response envelope did not contain 'Body' child element");
270     }
271 
272     /* ##################################################################################################### */
273 
274     protected Element writeActionRequestElement(Document d,
275                                                 Element bodyElement,
276                                                 ActionRequestMessage message,
277                                                 ActionInvocation actionInvocation) {
278 
279         log.fine("Writing action request element: " + actionInvocation.getAction().getName());
280 
281         Element actionRequestElement = d.createElementNS(
282                 message.getActionNamespace(),
283                 "u:" + actionInvocation.getAction().getName()
284         );
285         bodyElement.appendChild(actionRequestElement);
286 
287         return actionRequestElement;
288     }
289 
290     protected Element readActionRequestElement(Element bodyElement,
291                                                ActionRequestMessage message,
292                                                ActionInvocation actionInvocation) {
293         NodeList bodyChildren = bodyElement.getChildNodes();
294 
295         log.fine("Looking for action request element matching namespace:" + message.getActionNamespace());
296 
297         for (int i = 0; i < bodyChildren.getLength(); i++) {
298             Node bodyChild = bodyChildren.item(i);
299 
300             if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
301                 continue;
302 
303             String unprefixedName = getUnprefixedNodeName(bodyChild);
304             if (unprefixedName.equals(actionInvocation.getAction().getName())) {
305                 if (bodyChild.getNamespaceURI() == null
306                     || !bodyChild.getNamespaceURI().equals(message.getActionNamespace()))
307                     throw new UnsupportedDataException(
308                         "Illegal or missing namespace on action request element: " + bodyChild
309                     );
310                 log.fine("Reading action request element: " + unprefixedName);
311                 return (Element) bodyChild;
312             }
313         }
314         throw new UnsupportedDataException(
315             "Could not read action request element matching namespace: " + message.getActionNamespace()
316         );
317     }
318 
319     /* ##################################################################################################### */
320 
321     protected Element writeActionResponseElement(Document d,
322                                                  Element bodyElement,
323                                                  ActionResponseMessage message,
324                                                  ActionInvocation actionInvocation) {
325 
326         log.fine("Writing action response element: " + actionInvocation.getAction().getName());
327         Element actionResponseElement = d.createElementNS(
328                 message.getActionNamespace(),
329                 "u:" + actionInvocation.getAction().getName() + "Response"
330         );
331         bodyElement.appendChild(actionResponseElement);
332 
333         return actionResponseElement;
334     }
335 
336     protected Element readActionResponseElement(Element bodyElement, ActionInvocation actionInvocation) {
337         NodeList bodyChildren = bodyElement.getChildNodes();
338 
339         for (int i = 0; i < bodyChildren.getLength(); i++) {
340             Node bodyChild = bodyChildren.item(i);
341 
342             if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
343                 continue;
344 
345             if (getUnprefixedNodeName(bodyChild).equals(actionInvocation.getAction().getName() + "Response")) {
346                 log.fine("Reading action response element: " + getUnprefixedNodeName(bodyChild));
347                 return (Element) bodyChild;
348             }
349         }
350         log.fine("Could not read action response element");
351         return null;
352     }
353 
354     /* ##################################################################################################### */
355 
356     protected void writeActionInputArguments(Document d,
357                                              Element actionRequestElement,
358                                              ActionInvocation actionInvocation) {
359 
360         for (ActionArgument argument : actionInvocation.getAction().getInputArguments()) {
361             log.fine("Writing action input argument: " + argument.getName());
362             String value = actionInvocation.getInput(argument) != null ? actionInvocation.getInput(argument).toString() : "";
363             XMLUtil.appendNewElement(d, actionRequestElement, argument.getName(), value);
364         }
365     }
366 
367     public void readActionInputArguments(Element actionRequestElement,
368                                          ActionInvocation actionInvocation) throws ActionException {
369         actionInvocation.setInput(
370                 readArgumentValues(
371                         actionRequestElement.getChildNodes(),
372                         actionInvocation.getAction().getInputArguments()
373                 )
374         );
375     }
376 
377     /* ##################################################################################################### */
378 
379     protected void writeActionOutputArguments(Document d,
380                                               Element actionResponseElement,
381                                               ActionInvocation actionInvocation) {
382 
383         for (ActionArgument argument : actionInvocation.getAction().getOutputArguments()) {
384             log.fine("Writing action output argument: " + argument.getName());
385             String value = actionInvocation.getOutput(argument) != null ? actionInvocation.getOutput(argument).toString() : "";
386             XMLUtil.appendNewElement(d, actionResponseElement, argument.getName(), value);
387         }
388     }
389 
390     protected void readActionOutputArguments(Element actionResponseElement,
391                                              ActionInvocation actionInvocation) throws ActionException {
392 
393         actionInvocation.setOutput(
394                 readArgumentValues(
395                         actionResponseElement.getChildNodes(),
396                         actionInvocation.getAction().getOutputArguments()
397                 )
398         );
399     }
400 
401     /* ##################################################################################################### */
402 
403     protected void writeFaultElement(Document d, Element bodyElement, ActionInvocation actionInvocation) {
404 
405         Element faultElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Fault");
406         bodyElement.appendChild(faultElement);
407 
408         // This stuff is really completely arbitrary nonsense... let's hope they fired the guy who decided this
409         XMLUtil.appendNewElement(d, faultElement, "faultcode", "s:Client");
410         XMLUtil.appendNewElement(d, faultElement, "faultstring", "UPnPError");
411 
412         Element detailElement = d.createElement("detail");
413         faultElement.appendChild(detailElement);
414 
415         Element upnpErrorElement = d.createElementNS(Constants.NS_UPNP_CONTROL_10, "UPnPError");
416         detailElement.appendChild(upnpErrorElement);
417 
418         int errorCode = actionInvocation.getFailure().getErrorCode();
419         String errorDescription = actionInvocation.getFailure().getMessage();
420 
421         log.fine("Writing fault element: " + errorCode + " - " + errorDescription);
422 
423         XMLUtil.appendNewElement(d, upnpErrorElement, "errorCode", Integer.toString(errorCode));
424         XMLUtil.appendNewElement(d, upnpErrorElement, "errorDescription", errorDescription);
425 
426     }
427 
428     protected ActionException readFaultElement(Element bodyElement) {
429 
430         boolean receivedFaultElement = false;
431         String errorCode = null;
432         String errorDescription = null;
433 
434         NodeList bodyChildren = bodyElement.getChildNodes();
435 
436         for (int i = 0; i < bodyChildren.getLength(); i++) {
437             Node bodyChild = bodyChildren.item(i);
438 
439             if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
440                 continue;
441 
442             if (getUnprefixedNodeName(bodyChild).equals("Fault")) {
443 
444                 receivedFaultElement = true;
445 
446                 NodeList faultChildren = bodyChild.getChildNodes();
447 
448                 for (int j = 0; j < faultChildren.getLength(); j++) {
449                     Node faultChild = faultChildren.item(j);
450 
451                     if (faultChild.getNodeType() != Node.ELEMENT_NODE)
452                         continue;
453 
454                     if (getUnprefixedNodeName(faultChild).equals("detail")) {
455 
456                         NodeList detailChildren = faultChild.getChildNodes();
457                         for (int x = 0; x < detailChildren.getLength(); x++) {
458                             Node detailChild = detailChildren.item(x);
459 
460                             if (detailChild.getNodeType() != Node.ELEMENT_NODE)
461                                 continue;
462 
463                             if (getUnprefixedNodeName(detailChild).equals("UPnPError")) {
464 
465                                 NodeList errorChildren = detailChild.getChildNodes();
466                                 for (int y = 0; y < errorChildren.getLength(); y++) {
467                                     Node errorChild = errorChildren.item(y);
468 
469                                     if (errorChild.getNodeType() != Node.ELEMENT_NODE)
470                                         continue;
471 
472                                     if (getUnprefixedNodeName(errorChild).equals("errorCode"))
473                                         errorCode = XMLUtil.getTextContent(errorChild);
474 
475                                     if (getUnprefixedNodeName(errorChild).equals("errorDescription"))
476                                         errorDescription = XMLUtil.getTextContent(errorChild);
477                                 }
478                             }
479                         }
480                     }
481                 }
482             }
483         }
484 
485         if (errorCode != null) {
486             try {
487                 int numericCode = Integer.valueOf(errorCode);
488                 ErrorCode standardErrorCode = ErrorCode.getByCode(numericCode);
489                 if (standardErrorCode != null) {
490                     log.fine("Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription);
491                     return new ActionException(standardErrorCode, errorDescription, false);
492                 } else {
493                     log.fine("Reading fault element: " + numericCode + " - " + errorDescription);
494                     return new ActionException(numericCode, errorDescription);
495                 }
496             } catch (NumberFormatException ex) {
497                 throw new RuntimeException("Error code was not a number");
498             }
499         } else if (receivedFaultElement) {
500             throw new RuntimeException("Received fault element but no error code");
501         }
502         return null;
503     }
504 
505 
506     /* ##################################################################################################### */
507 
508     protected String getMessageBody(ActionMessage message) throws UnsupportedDataException {
509         if (!message.isBodyNonEmptyString())
510             throw new UnsupportedDataException(
511                 "Can't transform null or non-string/zero-length body of: " + message
512             );
513         return message.getBodyString().trim();
514     }
515 
516     protected String toString(Document d) throws Exception {
517         // Just to be safe, no newline at the end
518         String output = XMLUtil.documentToString(d);
519         while (output.endsWith("\n") || output.endsWith("\r")) {
520             output = output.substring(0, output.length() - 1);
521         }
522 
523         return output;
524     }
525 
526     protected String getUnprefixedNodeName(Node node) {
527         return node.getPrefix() != null
528                 ? node.getNodeName().substring(node.getPrefix().length() + 1)
529                 : node.getNodeName();
530     }
531 
532     /**
533      * The UPnP spec says that action arguments must be in the order as declared
534      * by the service. This method however is lenient, the action argument nodes
535      * in the XML can be in any order, as long as they are all there everything
536      * is OK.
537      */
538     protected ActionArgumentValue[] readArgumentValues(NodeList nodeList, ActionArgument[] args)
539             throws ActionException {
540 
541         List<Node> nodes = getMatchingNodes(nodeList, args);
542 
543         ActionArgumentValue[] values = new ActionArgumentValue[args.length];
544 
545         for (int i = 0; i < args.length; i++) {
546         	
547             ActionArgument arg = args[i];
548             Node node = findActionArgumentNode(nodes, arg);
549             if(node == null) {
550                 throw new ActionException(
551                         ErrorCode.ARGUMENT_VALUE_INVALID,
552                         "Could not find argument '" + arg.getName() + "' node");
553             }
554             log.fine("Reading action argument: " + arg.getName());
555             String value = XMLUtil.getTextContent(node);
556             values[i] = createValue(arg, value);
557         }
558         return values;
559     }
560 
561     /**
562      * Finds all element nodes in the list that match any argument name or argument
563      * alias, throws {@link ActionException} if not all arguments were found.
564      */
565     protected List<Node> getMatchingNodes(NodeList nodeList, ActionArgument[] args) throws ActionException {
566 
567         List<String> names = new ArrayList<>();
568         for (ActionArgument argument : args) {
569             names.add(argument.getName());
570             names.addAll(Arrays.asList(argument.getAliases()));
571         }
572 
573         List<Node> matches = new ArrayList<>();
574         for (int i = 0; i < nodeList.getLength(); i++) {
575             Node child = nodeList.item(i);
576 
577             if (child.getNodeType() != Node.ELEMENT_NODE)
578                 continue;
579 
580             if (names.contains(getUnprefixedNodeName(child)))
581                 matches.add(child);
582         }
583 
584         if (matches.size() < args.length) {
585             throw new ActionException(
586                     ErrorCode.ARGUMENT_VALUE_INVALID,
587                     "Invalid number of input or output arguments in XML message, expected " + args.length + " but found " + matches.size()
588             );
589         }
590         return matches;
591     }
592 
593     /**
594      * Creates an instance of {@link ActionArgumentValue} and wraps an
595      * {@link InvalidValueException} as an {@link ActionException} with the
596      * appropriate {@link ErrorCode}.
597      */
598     protected ActionArgumentValue createValue(ActionArgument arg, String value) throws ActionException {
599         try {
600             return new ActionArgumentValue(arg, value);
601         } catch (InvalidValueException ex) {
602             throw new ActionException(
603                     ErrorCode.ARGUMENT_VALUE_INVALID,
604                     "Wrong type or invalid value for '" + arg.getName() + "': " + ex.getMessage(),
605                     ex
606             );
607         }
608     }
609 
610     /**
611      * Returns the node with the same unprefixed name as the action argument
612      * name/alias or <code>null</code>.
613      */
614     protected Node findActionArgumentNode(List<Node> nodes, ActionArgument arg) {
615     	for(Node node : nodes) {
616     		if(arg.isNameOrAlias(getUnprefixedNodeName(node))) return node;
617     	}
618     	return null;
619     }
620 
621     public void warning(SAXParseException e) throws SAXException {
622         log.warning(e.toString());
623     }
624 
625     public void error(SAXParseException e) throws SAXException {
626         throw e;
627     }
628 
629     public void fatalError(SAXParseException e) throws SAXException {
630         throw e;
631     }
632 }