1 /* 2 * Copyright (C) 2009 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.sources; 18 19 import com.android.annotations.Nullable; 20 import com.android.annotations.VisibleForTesting; 21 import com.android.annotations.VisibleForTesting.Visibility; 22 import com.android.sdklib.internal.repository.DownloadCache; 23 import com.android.sdklib.internal.repository.IDescription; 24 import com.android.sdklib.internal.repository.ITaskMonitor; 25 import com.android.sdklib.internal.repository.UrlOpener; 26 import com.android.sdklib.internal.repository.UrlOpener.CanceledByUserException; 27 import com.android.sdklib.internal.repository.packages.AddonPackage; 28 import com.android.sdklib.internal.repository.packages.DocPackage; 29 import com.android.sdklib.internal.repository.packages.ExtraPackage; 30 import com.android.sdklib.internal.repository.packages.Package; 31 import com.android.sdklib.internal.repository.packages.PlatformPackage; 32 import com.android.sdklib.internal.repository.packages.PlatformToolPackage; 33 import com.android.sdklib.internal.repository.packages.SamplePackage; 34 import com.android.sdklib.internal.repository.packages.SourcePackage; 35 import com.android.sdklib.internal.repository.packages.SystemImagePackage; 36 import com.android.sdklib.internal.repository.packages.ToolPackage; 37 import com.android.sdklib.repository.RepoConstants; 38 import com.android.sdklib.repository.SdkAddonConstants; 39 import com.android.sdklib.repository.SdkRepoConstants; 40 41 import org.w3c.dom.Document; 42 import org.w3c.dom.NamedNodeMap; 43 import org.w3c.dom.Node; 44 import org.xml.sax.ErrorHandler; 45 import org.xml.sax.InputSource; 46 import org.xml.sax.SAXException; 47 import org.xml.sax.SAXParseException; 48 49 import java.io.FileNotFoundException; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.net.MalformedURLException; 53 import java.net.URL; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.HashMap; 57 import java.util.regex.Matcher; 58 import java.util.regex.Pattern; 59 60 import javax.net.ssl.SSLKeyException; 61 import javax.xml.XMLConstants; 62 import javax.xml.parsers.DocumentBuilder; 63 import javax.xml.parsers.DocumentBuilderFactory; 64 import javax.xml.parsers.ParserConfigurationException; 65 import javax.xml.transform.stream.StreamSource; 66 import javax.xml.validation.Schema; 67 import javax.xml.validation.SchemaFactory; 68 import javax.xml.validation.Validator; 69 70 /** 71 * An sdk-addon or sdk-repository source, i.e. a download site. 72 * It may be a full repository or an add-on only repository. 73 * A repository describes one or {@link Package}s available for download. 74 */ 75 public abstract class SdkSource implements IDescription, Comparable<SdkSource> { 76 77 private String mUrl; 78 79 private Package[] mPackages; 80 private String mDescription; 81 private String mFetchError; 82 private final String mUiName; 83 84 private static final SdkSourceProperties sSourcesProps = new SdkSourceProperties(); 85 86 /** 87 * Constructs a new source for the given repository URL. 88 * @param url The source URL. Cannot be null. If the URL ends with a /, the default 89 * repository.xml filename will be appended automatically. 90 * @param uiName The UI-visible name of the source. Can be null. 91 */ SdkSource(String url, String uiName)92 public SdkSource(String url, String uiName) { 93 94 // URLs should not be null and should not have whitespace. 95 if (url == null) { 96 url = ""; 97 } 98 url = url.trim(); 99 100 // if the URL ends with a /, it must be "directory" resource, 101 // in which case we automatically add the default file that will 102 // looked for. This way it will be obvious to the user which 103 // resource we are actually trying to fetch. 104 if (url.endsWith("/")) { //$NON-NLS-1$ 105 String[] names = getDefaultXmlFileUrls(); 106 if (names.length > 0) { 107 url += names[0]; 108 } 109 } 110 111 if (uiName == null) { 112 uiName = sSourcesProps.getProperty(SdkSourceProperties.KEY_NAME, url, null); 113 } else { 114 sSourcesProps.setProperty(SdkSourceProperties.KEY_NAME, url, uiName); 115 } 116 117 mUrl = url; 118 mUiName = uiName; 119 setDefaultDescription(); 120 } 121 122 /** 123 * Returns true if this is an addon source. 124 * We only load addons and extras from these sources. 125 */ isAddonSource()126 public abstract boolean isAddonSource(); 127 128 /** 129 * Returns the basename of the default URLs to try to download the 130 * XML manifest. 131 * E.g. this is typically SdkRepoConstants.URL_DEFAULT_XML_FILE 132 * or SdkAddonConstants.URL_DEFAULT_XML_FILE 133 */ getDefaultXmlFileUrls()134 protected abstract String[] getDefaultXmlFileUrls(); 135 136 /** Returns SdkRepoConstants.NS_LATEST_VERSION or SdkAddonConstants.NS_LATEST_VERSION. */ getNsLatestVersion()137 protected abstract int getNsLatestVersion(); 138 139 /** Returns SdkRepoConstants.NS_URI or SdkAddonConstants.NS_URI. */ getNsUri()140 protected abstract String getNsUri(); 141 142 /** Returns SdkRepoConstants.NS_PATTERN or SdkAddonConstants.NS_PATTERN. */ getNsPattern()143 protected abstract String getNsPattern(); 144 145 /** Returns SdkRepoConstants.getSchemaUri() or SdkAddonConstants.getSchemaUri(). */ getSchemaUri(int version)146 protected abstract String getSchemaUri(int version); 147 148 /* Returns SdkRepoConstants.NODE_SDK_REPOSITORY or SdkAddonConstants.NODE_SDK_ADDON. */ getRootElementName()149 protected abstract String getRootElementName(); 150 151 /** Returns SdkRepoConstants.getXsdStream() or SdkAddonConstants.getXsdStream(). */ getXsdStream(int version)152 protected abstract InputStream getXsdStream(int version); 153 154 /** 155 * In case we fail to load an XML, examine the XML to see if it matches a <b>future</b> 156 * schema that as at least a <code>tools</code> node that we could load to update the 157 * SDK Manager. 158 * 159 * @param xml The input XML stream. Can be null. 160 * @return Null on failure, otherwise returns an XML DOM with just the tools we 161 * need to update this SDK Manager. 162 * @null Can return null on failure. 163 */ findAlternateToolsXml(@ullable InputStream xml)164 protected abstract Document findAlternateToolsXml(@Nullable InputStream xml) 165 throws IOException; 166 167 /** 168 * Two repo source are equal if they have the same URL. 169 */ 170 @Override equals(Object obj)171 public boolean equals(Object obj) { 172 if (obj instanceof SdkSource) { 173 SdkSource rs = (SdkSource) obj; 174 return rs.getUrl().equals(this.getUrl()); 175 } 176 return false; 177 } 178 179 @Override hashCode()180 public int hashCode() { 181 return mUrl.hashCode(); 182 } 183 184 /** 185 * Implementation of the {@link Comparable} interface. 186 * Simply compares the URL using the string's default ordering. 187 */ 188 @Override compareTo(SdkSource rhs)189 public int compareTo(SdkSource rhs) { 190 return this.getUrl().compareTo(rhs.getUrl()); 191 } 192 193 /** 194 * Returns the UI-visible name of the source. Can be null. 195 */ getUiName()196 public String getUiName() { 197 return mUiName; 198 } 199 200 /** Returns the URL of the XML file for this source. */ getUrl()201 public String getUrl() { 202 return mUrl; 203 } 204 205 /** 206 * Returns the list of known packages found by the last call to load(). 207 * This is null when the source hasn't been loaded yet -- caller should 208 * then call {@link #load} to load the packages. 209 */ getPackages()210 public Package[] getPackages() { 211 return mPackages; 212 } 213 214 @VisibleForTesting(visibility=Visibility.PRIVATE) setPackages(Package[] packages)215 protected void setPackages(Package[] packages) { 216 mPackages = packages; 217 218 if (mPackages != null) { 219 // Order the packages. 220 Arrays.sort(mPackages, null); 221 } 222 } 223 224 /** 225 * Clear the internal packages list. After this call, {@link #getPackages()} will return 226 * null till load() is called. 227 */ clearPackages()228 public void clearPackages() { 229 setPackages(null); 230 } 231 232 /** 233 * Indicates if the source is enabled. 234 * <p/> 235 * A 3rd-party add-on source can be disabled by the user to prevent from loading it. 236 * 237 * @return True if the source is enabled (default is true). 238 */ isEnabled()239 public boolean isEnabled() { 240 // A URL is enabled if it's not in the disabled list. 241 return sSourcesProps.getProperty(SdkSourceProperties.KEY_DISABLED, mUrl, null) == null; 242 } 243 244 /** 245 * Changes whether the source is marked as enabled. 246 * <p/> 247 * When <em>changing</em> the enable state, the current package list is purged 248 * and the next {@code load} will either return an empty list (if disabled) or 249 * the actual package list (if enabled.) 250 * 251 * @param enabled True for the source to be enabled (can be loaded), false otherwise. 252 */ setEnabled(boolean enabled)253 public void setEnabled(boolean enabled) { 254 if (enabled != isEnabled()) { 255 // First we clear the current package list, which will force the 256 // next load() to actually set the package list as desired. 257 clearPackages(); 258 259 sSourcesProps.setProperty(SdkSourceProperties.KEY_DISABLED, mUrl, 260 enabled ? null /*remove*/ : "disabled"); //$NON-NLS-1$ 261 } 262 } 263 264 /** 265 * Returns the short description of the source, if not null. 266 * Otherwise returns the default Object toString result. 267 * <p/> 268 * This is mostly helpful for debugging. 269 * For UI display, use the {@link IDescription} interface. 270 */ 271 @Override toString()272 public String toString() { 273 String s = getShortDescription(); 274 if (s != null) { 275 return s; 276 } 277 return super.toString(); 278 } 279 280 @Override getShortDescription()281 public String getShortDescription() { 282 283 if (mUiName != null && mUiName.length() > 0) { 284 285 String host = "malformed URL"; 286 287 try { 288 URL u = new URL(mUrl); 289 host = u.getHost(); 290 } catch (MalformedURLException e) { 291 } 292 293 return String.format("%1$s (%2$s)", mUiName, host); 294 295 } 296 return mUrl; 297 } 298 299 @Override getLongDescription()300 public String getLongDescription() { 301 // Note: in a normal workflow, mDescription is filled by setDefaultDescription(). 302 // However for packages made by unit tests or such, this can be null. 303 return mDescription == null ? "" : mDescription; //$NON-NLS-1$ 304 } 305 306 /** 307 * Returns the last fetch error description. 308 * If there was no error, returns null. 309 */ getFetchError()310 public String getFetchError() { 311 return mFetchError; 312 } 313 314 /** 315 * Tries to fetch the repository index for the given URL and updates the package list. 316 * When a source is disabled, this create an empty non-null package list. 317 * <p/> 318 * Callers can get the package list using {@link #getPackages()} after this. It will be 319 * null in case of error, in which case {@link #getFetchError()} can be used to an 320 * error message. 321 */ load(DownloadCache cache, ITaskMonitor monitor, boolean forceHttp)322 public void load(DownloadCache cache, ITaskMonitor monitor, boolean forceHttp) { 323 324 setDefaultDescription(); 325 monitor.setProgressMax(7); 326 327 if (!isEnabled()) { 328 setPackages(new Package[0]); 329 mDescription += "\nSource is disabled."; 330 monitor.incProgress(7); 331 return; 332 } 333 334 String url = mUrl; 335 if (forceHttp) { 336 url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ 337 } 338 339 monitor.setDescription("Fetching URL: %1$s", url); 340 monitor.incProgress(1); 341 342 mFetchError = null; 343 Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; 344 String[] validationError = new String[] { null }; 345 Exception[] exception = new Exception[] { null }; 346 Document validatedDoc = null; 347 boolean usingAlternateXml = false; 348 boolean usingAlternateUrl = false; 349 String validatedUri = null; 350 351 String[] defaultNames = getDefaultXmlFileUrls(); 352 String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : ""; 353 354 InputStream xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception); 355 if (xml != null) { 356 int version = getXmlSchemaVersion(xml); 357 if (version == 0) { 358 xml = null; 359 } 360 } 361 362 // FIXME: this is a quick fix to support an alternate upgrade path. 363 // The whole logic below needs to be updated. 364 if (xml == null && defaultNames.length > 0) { 365 ITaskMonitor subMonitor = monitor.createSubMonitor(1); 366 subMonitor.setProgressMax(defaultNames.length); 367 368 String baseUrl = url; 369 if (!baseUrl.endsWith("/")) { 370 int pos = baseUrl.lastIndexOf('/'); 371 if (pos > 0) { 372 baseUrl = baseUrl.substring(0, pos + 1); 373 } 374 } 375 376 for(String name : defaultNames) { 377 String newUrl = baseUrl + name; 378 if (newUrl.equals(url)) { 379 continue; 380 } 381 xml = fetchUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception); 382 if (xml != null) { 383 int version = getXmlSchemaVersion(xml); 384 if (version == 0) { 385 xml = null; 386 } else { 387 url = newUrl; 388 subMonitor.incProgress( 389 subMonitor.getProgressMax() - subMonitor.getProgress()); 390 break; 391 } 392 } 393 } 394 } else { 395 monitor.incProgress(1); 396 } 397 398 // If the original URL can't be fetched 399 // and the URL doesn't explicitly end with our filename 400 // and it wasn't an HTTP authentication operation canceled by the user 401 // then make another tentative after changing the URL. 402 if (xml == null 403 && !url.endsWith(firstDefaultName) 404 && !(exception[0] instanceof CanceledByUserException)) { 405 if (!url.endsWith("/")) { //$NON-NLS-1$ 406 url += "/"; //$NON-NLS-1$ 407 } 408 url += firstDefaultName; 409 410 xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception); 411 usingAlternateUrl = true; 412 } else { 413 monitor.incProgress(1); 414 } 415 416 // FIXME this needs to revisited. 417 if (xml != null) { 418 monitor.setDescription("Validate XML: %1$s", url); 419 420 ITaskMonitor subMonitor = monitor.createSubMonitor(2); 421 subMonitor.setProgressMax(2); 422 for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) { 423 // Explore the XML to find the potential XML schema version 424 int version = getXmlSchemaVersion(xml); 425 426 if (version >= 1 && version <= getNsLatestVersion()) { 427 // This should be a version we can handle. Try to validate it 428 // and report any error as invalid XML syntax, 429 430 String uri = validateXml(xml, url, version, validationError, validatorFound); 431 if (uri != null) { 432 // Validation was successful 433 validatedDoc = getDocument(xml, monitor); 434 validatedUri = uri; 435 436 if (usingAlternateUrl && validatedDoc != null) { 437 // If the second tentative succeeded, indicate it in the console 438 // with the URL that worked. 439 monitor.log("Repository found at %1$s", url); 440 441 // Keep the modified URL 442 mUrl = url; 443 } 444 } else if (validatorFound[0].equals(Boolean.FALSE)) { 445 // Validation failed because this JVM lacks a proper XML Validator 446 mFetchError = validationError[0]; 447 } else { 448 // We got a validator but validation failed. We know there's 449 // what looks like a suitable root element with a suitable XMLNS 450 // so it must be a genuine error of an XML not conforming to the schema. 451 } 452 } else if (version > getNsLatestVersion()) { 453 // The schema used is more recent than what is supported by this tool. 454 // Tell the user to upgrade, pointing him to the right version of the tool 455 // package. 456 457 try { 458 validatedDoc = findAlternateToolsXml(xml); 459 } catch (IOException e) { 460 // Failed, will be handled below. 461 } 462 if (validatedDoc != null) { 463 validationError[0] = null; // remove error from XML validation 464 validatedUri = getNsUri(); 465 usingAlternateXml = true; 466 } 467 468 } else if (version < 1 && tryOtherUrl == 0 && !usingAlternateUrl) { 469 // This is obviously not one of our documents. 470 mFetchError = String.format( 471 "Failed to validate the XML for the repository at URL '%1$s'", 472 url); 473 474 // If we haven't already tried the alternate URL, let's do it now. 475 // We don't capture any fetch exception that happen during the second 476 // fetch in order to avoid hidding any previous fetch errors. 477 if (!url.endsWith(firstDefaultName)) { 478 if (!url.endsWith("/")) { //$NON-NLS-1$ 479 url += "/"; //$NON-NLS-1$ 480 } 481 url += firstDefaultName; 482 483 xml = fetchUrl(url, cache, subMonitor.createSubMonitor(1), 484 null /* outException */); 485 subMonitor.incProgress(1); 486 // Loop to try the alternative document 487 if (xml != null) { 488 usingAlternateUrl = true; 489 continue; 490 } 491 } 492 } else if (version < 1 && usingAlternateUrl && mFetchError == null) { 493 // The alternate URL is obviously not a valid XML either. 494 // We only report the error if we failed to produce one earlier. 495 mFetchError = String.format( 496 "Failed to validate the XML for the repository at URL '%1$s'", 497 url); 498 } 499 500 // If we get here either we succeeded or we ran out of alternatives. 501 break; 502 } 503 } 504 505 // If any exception was handled during the URL fetch, display it now. 506 if (exception[0] != null) { 507 mFetchError = "Failed to fetch URL"; 508 509 String reason = null; 510 if (exception[0] instanceof FileNotFoundException) { 511 // FNF has no useful getMessage, so we need to special handle it. 512 reason = "File not found"; 513 mFetchError += ": " + reason; 514 } else if (exception[0] instanceof SSLKeyException) { 515 // That's a common error and we have a pref for it. 516 reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; 517 mFetchError += ": HTTPS SSL error"; 518 } else if (exception[0].getMessage() != null) { 519 reason = exception[0].getMessage(); 520 } else { 521 // We don't know what's wrong. Let's give the exception class at least. 522 reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); 523 } 524 525 monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); 526 } 527 528 if (validationError[0] != null) { 529 monitor.logError("%s", validationError[0]); //$NON-NLS-1$ 530 } 531 532 // Stop here if we failed to validate the XML. We don't want to load it. 533 if (validatedDoc == null) { 534 return; 535 } 536 537 if (usingAlternateXml) { 538 // We found something using the "alternate" XML schema (that is the one made up 539 // to support schema upgrades). That means the user can only install the tools 540 // and needs to upgrade them before it download more stuff. 541 542 // Is the manager running from inside ADT? 543 // We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection. 544 545 boolean isADT = false; 546 try { 547 Class<?> adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin"); //$NON-NLS-1$ 548 isADT = (adt != null); 549 } catch (ClassNotFoundException e) { 550 // pass 551 } 552 553 String info; 554 if (isADT) { 555 info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin."; 556 mDescription = "This repository requires a more recent version of ADT, the Eclipse Android plugin.\nYou must update it before you can see other new packages."; 557 558 } else { 559 info = "This repository requires a more recent version of the Tools. Please update."; 560 mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages."; 561 } 562 563 mFetchError = mFetchError == null ? info : mFetchError + ". " + info; 564 } 565 566 monitor.incProgress(1); 567 568 if (xml != null) { 569 monitor.setDescription("Parse XML: %1$s", url); 570 monitor.incProgress(1); 571 parsePackages(validatedDoc, validatedUri, monitor); 572 if (mPackages == null || mPackages.length == 0) { 573 mDescription += "\nNo packages found."; 574 } else if (mPackages.length == 1) { 575 mDescription += "\nOne package found."; 576 } else { 577 mDescription += String.format("\n%1$d packages found.", mPackages.length); 578 } 579 } 580 581 // done 582 monitor.incProgress(1); 583 } 584 setDefaultDescription()585 private void setDefaultDescription() { 586 if (isAddonSource()) { 587 String desc = ""; 588 589 if (mUiName != null) { 590 desc += "Add-on Provider: " + mUiName; 591 desc += "\n"; 592 } 593 desc += "Add-on URL: " + mUrl; 594 595 mDescription = desc; 596 } else { 597 mDescription = String.format("SDK Source: %1$s", mUrl); 598 } 599 } 600 601 /** 602 * Fetches the document at the given URL and returns it as a string. Returns 603 * null if anything wrong happens and write errors to the monitor. 604 * References: <br/> 605 * URL Connection: 606 * 607 * @param urlString The URL to load, as a string. 608 * @param monitor {@link ITaskMonitor} related to this URL. 609 * @param outException If non null, where to store any exception that 610 * happens during the fetch. 611 * @see UrlOpener UrlOpener, which handles all URL logic. 612 */ fetchUrl(String urlString, DownloadCache cache, ITaskMonitor monitor, Exception[] outException)613 private InputStream fetchUrl(String urlString, 614 DownloadCache cache, 615 ITaskMonitor monitor, 616 Exception[] outException) { 617 try { 618 return cache.openCachedUrl(urlString, monitor); 619 } catch (Exception e) { 620 if (outException != null) { 621 outException[0] = e; 622 } 623 } 624 625 return null; 626 } 627 628 /** 629 * Validates this XML against one of the requested SDK Repository schemas. 630 * If the XML was correctly validated, returns the schema that worked. 631 * If it doesn't validate, returns null and stores the error in outError[0]. 632 * If we can't find a validator, returns null and set validatorFound[0] to false. 633 */ 634 @VisibleForTesting(visibility=Visibility.PRIVATE) validateXml(InputStream xml, String url, int version, String[] outError, Boolean[] validatorFound)635 protected String validateXml(InputStream xml, String url, int version, 636 String[] outError, Boolean[] validatorFound) { 637 638 if (xml == null) { 639 return null; 640 } 641 642 try { 643 Validator validator = getValidator(version); 644 645 if (validator == null) { 646 validatorFound[0] = Boolean.FALSE; 647 outError[0] = String.format( 648 "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.", 649 url); 650 return null; 651 } 652 653 validatorFound[0] = Boolean.TRUE; 654 655 // Reset the stream if it supports that operation. 656 xml.reset(); 657 658 // Validation throws a bunch of possible Exceptions on failure. 659 validator.validate(new StreamSource(xml)); 660 return getSchemaUri(version); 661 662 } catch (SAXParseException e) { 663 outError[0] = String.format( 664 "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", 665 url, 666 e.getLineNumber(), 667 e.getColumnNumber(), 668 e.toString()); 669 670 } catch (Exception e) { 671 outError[0] = String.format( 672 "XML verification failed for %1$s.\nError: %2$s", 673 url, 674 e.toString()); 675 } 676 return null; 677 } 678 679 /** 680 * Manually parses the root element of the XML to extract the schema version 681 * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N" 682 * declaration. 683 * 684 * @return 1..{@link SdkRepoConstants#NS_LATEST_VERSION} for a valid schema version 685 * or 0 if no schema could be found. 686 */ 687 @VisibleForTesting(visibility=Visibility.PRIVATE) getXmlSchemaVersion(InputStream xml)688 protected int getXmlSchemaVersion(InputStream xml) { 689 if (xml == null) { 690 return 0; 691 } 692 693 // Get an XML document 694 Document doc = null; 695 try { 696 xml.reset(); 697 698 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 699 factory.setIgnoringComments(false); 700 factory.setValidating(false); 701 702 // Parse the old document using a non namespace aware builder 703 factory.setNamespaceAware(false); 704 DocumentBuilder builder = factory.newDocumentBuilder(); 705 706 // We don't want the default handler which prints errors to stderr. 707 builder.setErrorHandler(new ErrorHandler() { 708 @Override 709 public void warning(SAXParseException e) throws SAXException { 710 // pass 711 } 712 @Override 713 public void fatalError(SAXParseException e) throws SAXException { 714 throw e; 715 } 716 @Override 717 public void error(SAXParseException e) throws SAXException { 718 throw e; 719 } 720 }); 721 722 doc = builder.parse(xml); 723 724 // Prepare a new document using a namespace aware builder 725 factory.setNamespaceAware(true); 726 builder = factory.newDocumentBuilder(); 727 728 } catch (Exception e) { 729 // Failed to reset XML stream 730 // Failed to get builder factor 731 // Failed to create XML document builder 732 // Failed to parse XML document 733 // Failed to read XML document 734 } 735 736 if (doc == null) { 737 return 0; 738 } 739 740 // Check the root element is an XML with at least the following properties: 741 // <sdk:sdk-repository 742 // xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N"> 743 // 744 // Note that we don't have namespace support enabled, we just do it manually. 745 746 Pattern nsPattern = Pattern.compile(getNsPattern()); 747 748 String prefix = null; 749 for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { 750 if (child.getNodeType() == Node.ELEMENT_NODE) { 751 prefix = null; 752 String name = child.getNodeName(); 753 int pos = name.indexOf(':'); 754 if (pos > 0 && pos < name.length() - 1) { 755 prefix = name.substring(0, pos); 756 name = name.substring(pos + 1); 757 } 758 if (getRootElementName().equals(name)) { 759 NamedNodeMap attrs = child.getAttributes(); 760 String xmlns = "xmlns"; //$NON-NLS-1$ 761 if (prefix != null) { 762 xmlns += ":" + prefix; //$NON-NLS-1$ 763 } 764 Node attr = attrs.getNamedItem(xmlns); 765 if (attr != null) { 766 String uri = attr.getNodeValue(); 767 if (uri != null) { 768 Matcher m = nsPattern.matcher(uri); 769 if (m.matches()) { 770 String version = m.group(1); 771 try { 772 return Integer.parseInt(version); 773 } catch (NumberFormatException e) { 774 return 0; 775 } 776 } 777 } 778 } 779 } 780 } 781 } 782 783 return 0; 784 } 785 786 /** 787 * Helper method that returns a validator for our XSD, or null if the current Java 788 * implementation can't process XSD schemas. 789 * 790 * @param version The version of the XML Schema. 791 * See {@link SdkRepoConstants#getXsdStream(int)} 792 */ getValidator(int version)793 private Validator getValidator(int version) throws SAXException { 794 InputStream xsdStream = getXsdStream(version); 795 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 796 797 if (factory == null) { 798 return null; 799 } 800 801 // This may throw a SAX Exception if the schema itself is not a valid XSD 802 Schema schema = factory.newSchema(new StreamSource(xsdStream)); 803 804 Validator validator = schema == null ? null : schema.newValidator(); 805 806 // We don't want the default handler, which by default dumps errors to stderr. 807 validator.setErrorHandler(new ErrorHandler() { 808 @Override 809 public void warning(SAXParseException e) throws SAXException { 810 // pass 811 } 812 @Override 813 public void fatalError(SAXParseException e) throws SAXException { 814 throw e; 815 } 816 @Override 817 public void error(SAXParseException e) throws SAXException { 818 throw e; 819 } 820 }); 821 822 return validator; 823 } 824 825 /** 826 * Parse all packages defined in the SDK Repository XML and creates 827 * a new mPackages array with them. 828 */ 829 @VisibleForTesting(visibility=Visibility.PRIVATE) parsePackages(Document doc, String nsUri, ITaskMonitor monitor)830 protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) { 831 832 Node root = getFirstChild(doc, nsUri, getRootElementName()); 833 if (root != null) { 834 835 ArrayList<Package> packages = new ArrayList<Package>(); 836 837 // Parse license definitions 838 HashMap<String, String> licenses = new HashMap<String, String>(); 839 for (Node child = root.getFirstChild(); 840 child != null; 841 child = child.getNextSibling()) { 842 if (child.getNodeType() == Node.ELEMENT_NODE && 843 nsUri.equals(child.getNamespaceURI()) && 844 child.getLocalName().equals(RepoConstants.NODE_LICENSE)) { 845 Node id = child.getAttributes().getNamedItem(RepoConstants.ATTR_ID); 846 if (id != null) { 847 licenses.put(id.getNodeValue(), child.getTextContent()); 848 } 849 } 850 } 851 852 // Parse packages 853 for (Node child = root.getFirstChild(); 854 child != null; 855 child = child.getNextSibling()) { 856 if (child.getNodeType() == Node.ELEMENT_NODE && 857 nsUri.equals(child.getNamespaceURI())) { 858 String name = child.getLocalName(); 859 Package p = null; 860 861 try { 862 // We can load addon and extra packages from all sources, either 863 // internal or user sources. 864 if (SdkAddonConstants.NODE_ADD_ON.equals(name)) { 865 p = new AddonPackage(this, child, nsUri, licenses); 866 867 } else if (SdkAddonConstants.NODE_EXTRA.equals(name)) { 868 p = new ExtraPackage(this, child, nsUri, licenses); 869 870 } else if (!isAddonSource()) { 871 // We only load platform, doc and tool packages from internal 872 // sources, never from user sources. 873 if (SdkRepoConstants.NODE_PLATFORM.equals(name)) { 874 p = new PlatformPackage(this, child, nsUri, licenses); 875 } else if (SdkRepoConstants.NODE_DOC.equals(name)) { 876 p = new DocPackage(this, child, nsUri, licenses); 877 } else if (SdkRepoConstants.NODE_TOOL.equals(name)) { 878 p = new ToolPackage(this, child, nsUri, licenses); 879 } else if (SdkRepoConstants.NODE_PLATFORM_TOOL.equals(name)) { 880 p = new PlatformToolPackage(this, child, nsUri, licenses); 881 } else if (SdkRepoConstants.NODE_SAMPLE.equals(name)) { 882 p = new SamplePackage(this, child, nsUri, licenses); 883 } else if (SdkRepoConstants.NODE_SYSTEM_IMAGE.equals(name)) { 884 p = new SystemImagePackage(this, child, nsUri, licenses); 885 } else if (SdkRepoConstants.NODE_SOURCE.equals(name)) { 886 p = new SourcePackage(this, child, nsUri, licenses); 887 } 888 } 889 890 if (p != null) { 891 packages.add(p); 892 monitor.logVerbose("Found %1$s", p.getShortDescription()); 893 } 894 } catch (Exception e) { 895 // Ignore invalid packages 896 monitor.logError("Ignoring invalid %1$s element: %2$s", name, e.toString()); 897 } 898 } 899 } 900 901 setPackages(packages.toArray(new Package[packages.size()])); 902 903 return true; 904 } 905 906 return false; 907 } 908 909 /** 910 * Returns the first child element with the given XML local name. 911 * If xmlLocalName is null, returns the very first child element. 912 */ getFirstChild(Node node, String nsUri, String xmlLocalName)913 private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { 914 915 for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { 916 if (child.getNodeType() == Node.ELEMENT_NODE && 917 nsUri.equals(child.getNamespaceURI())) { 918 if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { 919 return child; 920 } 921 } 922 } 923 924 return null; 925 } 926 927 /** 928 * Takes an XML document as a string as parameter and returns a DOM for it. 929 * 930 * On error, returns null and prints a (hopefully) useful message on the monitor. 931 */ 932 @VisibleForTesting(visibility=Visibility.PRIVATE) getDocument(InputStream xml, ITaskMonitor monitor)933 protected Document getDocument(InputStream xml, ITaskMonitor monitor) { 934 try { 935 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 936 factory.setIgnoringComments(true); 937 factory.setNamespaceAware(true); 938 939 DocumentBuilder builder = factory.newDocumentBuilder(); 940 xml.reset(); 941 Document doc = builder.parse(new InputSource(xml)); 942 943 return doc; 944 } catch (ParserConfigurationException e) { 945 monitor.logError("Failed to create XML document builder"); 946 947 } catch (SAXException e) { 948 monitor.logError("Failed to parse XML document"); 949 950 } catch (IOException e) { 951 monitor.logError("Failed to read XML document"); 952 } 953 954 return null; 955 } 956 } 957