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