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