Home | History | Annotate | Download | only in repository
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdklib.internal.repository;
     18 
     19 import com.android.annotations.VisibleForTesting;
     20 import com.android.annotations.VisibleForTesting.Visibility;
     21 import com.android.sdklib.repository.SdkAddonsListConstants;
     22 
     23 import org.w3c.dom.Document;
     24 import org.w3c.dom.NamedNodeMap;
     25 import org.w3c.dom.Node;
     26 import org.xml.sax.InputSource;
     27 import org.xml.sax.SAXException;
     28 import org.xml.sax.SAXParseException;
     29 
     30 import java.io.ByteArrayInputStream;
     31 import java.io.FileNotFoundException;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.net.UnknownHostException;
     35 import java.util.ArrayList;
     36 import java.util.regex.Matcher;
     37 import java.util.regex.Pattern;
     38 
     39 import javax.net.ssl.SSLKeyException;
     40 import javax.xml.XMLConstants;
     41 import javax.xml.parsers.DocumentBuilder;
     42 import javax.xml.parsers.DocumentBuilderFactory;
     43 import javax.xml.parsers.ParserConfigurationException;
     44 import javax.xml.transform.stream.StreamSource;
     45 import javax.xml.validation.Schema;
     46 import javax.xml.validation.SchemaFactory;
     47 import javax.xml.validation.Validator;
     48 
     49 /**
     50  * Fetches and loads an sdk-addons-list XML.
     51  * <p/>
     52  * Such an XML contains a simple list of add-ons site that are to be loaded by default by the
     53  * SDK Manager. <br/>
     54  * The XML must conform to the sdk-addons-list-N.xsd. <br/>
     55  * Constants used in the XML are defined in {@link SdkAddonsListConstants}.
     56  */
     57 public class AddonsListFetcher {
     58 
     59     /**
     60      * An immutable structure representing an add-on site.
     61      */
     62     public static class Site {
     63         private final String mUrl;
     64         private final String mUiName;
     65 
     66         private Site(String url, String uiName) {
     67             mUrl = url.trim();
     68             mUiName = uiName;
     69         }
     70 
     71         public String getUrl() {
     72             return mUrl;
     73         }
     74 
     75         public String getUiName() {
     76             return mUiName;
     77         }
     78     }
     79 
     80     /**
     81      * Fetches the addons list from the given URL.
     82      *
     83      * @param monitor A monitor to report errors. Cannot be null.
     84      * @param url The URL of an XML file resource that conforms to the latest sdk-addons-list-N.xsd.
     85      *   For the default operation, use {@link SdkAddonsListConstants#URL_ADDON_LIST}.
     86      *   Cannot be null.
     87      * @return An array of {@link Site} on success (possibly empty), or null on error.
     88      */
     89     public Site[] fetch(ITaskMonitor monitor, String url) {
     90 
     91         url = url == null ? "" : url.trim();
     92 
     93         monitor.setProgressMax(5);
     94         monitor.setDescription("Fetching %1$s", url);
     95         monitor.incProgress(1);
     96 
     97         Exception[] exception = new Exception[] { null };
     98         Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
     99         String[] validationError = new String[] { null };
    100         Document validatedDoc = null;
    101         String validatedUri = null;
    102 
    103         ByteArrayInputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
    104 
    105         if (xml != null) {
    106             monitor.setDescription("Validate XML");
    107 
    108             // Explore the XML to find the potential XML schema version
    109             int version = getXmlSchemaVersion(xml);
    110 
    111             if (version >= 1 && version <= SdkAddonsListConstants.NS_LATEST_VERSION) {
    112                 // This should be a version we can handle. Try to validate it
    113                 // and report any error as invalid XML syntax,
    114 
    115                 String uri = validateXml(xml, url, version, validationError, validatorFound);
    116                 if (uri != null) {
    117                     // Validation was successful
    118                     validatedDoc = getDocument(xml, monitor);
    119                     validatedUri = uri;
    120 
    121                 }
    122             } else if (version > SdkAddonsListConstants.NS_LATEST_VERSION) {
    123                 // The schema used is more recent than what is supported by this tool.
    124                 // We don't have an upgrade-path support yet, so simply ignore the document.
    125                 return null;
    126             }
    127         }
    128 
    129         // If any exception was handled during the URL fetch, display it now.
    130         if (exception[0] != null) {
    131             String reason = null;
    132             if (exception[0] instanceof FileNotFoundException) {
    133                 // FNF has no useful getMessage, so we need to special handle it.
    134                 reason = "File not found";
    135             } else if (exception[0] instanceof UnknownHostException &&
    136                     exception[0].getMessage() != null) {
    137                 // This has no useful getMessage yet could really use one
    138                 reason = String.format("Unknown Host %1$s", exception[0].getMessage());
    139             } else if (exception[0] instanceof SSLKeyException) {
    140                 // That's a common error and we have a pref for it.
    141                 reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
    142             } else if (exception[0].getMessage() != null) {
    143                 reason = exception[0].getMessage();
    144             } else {
    145                 // We don't know what's wrong. Let's give the exception class at least.
    146                 reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
    147             }
    148 
    149             monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason);
    150         }
    151 
    152         if (validationError[0] != null) {
    153             monitor.logError("%s", validationError[0]);  //$NON-NLS-1$
    154         }
    155 
    156         // Stop here if we failed to validate the XML. We don't want to load it.
    157         if (validatedDoc == null) {
    158             return null;
    159         }
    160 
    161         monitor.incProgress(1);
    162 
    163         Site[] result = null;
    164 
    165         if (xml != null) {
    166             monitor.setDescription("Parse XML");
    167             monitor.incProgress(1);
    168             result = parseAddonsList(validatedDoc, validatedUri, monitor);
    169         }
    170 
    171         // done
    172         monitor.incProgress(1);
    173 
    174         return result;
    175     }
    176 
    177     /**
    178      * Fetches the document at the given URL and returns it as a stream. Returns
    179      * null if anything wrong happens. References: <br/>
    180      * URL Connection:
    181      *
    182      * @param urlString The URL to load, as a string.
    183      * @param monitor {@link ITaskMonitor} related to this URL.
    184      * @param outException If non null, where to store any exception that
    185      *            happens during the fetch.
    186      * @see UrlOpener UrlOpener, which handles all URL logic.
    187      */
    188     private ByteArrayInputStream fetchUrl(String urlString, ITaskMonitor monitor,
    189             Exception[] outException) {
    190         try {
    191 
    192             InputStream is = null;
    193 
    194             int inc = 65536;
    195             int curr = 0;
    196             byte[] result = new byte[inc];
    197 
    198             try {
    199                 is = UrlOpener.openUrl(urlString, monitor);
    200 
    201                 int n;
    202                 while ((n = is.read(result, curr, result.length - curr)) != -1) {
    203                     curr += n;
    204                     if (curr == result.length) {
    205                         byte[] temp = new byte[curr + inc];
    206                         System.arraycopy(result, 0, temp, 0, curr);
    207                         result = temp;
    208                     }
    209                 }
    210 
    211                 return new ByteArrayInputStream(result, 0, curr);
    212 
    213             } finally {
    214                 if (is != null) {
    215                     try {
    216                         is.close();
    217                     } catch (IOException e) {
    218                         // pass
    219                     }
    220                 }
    221             }
    222 
    223         } catch (Exception e) {
    224             if (outException != null) {
    225                 outException[0] = e;
    226             }
    227         }
    228 
    229         return null;
    230     }
    231 
    232     /**
    233      * Manually parses the root element of the XML to extract the schema version
    234      * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N"
    235      * declaration.
    236      *
    237      * @return 1..{@link SdkAddonsListConstants#NS_LATEST_VERSION} for a valid schema version
    238      *         or 0 if no schema could be found.
    239      */
    240     @VisibleForTesting(visibility=Visibility.PRIVATE)
    241     protected int getXmlSchemaVersion(InputStream xml) {
    242         if (xml == null) {
    243             return 0;
    244         }
    245 
    246         // Get an XML document
    247         Document doc = null;
    248         try {
    249             xml.reset();
    250 
    251             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    252             factory.setIgnoringComments(false);
    253             factory.setValidating(false);
    254 
    255             // Parse the old document using a non namespace aware builder
    256             factory.setNamespaceAware(false);
    257             DocumentBuilder builder = factory.newDocumentBuilder();
    258             doc = builder.parse(xml);
    259 
    260             // Prepare a new document using a namespace aware builder
    261             factory.setNamespaceAware(true);
    262             builder = factory.newDocumentBuilder();
    263 
    264         } catch (Exception e) {
    265             // Failed to reset XML stream
    266             // Failed to get builder factor
    267             // Failed to create XML document builder
    268             // Failed to parse XML document
    269             // Failed to read XML document
    270         }
    271 
    272         if (doc == null) {
    273             return 0;
    274         }
    275 
    276         // Check the root element is an XML with at least the following properties:
    277         // <sdk:sdk-addons-list
    278         //    xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N">
    279         //
    280         // Note that we don't have namespace support enabled, we just do it manually.
    281 
    282         Pattern nsPattern = Pattern.compile(SdkAddonsListConstants.NS_PATTERN);
    283 
    284         String prefix = null;
    285         for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
    286             if (child.getNodeType() == Node.ELEMENT_NODE) {
    287                 prefix = null;
    288                 String name = child.getNodeName();
    289                 int pos = name.indexOf(':');
    290                 if (pos > 0 && pos < name.length() - 1) {
    291                     prefix = name.substring(0, pos);
    292                     name = name.substring(pos + 1);
    293                 }
    294                 if (SdkAddonsListConstants.NODE_SDK_ADDONS_LIST.equals(name)) {
    295                     NamedNodeMap attrs = child.getAttributes();
    296                     String xmlns = "xmlns";                                         //$NON-NLS-1$
    297                     if (prefix != null) {
    298                         xmlns += ":" + prefix;                                      //$NON-NLS-1$
    299                     }
    300                     Node attr = attrs.getNamedItem(xmlns);
    301                     if (attr != null) {
    302                         String uri = attr.getNodeValue();
    303                         if (uri != null) {
    304                             Matcher m = nsPattern.matcher(uri);
    305                             if (m.matches()) {
    306                                 String version = m.group(1);
    307                                 try {
    308                                     return Integer.parseInt(version);
    309                                 } catch (NumberFormatException e) {
    310                                     return 0;
    311                                 }
    312                             }
    313                         }
    314                     }
    315                 }
    316             }
    317         }
    318 
    319         return 0;
    320     }
    321 
    322     /**
    323      * Validates this XML against one of the requested SDK Repository schemas.
    324      * If the XML was correctly validated, returns the schema that worked.
    325      * If it doesn't validate, returns null and stores the error in outError[0].
    326      * If we can't find a validator, returns null and set validatorFound[0] to false.
    327      */
    328     @VisibleForTesting(visibility=Visibility.PRIVATE)
    329     protected String validateXml(InputStream xml, String url, int version,
    330             String[] outError, Boolean[] validatorFound) {
    331 
    332         if (xml == null) {
    333             return null;
    334         }
    335 
    336         try {
    337             Validator validator = getValidator(version);
    338 
    339             if (validator == null) {
    340                 validatorFound[0] = Boolean.FALSE;
    341                 outError[0] = String.format(
    342                         "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.",
    343                         url);
    344                 return null;
    345             }
    346 
    347             validatorFound[0] = Boolean.TRUE;
    348 
    349             // Reset the stream if it supports that operation.
    350             xml.reset();
    351 
    352             // Validation throws a bunch of possible Exceptions on failure.
    353             validator.validate(new StreamSource(xml));
    354             return SdkAddonsListConstants.getSchemaUri(version);
    355 
    356         } catch (SAXParseException e) {
    357             outError[0] = String.format(
    358                     "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s",
    359                     url,
    360                     e.getLineNumber(),
    361                     e.getColumnNumber(),
    362                     e.toString());
    363 
    364         } catch (Exception e) {
    365             outError[0] = String.format(
    366                     "XML verification failed for %1$s.\nError: %2$s",
    367                     url,
    368                     e.toString());
    369         }
    370         return null;
    371     }
    372 
    373     /**
    374      * Helper method that returns a validator for our XSD, or null if the current Java
    375      * implementation can't process XSD schemas.
    376      *
    377      * @param version The version of the XML Schema.
    378      *        See {@link SdkAddonsListConstants#getXsdStream(int)}
    379      */
    380     private Validator getValidator(int version) throws SAXException {
    381         InputStream xsdStream = SdkAddonsListConstants.getXsdStream(version);
    382         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    383 
    384         if (factory == null) {
    385             return null;
    386         }
    387 
    388         // This may throw a SAX Exception if the schema itself is not a valid XSD
    389         Schema schema = factory.newSchema(new StreamSource(xsdStream));
    390 
    391         Validator validator = schema == null ? null : schema.newValidator();
    392 
    393         return validator;
    394     }
    395 
    396     /**
    397      * Takes an XML document as a string as parameter and returns a DOM for it.
    398      *
    399      * On error, returns null and prints a (hopefully) useful message on the monitor.
    400      */
    401     @VisibleForTesting(visibility=Visibility.PRIVATE)
    402     protected Document getDocument(InputStream xml, ITaskMonitor monitor) {
    403         try {
    404             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    405             factory.setIgnoringComments(true);
    406             factory.setNamespaceAware(true);
    407 
    408             DocumentBuilder builder = factory.newDocumentBuilder();
    409             xml.reset();
    410             Document doc = builder.parse(new InputSource(xml));
    411 
    412             return doc;
    413         } catch (ParserConfigurationException e) {
    414             monitor.logError("Failed to create XML document builder");
    415 
    416         } catch (SAXException e) {
    417             monitor.logError("Failed to parse XML document");
    418 
    419         } catch (IOException e) {
    420             monitor.logError("Failed to read XML document");
    421         }
    422 
    423         return null;
    424     }
    425 
    426     /**
    427      * Parse all sites defined in the Addaons list XML and returns an array of sites.
    428      */
    429     @VisibleForTesting(visibility=Visibility.PRIVATE)
    430     protected Site[] parseAddonsList(Document doc, String nsUri, ITaskMonitor monitor) {
    431 
    432         Node root = getFirstChild(doc, nsUri, SdkAddonsListConstants.NODE_SDK_ADDONS_LIST);
    433         if (root != null) {
    434             ArrayList<Site> sites = new ArrayList<Site>();
    435 
    436             for (Node child = root.getFirstChild();
    437                  child != null;
    438                  child = child.getNextSibling()) {
    439                 if (child.getNodeType() == Node.ELEMENT_NODE &&
    440                         nsUri.equals(child.getNamespaceURI()) &&
    441                         child.getLocalName().equals(SdkAddonsListConstants.NODE_ADDON_SITE)) {
    442 
    443                     Node url = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_URL);
    444                     Node name = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_NAME);
    445 
    446                     if (name != null && url != null) {
    447                         String strUrl  = url.getTextContent().trim();
    448                         String strName = name.getTextContent().trim();
    449 
    450                         if (strUrl.length() > 0 && strName.length() > 0) {
    451                             sites.add(new Site(strUrl, strName));
    452                         }
    453                     }
    454                 }
    455             }
    456 
    457             return sites.toArray(new Site[sites.size()]);
    458         }
    459 
    460         return null;
    461     }
    462 
    463     /**
    464      * Returns the first child element with the given XML local name.
    465      * If xmlLocalName is null, returns the very first child element.
    466      */
    467     private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
    468 
    469         for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
    470             if (child.getNodeType() == Node.ELEMENT_NODE &&
    471                     nsUri.equals(child.getNamespaceURI())) {
    472                 if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
    473                     return child;
    474                 }
    475             }
    476         }
    477 
    478         return null;
    479     }
    480 
    481 
    482 }
    483