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 66 private Site(String url, String uiName) { 67 mUrl = url.trim(); 68 mUiName = uiName; 69 } 70 71 public String getUrl() { 72 return mUrl; 73 } 74 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 */ 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 */ 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) 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) 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 */ 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) 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) 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 */ 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