• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.internal.repository;
18 
19 import com.android.annotations.VisibleForTesting;
20 import com.android.annotations.VisibleForTesting.Visibility;
21 import com.android.sdklib.repository.SdkAddonsListConstants;
22 
23 import org.w3c.dom.Document;
24 import org.w3c.dom.NamedNodeMap;
25 import org.w3c.dom.Node;
26 import org.xml.sax.InputSource;
27 import org.xml.sax.SAXException;
28 import org.xml.sax.SAXParseException;
29 
30 import java.io.ByteArrayInputStream;
31 import java.io.FileNotFoundException;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.net.UnknownHostException;
35 import java.util.ArrayList;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 
39 import javax.net.ssl.SSLKeyException;
40 import javax.xml.XMLConstants;
41 import javax.xml.parsers.DocumentBuilder;
42 import javax.xml.parsers.DocumentBuilderFactory;
43 import javax.xml.parsers.ParserConfigurationException;
44 import javax.xml.transform.stream.StreamSource;
45 import javax.xml.validation.Schema;
46 import javax.xml.validation.SchemaFactory;
47 import javax.xml.validation.Validator;
48 
49 /**
50  * Fetches and loads an sdk-addons-list XML.
51  * <p/>
52  * Such an XML contains a simple list of add-ons site that are to be loaded by default by the
53  * SDK Manager. <br/>
54  * The XML must conform to the sdk-addons-list-N.xsd. <br/>
55  * Constants used in the XML are defined in {@link SdkAddonsListConstants}.
56  */
57 public class AddonsListFetcher {
58 
59     /**
60      * An immutable structure representing an add-on site.
61      */
62     public static class Site {
63         private final String mUrl;
64         private final String mUiName;
65 
Site(String url, String uiName)66         private Site(String url, String uiName) {
67             mUrl = url.trim();
68             mUiName = uiName;
69         }
70 
getUrl()71         public String getUrl() {
72             return mUrl;
73         }
74 
getUiName()75         public String getUiName() {
76             return mUiName;
77         }
78     }
79 
80     /**
81      * Fetches the addons list from the given URL.
82      *
83      * @param monitor A monitor to report errors. Cannot be null.
84      * @param url The URL of an XML file resource that conforms to the latest sdk-addons-list-N.xsd.
85      *   For the default operation, use {@link SdkAddonsListConstants#URL_ADDON_LIST}.
86      *   Cannot be null.
87      * @return An array of {@link Site} on success (possibly empty), or null on error.
88      */
fetch(ITaskMonitor monitor, String url)89     public Site[] fetch(ITaskMonitor monitor, String url) {
90 
91         url = url == null ? "" : url.trim();
92 
93         monitor.setProgressMax(5);
94         monitor.setDescription("Fetching %1$s", url);
95         monitor.incProgress(1);
96 
97         Exception[] exception = new Exception[] { null };
98         Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
99         String[] validationError = new String[] { null };
100         Document validatedDoc = null;
101         String validatedUri = null;
102 
103         ByteArrayInputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
104 
105         if (xml != null) {
106             monitor.setDescription("Validate XML");
107 
108             // Explore the XML to find the potential XML schema version
109             int version = getXmlSchemaVersion(xml);
110 
111             if (version >= 1 && version <= SdkAddonsListConstants.NS_LATEST_VERSION) {
112                 // This should be a version we can handle. Try to validate it
113                 // and report any error as invalid XML syntax,
114 
115                 String uri = validateXml(xml, url, version, validationError, validatorFound);
116                 if (uri != null) {
117                     // Validation was successful
118                     validatedDoc = getDocument(xml, monitor);
119                     validatedUri = uri;
120 
121                 }
122             } else if (version > SdkAddonsListConstants.NS_LATEST_VERSION) {
123                 // The schema used is more recent than what is supported by this tool.
124                 // We don't have an upgrade-path support yet, so simply ignore the document.
125                 return null;
126             }
127         }
128 
129         // If any exception was handled during the URL fetch, display it now.
130         if (exception[0] != null) {
131             String reason = null;
132             if (exception[0] instanceof FileNotFoundException) {
133                 // FNF has no useful getMessage, so we need to special handle it.
134                 reason = "File not found";
135             } else if (exception[0] instanceof UnknownHostException &&
136                     exception[0].getMessage() != null) {
137                 // This has no useful getMessage yet could really use one
138                 reason = String.format("Unknown Host %1$s", exception[0].getMessage());
139             } else if (exception[0] instanceof SSLKeyException) {
140                 // That's a common error and we have a pref for it.
141                 reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
142             } else if (exception[0].getMessage() != null) {
143                 reason = exception[0].getMessage();
144             } else {
145                 // We don't know what's wrong. Let's give the exception class at least.
146                 reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
147             }
148 
149             monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason);
150         }
151 
152         if (validationError[0] != null) {
153             monitor.logError("%s", validationError[0]);  //$NON-NLS-1$
154         }
155 
156         // Stop here if we failed to validate the XML. We don't want to load it.
157         if (validatedDoc == null) {
158             return null;
159         }
160 
161         monitor.incProgress(1);
162 
163         Site[] result = null;
164 
165         if (xml != null) {
166             monitor.setDescription("Parse XML");
167             monitor.incProgress(1);
168             result = parseAddonsList(validatedDoc, validatedUri, monitor);
169         }
170 
171         // done
172         monitor.incProgress(1);
173 
174         return result;
175     }
176 
177     /**
178      * Fetches the document at the given URL and returns it as a stream. Returns
179      * null if anything wrong happens. References: <br/>
180      * URL Connection:
181      *
182      * @param urlString The URL to load, as a string.
183      * @param monitor {@link ITaskMonitor} related to this URL.
184      * @param outException If non null, where to store any exception that
185      *            happens during the fetch.
186      * @see UrlOpener UrlOpener, which handles all URL logic.
187      */
fetchUrl(String urlString, ITaskMonitor monitor, Exception[] outException)188     private ByteArrayInputStream fetchUrl(String urlString, ITaskMonitor monitor,
189             Exception[] outException) {
190         try {
191 
192             InputStream is = null;
193 
194             int inc = 65536;
195             int curr = 0;
196             byte[] result = new byte[inc];
197 
198             try {
199                 is = UrlOpener.openUrl(urlString, monitor);
200 
201                 int n;
202                 while ((n = is.read(result, curr, result.length - curr)) != -1) {
203                     curr += n;
204                     if (curr == result.length) {
205                         byte[] temp = new byte[curr + inc];
206                         System.arraycopy(result, 0, temp, 0, curr);
207                         result = temp;
208                     }
209                 }
210 
211                 return new ByteArrayInputStream(result, 0, curr);
212 
213             } finally {
214                 if (is != null) {
215                     try {
216                         is.close();
217                     } catch (IOException e) {
218                         // pass
219                     }
220                 }
221             }
222 
223         } catch (Exception e) {
224             if (outException != null) {
225                 outException[0] = e;
226             }
227         }
228 
229         return null;
230     }
231 
232     /**
233      * Manually parses the root element of the XML to extract the schema version
234      * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N"
235      * declaration.
236      *
237      * @return 1..{@link SdkAddonsListConstants#NS_LATEST_VERSION} for a valid schema version
238      *         or 0 if no schema could be found.
239      */
240     @VisibleForTesting(visibility=Visibility.PRIVATE)
getXmlSchemaVersion(InputStream xml)241     protected int getXmlSchemaVersion(InputStream xml) {
242         if (xml == null) {
243             return 0;
244         }
245 
246         // Get an XML document
247         Document doc = null;
248         try {
249             xml.reset();
250 
251             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
252             factory.setIgnoringComments(false);
253             factory.setValidating(false);
254 
255             // Parse the old document using a non namespace aware builder
256             factory.setNamespaceAware(false);
257             DocumentBuilder builder = factory.newDocumentBuilder();
258             doc = builder.parse(xml);
259 
260             // Prepare a new document using a namespace aware builder
261             factory.setNamespaceAware(true);
262             builder = factory.newDocumentBuilder();
263 
264         } catch (Exception e) {
265             // Failed to reset XML stream
266             // Failed to get builder factor
267             // Failed to create XML document builder
268             // Failed to parse XML document
269             // Failed to read XML document
270         }
271 
272         if (doc == null) {
273             return 0;
274         }
275 
276         // Check the root element is an XML with at least the following properties:
277         // <sdk:sdk-addons-list
278         //    xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N">
279         //
280         // Note that we don't have namespace support enabled, we just do it manually.
281 
282         Pattern nsPattern = Pattern.compile(SdkAddonsListConstants.NS_PATTERN);
283 
284         String prefix = null;
285         for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
286             if (child.getNodeType() == Node.ELEMENT_NODE) {
287                 prefix = null;
288                 String name = child.getNodeName();
289                 int pos = name.indexOf(':');
290                 if (pos > 0 && pos < name.length() - 1) {
291                     prefix = name.substring(0, pos);
292                     name = name.substring(pos + 1);
293                 }
294                 if (SdkAddonsListConstants.NODE_SDK_ADDONS_LIST.equals(name)) {
295                     NamedNodeMap attrs = child.getAttributes();
296                     String xmlns = "xmlns";                                         //$NON-NLS-1$
297                     if (prefix != null) {
298                         xmlns += ":" + prefix;                                      //$NON-NLS-1$
299                     }
300                     Node attr = attrs.getNamedItem(xmlns);
301                     if (attr != null) {
302                         String uri = attr.getNodeValue();
303                         if (uri != null) {
304                             Matcher m = nsPattern.matcher(uri);
305                             if (m.matches()) {
306                                 String version = m.group(1);
307                                 try {
308                                     return Integer.parseInt(version);
309                                 } catch (NumberFormatException e) {
310                                     return 0;
311                                 }
312                             }
313                         }
314                     }
315                 }
316             }
317         }
318 
319         return 0;
320     }
321 
322     /**
323      * Validates this XML against one of the requested SDK Repository schemas.
324      * If the XML was correctly validated, returns the schema that worked.
325      * If it doesn't validate, returns null and stores the error in outError[0].
326      * If we can't find a validator, returns null and set validatorFound[0] to false.
327      */
328     @VisibleForTesting(visibility=Visibility.PRIVATE)
validateXml(InputStream xml, String url, int version, String[] outError, Boolean[] validatorFound)329     protected String validateXml(InputStream xml, String url, int version,
330             String[] outError, Boolean[] validatorFound) {
331 
332         if (xml == null) {
333             return null;
334         }
335 
336         try {
337             Validator validator = getValidator(version);
338 
339             if (validator == null) {
340                 validatorFound[0] = Boolean.FALSE;
341                 outError[0] = String.format(
342                         "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.",
343                         url);
344                 return null;
345             }
346 
347             validatorFound[0] = Boolean.TRUE;
348 
349             // Reset the stream if it supports that operation.
350             xml.reset();
351 
352             // Validation throws a bunch of possible Exceptions on failure.
353             validator.validate(new StreamSource(xml));
354             return SdkAddonsListConstants.getSchemaUri(version);
355 
356         } catch (SAXParseException e) {
357             outError[0] = String.format(
358                     "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s",
359                     url,
360                     e.getLineNumber(),
361                     e.getColumnNumber(),
362                     e.toString());
363 
364         } catch (Exception e) {
365             outError[0] = String.format(
366                     "XML verification failed for %1$s.\nError: %2$s",
367                     url,
368                     e.toString());
369         }
370         return null;
371     }
372 
373     /**
374      * Helper method that returns a validator for our XSD, or null if the current Java
375      * implementation can't process XSD schemas.
376      *
377      * @param version The version of the XML Schema.
378      *        See {@link SdkAddonsListConstants#getXsdStream(int)}
379      */
getValidator(int version)380     private Validator getValidator(int version) throws SAXException {
381         InputStream xsdStream = SdkAddonsListConstants.getXsdStream(version);
382         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
383 
384         if (factory == null) {
385             return null;
386         }
387 
388         // This may throw a SAX Exception if the schema itself is not a valid XSD
389         Schema schema = factory.newSchema(new StreamSource(xsdStream));
390 
391         Validator validator = schema == null ? null : schema.newValidator();
392 
393         return validator;
394     }
395 
396     /**
397      * Takes an XML document as a string as parameter and returns a DOM for it.
398      *
399      * On error, returns null and prints a (hopefully) useful message on the monitor.
400      */
401     @VisibleForTesting(visibility=Visibility.PRIVATE)
getDocument(InputStream xml, ITaskMonitor monitor)402     protected Document getDocument(InputStream xml, ITaskMonitor monitor) {
403         try {
404             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
405             factory.setIgnoringComments(true);
406             factory.setNamespaceAware(true);
407 
408             DocumentBuilder builder = factory.newDocumentBuilder();
409             xml.reset();
410             Document doc = builder.parse(new InputSource(xml));
411 
412             return doc;
413         } catch (ParserConfigurationException e) {
414             monitor.logError("Failed to create XML document builder");
415 
416         } catch (SAXException e) {
417             monitor.logError("Failed to parse XML document");
418 
419         } catch (IOException e) {
420             monitor.logError("Failed to read XML document");
421         }
422 
423         return null;
424     }
425 
426     /**
427      * Parse all sites defined in the Addaons list XML and returns an array of sites.
428      */
429     @VisibleForTesting(visibility=Visibility.PRIVATE)
parseAddonsList(Document doc, String nsUri, ITaskMonitor monitor)430     protected Site[] parseAddonsList(Document doc, String nsUri, ITaskMonitor monitor) {
431 
432         Node root = getFirstChild(doc, nsUri, SdkAddonsListConstants.NODE_SDK_ADDONS_LIST);
433         if (root != null) {
434             ArrayList<Site> sites = new ArrayList<Site>();
435 
436             for (Node child = root.getFirstChild();
437                  child != null;
438                  child = child.getNextSibling()) {
439                 if (child.getNodeType() == Node.ELEMENT_NODE &&
440                         nsUri.equals(child.getNamespaceURI()) &&
441                         child.getLocalName().equals(SdkAddonsListConstants.NODE_ADDON_SITE)) {
442 
443                     Node url = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_URL);
444                     Node name = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_NAME);
445 
446                     if (name != null && url != null) {
447                         String strUrl  = url.getTextContent().trim();
448                         String strName = name.getTextContent().trim();
449 
450                         if (strUrl.length() > 0 && strName.length() > 0) {
451                             sites.add(new Site(strUrl, strName));
452                         }
453                     }
454                 }
455             }
456 
457             return sites.toArray(new Site[sites.size()]);
458         }
459 
460         return null;
461     }
462 
463     /**
464      * Returns the first child element with the given XML local name.
465      * If xmlLocalName is null, returns the very first child element.
466      */
getFirstChild(Node node, String nsUri, String xmlLocalName)467     private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
468 
469         for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
470             if (child.getNodeType() == Node.ELEMENT_NODE &&
471                     nsUri.equals(child.getNamespaceURI())) {
472                 if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
473                     return child;
474                 }
475             }
476         }
477 
478         return null;
479     }
480 
481 
482 }
483