Home | History | Annotate | Download | only in repository
      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