blob: 430839edeb483253dbf250384e8fe7c1c26e8424 [file] [log] [blame]
/*
* 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;
import com.android.sdklib.internal.repository.Archive.Arch;
import com.android.sdklib.internal.repository.Archive.Os;
import com.android.sdklib.repository.SdkRepository;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.regex.Pattern;
import javax.net.ssl.SSLKeyException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
/**
* An sdk-repository source, i.e. a download site.
* It may be a full repository or an add-on only repository.
* A repository describes one or {@link Package}s available for download.
*/
public class RepoSource implements IDescription {
private String mUrl;
private final boolean mUserSource;
private Package[] mPackages;
private String mDescription;
private String mFetchError;
/**
* 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 userSource True if this a user source (add-ons & packages only.)
*/
public RepoSource(String url, boolean userSource) {
// if the URL ends with a /, it must be "directory" resource,
// in which case we automatically add the default file that will
// looked for. This way it will be obvious to the user which
// resource we are actually trying to fetch.
if (url.endsWith("/")) { //$NON-NLS-1$
url += SdkRepository.URL_DEFAULT_XML_FILE;
}
mUrl = url;
mUserSource = userSource;
setDefaultDescription();
}
/**
* Two repo source are equal if they have the same userSource flag and the same URL.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof RepoSource) {
RepoSource rs = (RepoSource) obj;
return rs.isUserSource() == this.isUserSource() && rs.getUrl().equals(this.getUrl());
}
return false;
}
@Override
public int hashCode() {
return mUrl.hashCode() ^ Boolean.valueOf(mUserSource).hashCode();
}
/** Returns true if this is a user source. We only load addon and extra packages
* from a user source and ignore the rest. */
public boolean isUserSource() {
return mUserSource;
}
/** Returns the URL of the repository.xml file for this source. */
public String getUrl() {
return mUrl;
}
/**
* Returns the list of known packages found by the last call to load().
* This is null when the source hasn't been loaded yet.
*/
public Package[] getPackages() {
return mPackages;
}
/**
* Clear the internal packages list. After this call, {@link #getPackages()} will return
* null till load() is called.
*/
public void clearPackages() {
mPackages = null;
}
public String getShortDescription() {
return mUrl;
}
public String getLongDescription() {
return mDescription == null ? "" : mDescription; //$NON-NLS-1$
}
/**
* Returns the last fetch error description.
* If there was no error, returns null.
*/
public String getFetchError() {
return mFetchError;
}
/**
* Tries to fetch the repository index for the given URL.
*/
public void load(ITaskMonitor monitor, boolean forceHttp) {
monitor.setProgressMax(4);
setDefaultDescription();
String url = mUrl;
if (forceHttp) {
url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
}
monitor.setDescription("Fetching %1$s", url);
monitor.incProgress(1);
mFetchError = null;
String[] validationError = new String[] { null };
Exception[] exception = new Exception[] { null };
ByteArrayInputStream xml = fetchUrl(url, exception);
Document validatedDoc = null;
boolean usingAlternateXml = false;
String validatedUri = null;
if (xml != null) {
monitor.setDescription("Validate XML");
String uri = validateXml(xml, url, validationError);
if (uri != null) {
validatedDoc = getDocument(xml, monitor);
validatedUri = uri;
} else {
validatedDoc = findAlternateToolsXml(xml);
if (validatedDoc != null) {
validationError[0] = null; // remove error from XML validation
validatedUri = SdkRepository.NS_SDK_REPOSITORY;
usingAlternateXml = true;
}
}
}
// If we failed the first time and the URL doesn't explicitly end with
// our filename, make another tentative after changing the URL.
if (validatedDoc == null && !url.endsWith(SdkRepository.URL_DEFAULT_XML_FILE)) {
if (!url.endsWith("/")) { //$NON-NLS-1$
url += "/"; //$NON-NLS-1$
}
url += SdkRepository.URL_DEFAULT_XML_FILE;
xml = fetchUrl(url, exception);
if (xml != null) {
String uri = validateXml(xml, url, validationError);
if (uri != null) {
validatedDoc = getDocument(xml, monitor);
validatedUri = uri;
} else {
validatedDoc = findAlternateToolsXml(xml);
if (validatedDoc != null) {
validationError[0] = null; // remove error from XML validation
validatedUri = SdkRepository.NS_SDK_REPOSITORY;
usingAlternateXml = true;
}
}
}
if (validatedDoc != null) {
// If the second tentative succeeded, indicate it in the console
// with the URL that worked.
monitor.setResult("Repository found at %1$s", url);
// Keep the modified URL
mUrl = url;
}
}
// If any exception was handled during the URL fetch, display it now.
if (exception[0] != null) {
mFetchError = "Failed to fetch URL";
String reason = null;
if (exception[0] instanceof FileNotFoundException) {
// FNF has no useful getMessage, so we need to special handle it.
reason = "File not found";
mFetchError += ": " + reason;
} else if (exception[0] instanceof SSLKeyException) {
// That's a common error and we have a pref for it.
reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
mFetchError += ": HTTPS SSL error";
} else if (exception[0].getMessage() != null) {
reason = exception[0].getMessage();
} else {
// We don't know what's wrong. Let's give the exception class at least.
reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
}
monitor.setResult("Failed to fetch URL %1$s, reason: %2$s", url, reason);
}
if(validationError[0] != null) {
monitor.setResult("%s", validationError[0]); //$NON-NLS-1$
}
// Stop here if we failed to validate the XML. We don't want to load it.
if (validatedDoc == null) {
return;
}
if (usingAlternateXml) {
String info = "This repository requires a more recent version of the Tools. Please update.";
mFetchError = mFetchError == null ? info : mFetchError + ". " + info;
mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages.";
}
monitor.incProgress(1);
if (xml != null) {
monitor.setDescription("Parse XML");
monitor.incProgress(1);
parsePackages(validatedDoc, validatedUri, monitor);
if (mPackages == null || mPackages.length == 0) {
mDescription += "\nNo packages found.";
} else if (mPackages.length == 1) {
mDescription += "\nOne package found.";
} else {
mDescription += String.format("\n%1$d packages found.", mPackages.length);
}
}
// done
monitor.incProgress(1);
}
private void setDefaultDescription() {
if (mUserSource) {
mDescription = String.format("Add-on Source: %1$s", mUrl);
} else {
mDescription = String.format("SDK Source: %1$s", mUrl);
}
}
/**
* Fetches the document at the given URL and returns it as a string.
* Returns null if anything wrong happens and write errors to the monitor.
*
* References:
* Java URL Connection: http://java.sun.com/docs/books/tutorial/networking/urls/readingWriting.html
* Java URL Reader: http://java.sun.com/docs/books/tutorial/networking/urls/readingURL.html
* Java set Proxy: http://java.sun.com/docs/books/tutorial/networking/urls/_setProxy.html
*/
private ByteArrayInputStream fetchUrl(String urlString, Exception[] outException) {
URL url;
try {
url = new URL(urlString);
InputStream is = null;
int inc = 65536;
int curr = 0;
byte[] result = new byte[inc];
try {
is = url.openStream();
int n;
while ((n = is.read(result, curr, result.length - curr)) != -1) {
curr += n;
if (curr == result.length) {
byte[] temp = new byte[curr + inc];
System.arraycopy(result, 0, temp, 0, curr);
result = temp;
}
}
return new ByteArrayInputStream(result, 0, curr);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// pass
}
}
}
} catch (Exception e) {
outException[0] = e;
}
return null;
}
/**
* Validates this XML against one of the possible SDK Repository schema, starting
* by the most recent one.
* If the XML was correctly validated, returns the schema that worked.
* If no schema validated the XML, returns null.
*/
private String validateXml(ByteArrayInputStream xml, String url, String[] outError) {
String lastError = null;
String extraError = null;
for (int version = SdkRepository.NS_LATEST_VERSION; version >= 1; version--) {
try {
Validator validator = getValidator(version);
if (validator == null) {
lastError = "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.";
continue;
}
xml.reset();
// Validation throws a bunch of possible Exceptions on failure.
validator.validate(new StreamSource(xml));
return SdkRepository.getSchemaUri(version);
} catch (Exception e) {
lastError = "XML verification failed for %1$s.\nError: %2$s";
extraError = e.getMessage();
if (extraError == null) {
extraError = e.getClass().getName();
}
}
}
if (lastError != null) {
outError[0] = String.format(lastError, url, extraError);
}
return null;
}
/**
* 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 know 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 &lt;tool&gt; elements. If we find any, we recreate a suitable document
* that conforms to what we expect from our XSD schema with only those elements.
* To be valid, the &lt;tool&gt; element must have at least one &lt;archive&gt;
* compatible with this platform.
*
* 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 &lt;tool&gt;
* element or null.
*/
protected Document findAlternateToolsXml(InputStream xml) {
// Note: protected for unit-test access
if (xml == null) {
return null;
}
// Reset the stream if it supports that operation.
// At runtime we use a ByteArrayInputStream which can be reset; however for unit tests
// we use a FileInputStream that doesn't support resetting and is read-once.
try {
xml.reset();
} catch (IOException e1) {
// ignore if not supported
}
// 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();
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 xsd-schema 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(SdkRepository.NS_SDK_REPOSITORY_PATTERN);
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 (SdkRepository.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.length() == 0) {
return null;
}
final String ns = SdkRepository.NS_SDK_REPOSITORY;
Element newRoot = newDoc.createElementNS(ns, SdkRepository.NODE_SDK_REPOSITORY);
newRoot.setPrefix(prefix);
newDoc.appendChild(newRoot);
int numTool = 0;
// Find an inner <tool> node and extract its required parameters
Node tool = null;
while ((tool = findChild(oldRoot, tool, prefix, SdkRepository.NODE_TOOL)) != null) {
// To be valid, the tool element must have:
// - a <revision> element with a number
// - an optional <uses-license> node, which we'll skip right now.
// (if we add it later, we must find the license declaration element too)
// - 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>.
try {
Node revision = findChild(tool, null, prefix, SdkRepository.NODE_REVISION);
Node archives = findChild(tool, null, prefix, SdkRepository.NODE_ARCHIVES);
if (revision == null || archives == null) {
continue;
}
int rev = 0;
try {
String content = revision.getTextContent();
content = content.trim();
rev = Integer.parseInt(content);
if (rev < 1) {
continue;
}
} catch (NumberFormatException ignore) {
continue;
}
Element newTool = newDoc.createElementNS(ns, SdkRepository.NODE_TOOL);
newTool.setPrefix(prefix);
appendChild(newTool, ns, prefix,
SdkRepository.NODE_REVISION, Integer.toString(rev));
Element newArchives = appendChild(newTool, ns, prefix,
SdkRepository.NODE_ARCHIVES, null);
int numArchives = 0;
Node archive = null;
while ((archive = findChild(archives,
archive,
prefix,
SdkRepository.NODE_ARCHIVE)) != null) {
try {
Os os = (Os) XmlParserUtils.getEnumAttribute(archive,
SdkRepository.ATTR_OS,
Os.values(),
null /*default*/);
Arch arch = (Arch) XmlParserUtils.getEnumAttribute(archive,
SdkRepository.ATTR_ARCH,
Arch.values(),
Arch.ANY);
if (os == null || !os.isCompatible() ||
arch == null || !arch.isCompatible()) {
continue;
}
Node node = findChild(archive, null, prefix, SdkRepository.NODE_URL);
String url = node == null ? null : node.getTextContent().trim();
if (url == null || url.length() == 0) {
continue;
}
node = findChild(archive, null, prefix, SdkRepository.NODE_SIZE);
long size = 0;
try {
size = Long.parseLong(node.getTextContent());
} catch (Exception e) {
// pass
}
if (size < 1) {
continue;
}
node = findChild(archive, null, prefix, SdkRepository.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(SdkRepository.ATTR_TYPE);
if (typeNode == null ||
!SdkRepository.ATTR_TYPE.equals(typeNode.getNodeName()) ||
!SdkRepository.SHA1_TYPE.equals(typeNode.getNodeValue())) {
continue;
}
String sha1 = node == null ? null : node.getTextContent().trim();
if (sha1 == null || sha1.length() != SdkRepository.SHA1_CHECKSUM_LEN) {
continue;
}
// Use that archive for the new tool element
Element ar = appendChild(newArchives, ns, prefix,
SdkRepository.NODE_ARCHIVE, null);
ar.setAttributeNS(ns, SdkRepository.ATTR_OS, os.getXmlName());
ar.setAttributeNS(ns, SdkRepository.ATTR_ARCH, arch.getXmlName());
appendChild(ar, ns, prefix, SdkRepository.NODE_URL, url);
appendChild(ar, ns, prefix, SdkRepository.NODE_SIZE, Long.toString(size));
Element cs = appendChild(ar, ns, prefix, SdkRepository.NODE_CHECKSUM, sha1);
cs.setAttributeNS(ns, SdkRepository.ATTR_TYPE, SdkRepository.SHA1_TYPE);
numArchives++;
} catch (Exception ignore1) {
// pass
}
} // while <archive>
if (numArchives > 0) {
newRoot.appendChild(newTool);
numTool++;
}
} catch (Exception ignore2) {
// pass
}
} // 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 Node findChild(Node rootNode, Node after, String prefix, String nodeName) {
nodeName = prefix + ":" + nodeName;
Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling();
for(; child != null; child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE && nodeName.equals(child.getNodeName())) {
return child;
}
}
return null;
}
/**
* Helper method used by {@link #findAlternateToolsXml(InputStream)} to create a new
* XML element into a parent element.
*/
private Element appendChild(Element rootNode, String namespaceUri,
String prefix, String nodeName,
String nodeValue) {
Element node = rootNode.getOwnerDocument().createElementNS(namespaceUri, nodeName);
node.setPrefix(prefix);
if (nodeValue != null) {
node.setTextContent(nodeValue);
}
rootNode.appendChild(node);
return node;
}
/**
* Helper method that returns a validator for our XSD, or null if the current Java
* implementation can't process XSD schemas.
*
* @param version The version of the XML Schema.
* See {@link SdkRepository#getXsdStream(int)}
*/
private Validator getValidator(int version) throws SAXException {
InputStream xsdStream = SdkRepository.getXsdStream(version);
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
if (factory == null) {
return null;
}
// This may throw a SAX Exception if the schema itself is not a valid XSD
Schema schema = factory.newSchema(new StreamSource(xsdStream));
Validator validator = schema == null ? null : schema.newValidator();
return validator;
}
/**
* Parse all packages defined in the SDK Repository XML and creates
* a new mPackages array with them.
*/
protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) {
// protected for unit-test acces
assert doc != null;
Node root = getFirstChild(doc, nsUri, SdkRepository.NODE_SDK_REPOSITORY);
if (root != null) {
ArrayList<Package> packages = new ArrayList<Package>();
// Parse license definitions
HashMap<String, String> licenses = new HashMap<String, String>();
for (Node child = root.getFirstChild();
child != null;
child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE &&
nsUri.equals(child.getNamespaceURI()) &&
child.getLocalName().equals(SdkRepository.NODE_LICENSE)) {
Node id = child.getAttributes().getNamedItem(SdkRepository.ATTR_ID);
if (id != null) {
licenses.put(id.getNodeValue(), child.getTextContent());
}
}
}
// Parse packages
for (Node child = root.getFirstChild();
child != null;
child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE &&
nsUri.equals(child.getNamespaceURI())) {
String name = child.getLocalName();
Package p = null;
try {
// We can load addon and extra packages from all sources, either
// internal or user sources.
if (SdkRepository.NODE_ADD_ON.equals(name)) {
p = new AddonPackage(this, child, licenses);
} else if (SdkRepository.NODE_EXTRA.equals(name)) {
p = new ExtraPackage(this, child, licenses);
} else if (!mUserSource) {
// We only load platform, doc and tool packages from internal
// sources, never from user sources.
if (SdkRepository.NODE_PLATFORM.equals(name)) {
p = new PlatformPackage(this, child, licenses);
} else if (SdkRepository.NODE_DOC.equals(name)) {
p = new DocPackage(this, child, licenses);
} else if (SdkRepository.NODE_TOOL.equals(name)) {
p = new ToolPackage(this, child, licenses);
}
}
if (p != null) {
packages.add(p);
monitor.setDescription("Found %1$s", p.getShortDescription());
}
} catch (Exception e) {
// Ignore invalid packages
}
}
}
mPackages = packages.toArray(new Package[packages.size()]);
// Order the packages.
Arrays.sort(mPackages, null);
return true;
}
return false;
}
/**
* Returns the first child element with the given XML local name.
* If xmlLocalName is null, returns the very first child element.
*/
private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE &&
nsUri.equals(child.getNamespaceURI())) {
if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
return child;
}
}
}
return null;
}
/**
* Takes an XML document as a string as parameter and returns a DOM for it.
*
* On error, returns null and prints a (hopefully) useful message on the monitor.
*/
private Document getDocument(ByteArrayInputStream xml, ITaskMonitor monitor) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setIgnoringComments(true);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
xml.reset();
Document doc = builder.parse(new InputSource(xml));
return doc;
} catch (ParserConfigurationException e) {
monitor.setResult("Failed to create XML document builder");
} catch (SAXException e) {
monitor.setResult("Failed to parse XML document");
} catch (IOException e) {
monitor.setResult("Failed to read XML document");
}
return null;
}
}