/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdklib.internal.repository.sources; import com.android.annotations.Nullable; import com.android.sdklib.internal.repository.archives.ArchFilter; import com.android.sdklib.internal.repository.packages.Package; import com.android.sdklib.internal.repository.packages.PackageParserUtils; import com.android.sdklib.repository.RepoConstants; import com.android.sdklib.repository.SdkRepoConstants; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import org.xml.sax.ErrorHandler; import java.io.IOException; import java.io.InputStream; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; /** * An sdk-repository source, i.e. a download site. * A repository describes one or more {@link Package}s available for download. * * @deprecated * com.android.sdklib.internal.repository has moved into Studio as * com.android.tools.idea.sdk.remote.internal. */ @Deprecated public class SdkRepoSource extends SdkSource { /** * Constructs a new source for the given repository URL. * @param url The source URL. Cannot be null. If the URL ends with a /, the default * repository.xml filename will be appended automatically. * @param uiName The UI-visible name of the source. Can be null. */ public SdkRepoSource(String url, String uiName) { super(url, uiName); } /** * Returns true if this is an addon source. * We only load addons and extras from these sources. */ @Override public boolean isAddonSource() { return false; } /** * Returns true if this is a system-image source. * We only load system-images from these sources. */ @Override public boolean isSysImgSource() { return false; } private static String[] sDefaults = null; // lazily allocated in getDefaultXmlFileUrls @Override protected String[] getDefaultXmlFileUrls() { if (sDefaults == null) { String[] values = new String[SdkRepoConstants.NS_LATEST_VERSION - SdkRepoConstants.NS_SERVER_MIN_VERSION + 2]; int k = 0; for (int i = SdkRepoConstants.NS_LATEST_VERSION; i >= SdkRepoConstants.NS_SERVER_MIN_VERSION; i--) { values[k++] = String.format(SdkRepoConstants.URL_FILENAME_PATTERN, i); } values[k++] = SdkRepoConstants.URL_DEFAULT_FILENAME; assert k == values.length; sDefaults = values; } return sDefaults; } @Override protected int getNsLatestVersion() { return SdkRepoConstants.NS_LATEST_VERSION; } @Override protected String getNsUri() { return SdkRepoConstants.NS_URI; } @Override protected String getNsPattern() { return SdkRepoConstants.NS_PATTERN; } @Override protected String getSchemaUri(int version) { return SdkRepoConstants.getSchemaUri(version); } @Override protected String getRootElementName() { return SdkRepoConstants.NODE_SDK_REPOSITORY; } @Override protected InputStream getXsdStream(int version) { return SdkRepoConstants.getXsdStream(version); } /** * The purpose of this method is to support forward evolution of our schema. * <p/> * At this point, we know that xml does not point to any schema that this version of * the tool knows how to process, so it's not one of the possible 1..N versions of our * XSD schema. * <p/> * We thus try to interpret the byte stream as a possible XML stream. It may not be * one at all in the first place. If it looks anything line an XML schema, we try to * find its <tool> and the <platform-tools> elements. If we find any, * we recreate a suitable document that conforms to what we expect from our XSD schema * with only those elements. * <p/> * To be valid, the <tool> and the <platform-tools> elements must have at * least one <archive> compatible with this platform. * <p/> * Starting the sdk-repository schema v3, <tools> has a <min-platform-tools-rev> * node, so technically the corresponding XML schema will be usable only if there's a * <platform-tools> with the request revision number. We don't enforce that here, as * this is done at install time. * <p/> * If we don't find anything suitable, we drop the whole thing. * * @param xml The input XML stream. Can be null. * @return Either a new XML document conforming to our schema with at least one <tool> * and <platform-tools> element or null. * @throws IOException if InputStream.reset() fails * @null Can return null on failure. */ @Override protected Document findAlternateToolsXml(@Nullable InputStream xml) throws IOException { return findAlternateToolsXml(xml, null /*errorHandler*/); } /** * An alternate version of {@link #findAlternateToolsXml(InputStream)} that allows * the caller to specify the XML error handler. The default from the underlying Java * XML Xerces parser will dump to stdout/stderr, which is not convenient during unit tests. * * @param xml The input XML stream. Can be null. * @param errorHandler An optional XML error handler. If null, the default will be used. * @return Either a new XML document conforming to our schema with at least one <tool> * and <platform-tools> element or null. * @throws IOException if InputStream.reset() fails * @null Can return null on failure. * @see #findAlternateToolsXml(InputStream) findAlternateToolsXml() provides more details. */ protected Document findAlternateToolsXml( @Nullable InputStream xml, @Nullable ErrorHandler errorHandler) throws IOException { if (xml == null) { return null; } // Reset the stream if it supports that operation. assert xml.markSupported(); xml.reset(); // Get an XML document Document oldDoc = null; Document newDoc = null; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(false); factory.setValidating(false); // Parse the old document using a non namespace aware builder factory.setNamespaceAware(false); DocumentBuilder builder = factory.newDocumentBuilder(); if (errorHandler != null) { builder.setErrorHandler(errorHandler); } oldDoc = builder.parse(xml); // Prepare a new document using a namespace aware builder factory.setNamespaceAware(true); builder = factory.newDocumentBuilder(); newDoc = builder.newDocument(); } catch (Exception e) { // Failed to get builder factor // Failed to create XML document builder // Failed to parse XML document // Failed to read XML document } if (oldDoc == null || newDoc == null) { return null; } // Check the root element is an XML with at least the following properties: // <sdk:sdk-repository // xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N"> // // Note that we don't have namespace support enabled, we just do it manually. Pattern nsPattern = Pattern.compile(getNsPattern()); Node oldRoot = null; String prefix = null; for (Node child = oldDoc.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE) { prefix = null; String name = child.getNodeName(); int pos = name.indexOf(':'); if (pos > 0 && pos < name.length() - 1) { prefix = name.substring(0, pos); name = name.substring(pos + 1); } if (SdkRepoConstants.NODE_SDK_REPOSITORY.equals(name)) { NamedNodeMap attrs = child.getAttributes(); String xmlns = "xmlns"; //$NON-NLS-1$ if (prefix != null) { xmlns += ":" + prefix; //$NON-NLS-1$ } Node attr = attrs.getNamedItem(xmlns); if (attr != null) { String uri = attr.getNodeValue(); if (uri != null && nsPattern.matcher(uri).matches()) { oldRoot = child; break; } } } } } // we must have found the root node, and it must have an XML namespace prefix. if (oldRoot == null || prefix == null || prefix.isEmpty()) { return null; } final String ns = getNsUri(); Element newRoot = newDoc.createElementNS(ns, getRootElementName()); newRoot.setPrefix(prefix); newDoc.appendChild(newRoot); int numTool = 0; // Find any inner <tool> or <platform-tool> nodes and extract their required parameters String[] elementNames = { SdkRepoConstants.NODE_TOOL, SdkRepoConstants.NODE_PLATFORM_TOOL, SdkRepoConstants.NODE_LICENSE }; Element element = null; while ((element = findChild(oldRoot, element, prefix, elementNames)) != null) { boolean isElementValid = false; String name = element.getLocalName(); if (name == null) { name = element.getNodeName(); int pos = name.indexOf(':'); if (pos > 0 && pos < name.length() - 1) { name = name.substring(pos + 1); } } // To be valid, the tool or platform-tool element must have: // - a <revision> element with a number // - a <min-platform-tools-rev> element with a number for a <tool> element // - an <archives> element with one or more <archive> elements inside // - one of the <archive> elements must have an "os" and "arch" attributes // compatible with the current platform. Only keep the first such element found. // - the <archive> element must contain a <size>, a <checksum> and a <url>. // - none of the above for a license element if (SdkRepoConstants.NODE_LICENSE.equals(name)) { isElementValid = true; } else { try { Node revision = findChild(element, null, prefix, RepoConstants.NODE_REVISION); Node archives = findChild(element, null, prefix, RepoConstants.NODE_ARCHIVES); if (revision == null || archives == null) { continue; } // check revision contains a number try { String content = revision.getTextContent(); content = content.trim(); int rev = Integer.parseInt(content); if (rev < 1) { continue; } } catch (NumberFormatException ignore) { continue; } if (SdkRepoConstants.NODE_TOOL.equals(name)) { Node minPTRev = findChild(element, null, prefix, RepoConstants.NODE_MIN_PLATFORM_TOOLS_REV); if (minPTRev == null) { continue; } // check min-platform-tools-rev contains a number try { String content = minPTRev.getTextContent(); content = content.trim(); int rev = Integer.parseInt(content); if (rev < 1) { continue; } } catch (NumberFormatException ignore) { continue; } } Node archive = null; while ((archive = findChild(archives, archive, prefix, RepoConstants.NODE_ARCHIVE)) != null) { try { ArchFilter af = PackageParserUtils.parseArchFilter(archive); if (af == null || !af.isCompatibleWith(ArchFilter.getCurrent())) { continue; } Node node = findChild(archive, null, prefix, RepoConstants.NODE_URL); String url = node == null ? null : node.getTextContent().trim(); if (url == null || url.isEmpty()) { continue; } node = findChild(archive, null, prefix, RepoConstants.NODE_SIZE); long size = 0; try { size = Long.parseLong(node.getTextContent()); } catch (Exception e) { // pass } if (size < 1) { continue; } node = findChild(archive, null, prefix, RepoConstants.NODE_CHECKSUM); // double check that the checksum element contains a type=sha1 attribute if (node == null) { continue; } NamedNodeMap attrs = node.getAttributes(); Node typeNode = attrs.getNamedItem(RepoConstants.ATTR_TYPE); if (typeNode == null || !RepoConstants.ATTR_TYPE.equals(typeNode.getNodeName()) || !RepoConstants.SHA1_TYPE.equals(typeNode.getNodeValue())) { continue; } String sha1 = node == null ? null : node.getTextContent().trim(); if (sha1 == null || sha1.length() != RepoConstants.SHA1_CHECKSUM_LEN) { continue; } isElementValid = true; } catch (Exception ignore1) { // For debugging it is useful to re-throw the exception. // For end-users, not so much. It would be nice to make it // happen automatically during unit tests. if (System.getenv("TESTING") != null || System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { throw new RuntimeException(ignore1); } } } // while <archive> } catch (Exception ignore2) { // For debugging it is useful to re-throw the exception. // For end-users, not so much. It would be nice to make it // happen automatically during unit tests. if (System.getenv("TESTING") != null || System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { throw new RuntimeException(ignore2); } } } if (isElementValid) { duplicateNode(newRoot, element, SdkRepoConstants.NS_URI, prefix); numTool++; } } // while <tool> return numTool > 0 ? newDoc : null; } /** * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given * element child in a root XML node. */ private Element findChild(Node rootNode, Node after, String prefix, String[] nodeNames) { for (int i = 0; i < nodeNames.length; i++) { if (nodeNames[i].indexOf(':') < 0) { nodeNames[i] = prefix + ":" + nodeNames[i]; } } Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling(); for(; child != null; child = child.getNextSibling()) { if (child.getNodeType() != Node.ELEMENT_NODE) { continue; } for (String nodeName : nodeNames) { if (nodeName.equals(child.getNodeName())) { return (Element) child; } } } return null; } /** * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given * element child in a root XML node. */ private Node findChild(Node rootNode, Node after, String prefix, String nodeName) { return findChild(rootNode, after, prefix, new String[] { nodeName }); } /** * Helper method used by {@link #findAlternateToolsXml(InputStream)} to duplicate a node * and attach it to the given root in the new document. */ private Element duplicateNode(Element newRootNode, Element oldNode, String namespaceUri, String prefix) { // The implementation here is more or less equivalent to // // newRoot.appendChild(newDoc.importNode(oldNode, deep=true)) // // except we can't just use importNode() since we need to deal with the fact // that the old document is not namespace-aware yet the new one is. Document newDoc = newRootNode.getOwnerDocument(); Element newNode = null; String nodeName = oldNode.getNodeName(); int pos = nodeName.indexOf(':'); if (pos > 0 && pos < nodeName.length() - 1) { nodeName = nodeName.substring(pos + 1); newNode = newDoc.createElementNS(namespaceUri, nodeName); newNode.setPrefix(prefix); } else { newNode = newDoc.createElement(nodeName); } newRootNode.appendChild(newNode); // Merge in all the attributes NamedNodeMap attrs = oldNode.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); Attr newAttr = null; String attrName = attr.getNodeName(); pos = attrName.indexOf(':'); if (pos > 0 && pos < attrName.length() - 1) { attrName = attrName.substring(pos + 1); newAttr = newDoc.createAttributeNS(namespaceUri, attrName); newAttr.setPrefix(prefix); } else { newAttr = newDoc.createAttribute(attrName); } newAttr.setNodeValue(attr.getNodeValue()); if (pos > 0) { newNode.getAttributes().setNamedItemNS(newAttr); } else { newNode.getAttributes().setNamedItem(newAttr); } } // Merge all child elements and texts for (Node child = oldNode.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE) { duplicateNode(newNode, (Element) child, namespaceUri, prefix); } else if (child.getNodeType() == Node.TEXT_NODE) { Text newText = newDoc.createTextNode(child.getNodeValue()); newNode.appendChild(newText); } } return newNode; } }