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 <tool> and the <platform-tools> 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 <tool> and the <platform-tools> elements must have at 118 * least one <archive> compatible with this platform. 119 * <p/> 120 * Starting the sdk-repository schema v3, <tools> has a <min-platform-tools-rev> 121 * node, so technically the corresponding XML schema will be usable only if there's a 122 * <platform-tools> 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 <tool> 129 * and <platform-tools> 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 <tool> 146 * and <platform-tools> 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