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.sdklib.internal.repository.Archive.Arch;
     21 import com.android.sdklib.internal.repository.Archive.Os;
     22 import com.android.sdklib.repository.RepoConstants;
     23 import com.android.sdklib.repository.SdkRepoConstants;
     24 
     25 import org.w3c.dom.Attr;
     26 import org.w3c.dom.Document;
     27 import org.w3c.dom.Element;
     28 import org.w3c.dom.NamedNodeMap;
     29 import org.w3c.dom.Node;
     30 import org.w3c.dom.Text;
     31 import org.xml.sax.ErrorHandler;
     32 
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 import java.util.regex.Pattern;
     36 
     37 import javax.xml.parsers.DocumentBuilder;
     38 import javax.xml.parsers.DocumentBuilderFactory;
     39 
     40 
     41 /**
     42  * An sdk-repository source, i.e. a download site.
     43  * A repository describes one or more {@link Package}s available for download.
     44  */
     45 public class SdkRepoSource extends SdkSource {
     46 
     47     /**
     48      * Constructs a new source for the given repository URL.
     49      * @param url The source URL. Cannot be null. If the URL ends with a /, the default
     50      *            repository.xml filename will be appended automatically.
     51      * @param uiName The UI-visible name of the source. Can be null.
     52      */
     53     public SdkRepoSource(String url, String uiName) {
     54         super(url, uiName);
     55     }
     56 
     57     /**
     58      * Returns true if this is an addon source.
     59      * We only load addons and extras from these sources.
     60      */
     61     @Override
     62     public boolean isAddonSource() {
     63         return false;
     64     }
     65 
     66     @Override
     67     protected String[] getDefaultXmlFileUrls() {
     68         return new String[] {
     69                 SdkRepoConstants.URL_DEFAULT_FILENAME2,
     70                 SdkRepoConstants.URL_DEFAULT_FILENAME
     71         };
     72     }
     73 
     74     @Override
     75     protected int getNsLatestVersion() {
     76         return SdkRepoConstants.NS_LATEST_VERSION;
     77     }
     78 
     79     @Override
     80     protected String getNsUri() {
     81         return SdkRepoConstants.NS_URI;
     82     }
     83 
     84     @Override
     85     protected String getNsPattern() {
     86         return SdkRepoConstants.NS_PATTERN;
     87     }
     88 
     89     @Override
     90     protected String getSchemaUri(int version) {
     91         return SdkRepoConstants.getSchemaUri(version);
     92     }
     93 
     94     @Override
     95     protected String getRootElementName() {
     96         return SdkRepoConstants.NODE_SDK_REPOSITORY;
     97     }
     98 
     99     @Override
    100     protected InputStream getXsdStream(int version) {
    101         return SdkRepoConstants.getXsdStream(version);
    102     }
    103 
    104     /**
    105      * The purpose of this method is to support forward evolution of our schema.
    106      * <p/>
    107      * At this point, we know that xml does not point to any schema that this version of
    108      * the tool knows how to process, so it's not one of the possible 1..N versions of our
    109      * XSD schema.
    110      * <p/>
    111      * We thus try to interpret the byte stream as a possible XML stream. It may not be
    112      * one at all in the first place. If it looks anything line an XML schema, we try to
    113      * find its &lt;tool&gt; and the &lt;platform-tools&gt; elements. If we find any,
    114      * we recreate a suitable document that conforms to what we expect from our XSD schema
    115      * with only those elements.
    116      * <p/>
    117      * To be valid, the &lt;tool&gt; and the &lt;platform-tools&gt; elements must have at
    118      * least one &lt;archive&gt; compatible with this platform.
    119      * <p/>
    120      * Starting the sdk-repository schema v3, &lt;tools&gt; has a &lt;min-platform-tools-rev&gt;
    121      * node, so technically the corresponding XML schema will be usable only if there's a
    122      * &lt;platform-tools&gt; with the request revision number. We don't enforce that here, as
    123      * this is done at install time.
    124      * <p/>
    125      * If we don't find anything suitable, we drop the whole thing.
    126      *
    127      * @param xml The input XML stream. Can be null.
    128      * @return Either a new XML document conforming to our schema with at least one &lt;tool&gt;
    129      *         and &lt;platform-tools&gt; element or null.
    130      * @throws IOException if InputStream.reset() fails
    131      * @null Can return null on failure.
    132      */
    133     @Override
    134     protected Document findAlternateToolsXml(@Nullable InputStream xml) throws IOException {
    135         return findAlternateToolsXml(xml, null /*errorHandler*/);
    136     }
    137 
    138     /**
    139      * An alternate version of {@link #findAlternateToolsXml(InputStream)} that allows
    140      * the caller to specify the XML error handler. The default from the underlying Java
    141      * XML Xerces parser will dump to stdout/stderr, which is not convenient during unit tests.
    142      *
    143      * @param xml The input XML stream. Can be null.
    144      * @param errorHandler An optional XML error handler. If null, the default will be used.
    145      * @return Either a new XML document conforming to our schema with at least one &lt;tool&gt;
    146      *         and &lt;platform-tools&gt; element or null.
    147      * @throws IOException if InputStream.reset() fails
    148      * @null Can return null on failure.
    149      * @see #findAlternateToolsXml(InputStream) findAlternateToolsXml() provides more details.
    150      */
    151     protected Document findAlternateToolsXml(
    152             @Nullable InputStream xml,
    153             @Nullable ErrorHandler errorHandler)
    154                 throws IOException {
    155         if (xml == null) {
    156             return null;
    157         }
    158 
    159         // Reset the stream if it supports that operation.
    160         xml.reset();
    161 
    162         // Get an XML document
    163 
    164         Document oldDoc = null;
    165         Document newDoc = null;
    166         try {
    167             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    168             factory.setIgnoringComments(false);
    169             factory.setValidating(false);
    170 
    171             // Parse the old document using a non namespace aware builder
    172             factory.setNamespaceAware(false);
    173             DocumentBuilder builder = factory.newDocumentBuilder();
    174 
    175             if (errorHandler != null) {
    176                 builder.setErrorHandler(errorHandler);
    177             }
    178 
    179             oldDoc = builder.parse(xml);
    180 
    181             // Prepare a new document using a namespace aware builder
    182             factory.setNamespaceAware(true);
    183             builder = factory.newDocumentBuilder();
    184             newDoc = builder.newDocument();
    185 
    186         } catch (Exception e) {
    187             // Failed to get builder factor
    188             // Failed to create XML document builder
    189             // Failed to parse XML document
    190             // Failed to read XML document
    191         }
    192 
    193         if (oldDoc == null || newDoc == null) {
    194             return null;
    195         }
    196 
    197 
    198         // Check the root element is an XML with at least the following properties:
    199         // <sdk:sdk-repository
    200         //    xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N">
    201         //
    202         // Note that we don't have namespace support enabled, we just do it manually.
    203 
    204         Pattern nsPattern = Pattern.compile(getNsPattern());
    205 
    206         Node oldRoot = null;
    207         String prefix = null;
    208         for (Node child = oldDoc.getFirstChild(); child != null; child = child.getNextSibling()) {
    209             if (child.getNodeType() == Node.ELEMENT_NODE) {
    210                 prefix = null;
    211                 String name = child.getNodeName();
    212                 int pos = name.indexOf(':');
    213                 if (pos > 0 && pos < name.length() - 1) {
    214                     prefix = name.substring(0, pos);
    215                     name = name.substring(pos + 1);
    216                 }
    217                 if (SdkRepoConstants.NODE_SDK_REPOSITORY.equals(name)) {
    218                     NamedNodeMap attrs = child.getAttributes();
    219                     String xmlns = "xmlns";                                         //$NON-NLS-1$
    220                     if (prefix != null) {
    221                         xmlns += ":" + prefix;                                      //$NON-NLS-1$
    222                     }
    223                     Node attr = attrs.getNamedItem(xmlns);
    224                     if (attr != null) {
    225                         String uri = attr.getNodeValue();
    226                         if (uri != null && nsPattern.matcher(uri).matches()) {
    227                             oldRoot = child;
    228                             break;
    229                         }
    230                     }
    231                 }
    232             }
    233         }
    234 
    235         // we must have found the root node, and it must have an XML namespace prefix.
    236         if (oldRoot == null || prefix == null || prefix.length() == 0) {
    237             return null;
    238         }
    239 
    240         final String ns = getNsUri();
    241         Element newRoot = newDoc.createElementNS(ns, getRootElementName());
    242         newRoot.setPrefix(prefix);
    243         newDoc.appendChild(newRoot);
    244         int numTool = 0;
    245 
    246         // Find any inner <tool> or <platform-tool> nodes and extract their required parameters
    247 
    248         String[] elementNames = {
    249                 SdkRepoConstants.NODE_TOOL,
    250                 SdkRepoConstants.NODE_PLATFORM_TOOL,
    251                 SdkRepoConstants.NODE_LICENSE
    252         };
    253 
    254         Element element = null;
    255         while ((element = findChild(oldRoot, element, prefix, elementNames)) != null) {
    256             boolean isElementValid = false;
    257 
    258             String name = element.getLocalName();
    259             if (name == null) {
    260                 name = element.getNodeName();
    261 
    262                 int pos = name.indexOf(':');
    263                 if (pos > 0 && pos < name.length() - 1) {
    264                     name = name.substring(pos + 1);
    265                 }
    266             }
    267 
    268             // To be valid, the tool or platform-tool element must have:
    269             // - a <revision> element with a number
    270             // - a <min-platform-tools-rev> element with a number for a <tool> element
    271             // - an <archives> element with one or more <archive> elements inside
    272             // - one of the <archive> elements must have an "os" and "arch" attributes
    273             //   compatible with the current platform. Only keep the first such element found.
    274             // - the <archive> element must contain a <size>, a <checksum> and a <url>.
    275             // - none of the above for a license element
    276 
    277             if (SdkRepoConstants.NODE_LICENSE.equals(name)) {
    278                 isElementValid = true;
    279 
    280             } else {
    281                 try {
    282                     Node revision = findChild(element, null, prefix, RepoConstants.NODE_REVISION);
    283                     Node archives = findChild(element, null, prefix, RepoConstants.NODE_ARCHIVES);
    284 
    285                     if (revision == null || archives == null) {
    286                         continue;
    287                     }
    288 
    289                     // check revision contains a number
    290                     try {
    291                         String content = revision.getTextContent();
    292                         content = content.trim();
    293                         int rev = Integer.parseInt(content);
    294                         if (rev < 1) {
    295                             continue;
    296                         }
    297                     } catch (NumberFormatException ignore) {
    298                         continue;
    299                     }
    300 
    301                     if (SdkRepoConstants.NODE_TOOL.equals(name)) {
    302                         Node minPTRev = findChild(element, null, prefix,
    303                                 RepoConstants.NODE_MIN_PLATFORM_TOOLS_REV);
    304 
    305                         if (minPTRev == null) {
    306                             continue;
    307                         }
    308 
    309                         // check min-platform-tools-rev contains a number
    310                         try {
    311                             String content = minPTRev.getTextContent();
    312                             content = content.trim();
    313                             int rev = Integer.parseInt(content);
    314                             if (rev < 1) {
    315                                 continue;
    316                             }
    317                         } catch (NumberFormatException ignore) {
    318                             continue;
    319                         }
    320                     }
    321 
    322                     Node archive = null;
    323                     while ((archive = findChild(archives,
    324                                                 archive,
    325                                                 prefix,
    326                                                 RepoConstants.NODE_ARCHIVE)) != null) {
    327                         try {
    328                             Os os = (Os) XmlParserUtils.getEnumAttribute(archive,
    329                                     RepoConstants.ATTR_OS,
    330                                     Os.values(),
    331                                     null /*default*/);
    332                             Arch arch = (Arch) XmlParserUtils.getEnumAttribute(archive,
    333                                     RepoConstants.ATTR_ARCH,
    334                                     Arch.values(),
    335                                     Arch.ANY);
    336                             if (os == null || !os.isCompatible() ||
    337                                     arch == null || !arch.isCompatible()) {
    338                                 continue;
    339                             }
    340 
    341                             Node node = findChild(archive, null, prefix, RepoConstants.NODE_URL);
    342                             String url = node == null ? null : node.getTextContent().trim();
    343                             if (url == null || url.length() == 0) {
    344                                 continue;
    345                             }
    346 
    347                             node = findChild(archive, null, prefix, RepoConstants.NODE_SIZE);
    348                             long size = 0;
    349                             try {
    350                                 size = Long.parseLong(node.getTextContent());
    351                             } catch (Exception e) {
    352                                 // pass
    353                             }
    354                             if (size < 1) {
    355                                 continue;
    356                             }
    357 
    358                             node = findChild(archive, null, prefix, RepoConstants.NODE_CHECKSUM);
    359                             // double check that the checksum element contains a type=sha1 attribute
    360                             if (node == null) {
    361                                 continue;
    362                             }
    363                             NamedNodeMap attrs = node.getAttributes();
    364                             Node typeNode = attrs.getNamedItem(RepoConstants.ATTR_TYPE);
    365                             if (typeNode == null ||
    366                                     !RepoConstants.ATTR_TYPE.equals(typeNode.getNodeName()) ||
    367                                     !RepoConstants.SHA1_TYPE.equals(typeNode.getNodeValue())) {
    368                                 continue;
    369                             }
    370                             String sha1 = node == null ? null : node.getTextContent().trim();
    371                             if (sha1 == null ||
    372                                     sha1.length() != RepoConstants.SHA1_CHECKSUM_LEN) {
    373                                 continue;
    374                             }
    375 
    376                             isElementValid = true;
    377 
    378                         } catch (Exception ignore1) {
    379                             // pass
    380                         }
    381                     } // while <archive>
    382                 } catch (Exception ignore2) {
    383                     // For debugging it is useful to re-throw the exception.
    384                     // For end-users, not so much. It would be nice to make it
    385                     // happen automatically during unit tests.
    386                     if (System.getenv("TESTING") != null) {
    387                         throw new RuntimeException(ignore2);
    388                     }
    389                 }
    390             }
    391 
    392             if (isElementValid) {
    393                 duplicateNode(newRoot, element, SdkRepoConstants.NS_URI, prefix);
    394                 numTool++;
    395             }
    396         } // while <tool>
    397 
    398         return numTool > 0 ? newDoc : null;
    399     }
    400 
    401     /**
    402      * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given
    403      * element child in a root XML node.
    404      */
    405     private Element findChild(Node rootNode, Node after, String prefix, String[] nodeNames) {
    406         for (int i = 0; i < nodeNames.length; i++) {
    407             if (nodeNames[i].indexOf(':') < 0) {
    408                 nodeNames[i] = prefix + ":" + nodeNames[i];
    409             }
    410         }
    411         Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling();
    412         for(; child != null; child = child.getNextSibling()) {
    413             if (child.getNodeType() != Node.ELEMENT_NODE) {
    414                 continue;
    415             }
    416             for (String nodeName : nodeNames) {
    417                 if (nodeName.equals(child.getNodeName())) {
    418                     return (Element) child;
    419                 }
    420             }
    421         }
    422         return null;
    423     }
    424 
    425     /**
    426      * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given
    427      * element child in a root XML node.
    428      */
    429     private Node findChild(Node rootNode, Node after, String prefix, String nodeName) {
    430         return findChild(rootNode, after, prefix, new String[] { nodeName });
    431     }
    432 
    433     /**
    434      * Helper method used by {@link #findAlternateToolsXml(InputStream)} to duplicate a node
    435      * and attach it to the given root in the new document.
    436      */
    437     private Element duplicateNode(Element newRootNode, Element oldNode,
    438             String namespaceUri, String prefix) {
    439         // The implementation here is more or less equivalent to
    440         //
    441         //    newRoot.appendChild(newDoc.importNode(oldNode, deep=true))
    442         //
    443         // except we can't just use importNode() since we need to deal with the fact
    444         // that the old document is not namespace-aware yet the new one is.
    445 
    446         Document newDoc = newRootNode.getOwnerDocument();
    447         Element newNode = null;
    448 
    449         String nodeName = oldNode.getNodeName();
    450         int pos = nodeName.indexOf(':');
    451         if (pos > 0 && pos < nodeName.length() - 1) {
    452             nodeName = nodeName.substring(pos + 1);
    453             newNode = newDoc.createElementNS(namespaceUri, nodeName);
    454             newNode.setPrefix(prefix);
    455         } else {
    456             newNode = newDoc.createElement(nodeName);
    457         }
    458 
    459         newRootNode.appendChild(newNode);
    460 
    461         // Merge in all the attributes
    462         NamedNodeMap attrs = oldNode.getAttributes();
    463         for (int i = 0; i < attrs.getLength(); i++) {
    464             Attr attr = (Attr) attrs.item(i);
    465             Attr newAttr = null;
    466 
    467             String attrName = attr.getNodeName();
    468             pos = attrName.indexOf(':');
    469             if (pos > 0 && pos < attrName.length() - 1) {
    470                 attrName = attrName.substring(pos + 1);
    471                 newAttr = newDoc.createAttributeNS(namespaceUri, attrName);
    472                 newAttr.setPrefix(prefix);
    473             } else {
    474                 newAttr = newDoc.createAttribute(attrName);
    475             }
    476 
    477             newAttr.setNodeValue(attr.getNodeValue());
    478 
    479             if (pos > 0) {
    480                 newNode.getAttributes().setNamedItemNS(newAttr);
    481             } else {
    482                 newNode.getAttributes().setNamedItem(newAttr);
    483             }
    484         }
    485 
    486         // Merge all child elements and texts
    487         for (Node child = oldNode.getFirstChild(); child != null; child = child.getNextSibling()) {
    488             if (child.getNodeType() == Node.ELEMENT_NODE) {
    489                 duplicateNode(newNode, (Element) child, namespaceUri, prefix);
    490 
    491             } else if (child.getNodeType() == Node.TEXT_NODE) {
    492                 Text newText = newDoc.createTextNode(child.getNodeValue());
    493                 newNode.appendChild(newText);
    494             }
    495         }
    496 
    497         return newNode;
    498     }
    499 }
    500