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.model.ValidationException;
19  import org.fourthline.cling.model.meta.Device;
20  import org.seamless.util.Exceptions;
21  import org.seamless.xml.ParserException;
22  import org.seamless.xml.XmlPullParserUtils;
23  import org.xml.sax.SAXParseException;
24  
25  import java.util.Locale;
26  import java.util.logging.Logger;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  /**
31   * @author Michael Pujos
32   */
33  public class RecoveringUDA10DeviceDescriptorBinderImpl extends UDA10DeviceDescriptorBinderImpl {
34  
35      private static Logger log = Logger.getLogger(RecoveringUDA10DeviceDescriptorBinderImpl.class.getName());
36  
37      @Override
38      public <D extends Device> D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {
39  
40          D device = null;
41          DescriptorBindingException originalException;
42          try {
43  
44              try {
45                  if (descriptorXml != null)
46                    descriptorXml = descriptorXml.trim(); // Always trim whitespace
47                  device = super.describe(undescribedDevice, descriptorXml);
48                  return device;
49              } catch (DescriptorBindingException ex) {
50                  log.warning("Regular parsing failed: " + Exceptions.unwrap(ex).getMessage());
51                  originalException = ex;
52              }
53  
54              String fixedXml;
55              // The following modifications are not cumulative!
56  
57              fixedXml = fixGarbageLeadingChars(descriptorXml);
58              if (fixedXml != null) {
59                  try {
60                      device = super.describe(undescribedDevice, fixedXml);
61                      return device;
62                  } catch (DescriptorBindingException ex) {
63                      log.warning("Removing leading garbage didn't work: " + Exceptions.unwrap(ex).getMessage());
64                  }
65              }
66  
67              fixedXml = fixGarbageTrailingChars(descriptorXml, originalException);
68              if (fixedXml != null) {
69                  try {
70                      device = super.describe(undescribedDevice, fixedXml);
71                      return device;
72                  } catch (DescriptorBindingException ex) {
73                      log.warning("Removing trailing garbage didn't work: " + Exceptions.unwrap(ex).getMessage());
74                  }
75              }
76  
77              // Try to fix "up to five" missing namespace declarations
78              DescriptorBindingException lastException = originalException;
79              fixedXml = descriptorXml;
80              for (int retryCount = 0; retryCount < 5; retryCount++) {
81                  fixedXml = fixMissingNamespaces(fixedXml, lastException);
82                  if (fixedXml != null) {
83                      try {
84                          device = super.describe(undescribedDevice, fixedXml);
85                          return device;
86                      } catch (DescriptorBindingException ex) {
87                          log.warning("Fixing namespace prefix didn't work: " + Exceptions.unwrap(ex).getMessage());
88                          lastException = ex;
89                      }
90                  } else {
91                      break; // We can stop, no more namespace fixing can be done
92                  }
93              }
94  
95              fixedXml = XmlPullParserUtils.fixXMLEntities(descriptorXml);
96              if(!fixedXml.equals(descriptorXml)) {
97                  try {
98                      device = super.describe(undescribedDevice, fixedXml);
99                      return device;
100                 } catch (DescriptorBindingException ex) {
101                     log.warning("Fixing XML entities didn't work: " + Exceptions.unwrap(ex).getMessage());
102                 }
103             }
104 
105             handleInvalidDescriptor(descriptorXml, originalException);
106 
107         } catch (ValidationException ex) {
108             device = handleInvalidDevice(descriptorXml, device, ex);
109             if (device != null)
110                 return device;
111         }
112         throw new IllegalStateException("No device produced, did you swallow exceptions in your subclass?");
113     }
114 
115     private String fixGarbageLeadingChars(String descriptorXml) {
116     		/* Recover this:
117 
118     		HTTP/1.1 200 OK
119     		Content-Length: 4268
120     		Content-Type: text/xml; charset="utf-8"
121     		Server: Microsoft-Windows/6.2 UPnP/1.0 UPnP-Device-Host/1.0 Microsoft-HTTPAPI/2.0
122     		Date: Sun, 07 Apr 2013 02:11:30 GMT
123 
124     		@7:5 in java.io.StringReader@407f6b00) : HTTP/1.1 200 OK
125     		Content-Length: 4268
126     		Content-Type: text/xml; charset="utf-8"
127     		Server: Microsoft-Windows/6.2 UPnP/1.0 UPnP-Device-Host/1.0 Microsoft-HTTPAPI/2.0
128     		Date: Sun, 07 Apr 2013 02:11:30 GMT
129 
130     		<?xml version="1.0"?>...
131     	    */
132 
133     		int index = descriptorXml.indexOf("<?xml");
134     		if(index == -1) return descriptorXml;
135     		return descriptorXml.substring(index);
136     	}
137 
138     protected String fixGarbageTrailingChars(String descriptorXml, DescriptorBindingException ex) {
139         int index = descriptorXml.indexOf("</root>");
140         if (index == -1) {
141             log.warning("No closing </root> element in descriptor");
142             return null;
143         }
144         if (descriptorXml.length() != index + "</root>".length()) {
145             log.warning("Detected garbage characters after <root> node, removing");
146             return descriptorXml.substring(0, index) + "</root>";
147         }
148         return null;
149     }
150 
151     protected String fixMissingNamespaces(String descriptorXml, DescriptorBindingException ex) {
152         // Windows: org.fourthline.cling.binding.xml.DescriptorBindingException: Could not parse device descriptor: org.seamless.xml.ParserException: org.xml.sax.SAXParseException: The prefix "dlna" for element "dlna:X_DLNADOC" is not bound.
153         // Android: org.xmlpull.v1.XmlPullParserException: undefined prefix: dlna (position:START_TAG <{null}dlna:X_DLNADOC>@19:17 in java.io.StringReader@406dff48)
154 
155         // We can only handle certain exceptions, depending on their type and message
156         Throwable cause = ex.getCause();
157         if (!((cause instanceof SAXParseException) || (cause instanceof ParserException)))
158             return null;
159         String message = cause.getMessage();
160         if (message == null)
161             return null;
162 
163         Pattern pattern = Pattern.compile("The prefix \"(.*)\" for element"); // Windows
164         Matcher matcher = pattern.matcher(message);
165         if (!matcher.find() || matcher.groupCount() != 1) {
166             pattern = Pattern.compile("undefined prefix: ([^ ]*)"); // Android
167             matcher = pattern.matcher(message);
168             if (!matcher.find() || matcher.groupCount() != 1)
169                 return null;
170         }
171 
172         String missingNS = matcher.group(1);
173         log.warning("Fixing missing namespace declaration for: " + missingNS);
174 
175         // Extract <root> attributes
176         pattern = Pattern.compile("<root([^>]*)");
177         matcher = pattern.matcher(descriptorXml);
178         if (!matcher.find() || matcher.groupCount() != 1) {
179             log.fine("Could not find <root> element attributes");
180             return null;
181         }
182 
183         String rootAttributes = matcher.group(1);
184         log.fine("Preserving existing <root> element attributes/namespace declarations: " + matcher.group(0));
185 
186         // Extract <root> body
187         pattern = Pattern.compile("<root[^>]*>(.*)</root>", Pattern.DOTALL);
188         matcher = pattern.matcher(descriptorXml);
189         if (!matcher.find() || matcher.groupCount() != 1) {
190             log.fine("Could not extract body of <root> element");
191             return null;
192         }
193 
194         String rootBody = matcher.group(1);
195 
196         // Add missing namespace, it only matters that it is defined, not that it is correct
197         return "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
198             + "<root "
199             + String.format(Locale.ROOT, "xmlns:%s=\"urn:schemas-dlna-org:device-1-0\"", missingNS) + rootAttributes + ">"
200             + rootBody
201             + "</root>";
202 
203         // TODO: Should we match different undeclared prefixes with their correct namespace?
204         // So if it's "dlna" we use "urn:schemas-dlna-org:device-1-0" etc.
205     }
206 
207     /**
208      * Handle processing errors while reading XML descriptors.
209      * <p/>
210      * <p>
211      * Typically you want to log this problem or create an error report, and in any
212      * case, throw a {@link DescriptorBindingException} to notify the caller of the
213      * binder of this failure. The default implementation simply rethrows the
214      * given exception.
215      * </p>
216      *
217      * @param xml       The original XML causing the parsing failure.
218      * @param exception The original exception while parsing the XML.
219      */
220     protected void handleInvalidDescriptor(String xml, DescriptorBindingException exception)
221         throws DescriptorBindingException {
222         throw exception;
223     }
224 
225     /**
226      * Handle processing errors while binding XML descriptors.
227      * <p/>
228      * <p>
229      * Typically you want to log this problem or create an error report. You
230      * should throw a {@link ValidationException} to notify the caller of the
231      * binder of failure. The default implementation simply rethrows the
232      * given exception.
233      * </p>
234      * <p>
235      * This method gives you a final chance to fix the problem, instead of
236      * throwing an exception, you could try to create valid {@link Device}
237      * model and return it.
238      * </p>
239      *
240      * @param xml       The original XML causing the binding failure.
241      * @param device    The unfinished {@link Device} that failed validation
242      * @param exception The errors found when validating the {@link Device} model.
243      * @return Device A "fixed" {@link Device} model, instead of throwing an exception.
244      */
245     protected <D extends Device> D handleInvalidDevice(String xml, D device, ValidationException exception)
246         throws ValidationException {
247         throw exception;
248     }
249 }