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