blob: 0b5175bb5ffea31cb69dc8277d24cc1ad262e438 [file] [log] [blame]
/*
* Copyright (C) 2015 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.tools.idea.sdk.remote.internal.sources;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.annotations.VisibleForTesting.Visibility;
import com.android.io.NonClosingInputStream;
import com.android.io.NonClosingInputStream.CloseBehavior;
import com.android.sdklib.repository.IDescription;
import com.android.tools.idea.sdk.remote.RemotePkgInfo;
import com.android.tools.idea.sdk.remote.internal.CanceledByUserException;
import com.android.tools.idea.sdk.remote.internal.DownloadCache;
import com.android.tools.idea.sdk.remote.internal.ITaskMonitor;
import com.android.tools.idea.sdk.remote.internal.packages.*;
import com.intellij.openapi.diagnostic.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
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;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An sdk-addon or 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 abstract class SdkSource implements IDescription, Comparable<SdkSource> {
private String mUrl;
private RemotePkgInfo[] mPackages;
private String mDescription;
private String mFetchError;
private final String mUiName;
private static final SdkSourceProperties sSourcesProps = new SdkSourceProperties();
/**
* 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 SdkSource(String url, String uiName) {
// URLs should not be null and should not have whitespace.
if (url == null) {
url = "";
}
url = url.trim();
// 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$
String[] names = getDefaultXmlFileUrls();
if (names.length > 0) {
url += names[0];
}
}
if (uiName == null) {
uiName = sSourcesProps.getProperty(SdkSourceProperties.KEY_NAME, url, null);
}
else {
sSourcesProps.setProperty(SdkSourceProperties.KEY_NAME, url, uiName);
}
mUrl = url;
mUiName = uiName;
setDefaultDescription();
}
/**
* Returns true if this is an addon source.
* We only load addons and extras from these sources.
*/
public abstract boolean isAddonSource();
/**
* Returns true if this is a system-image source.
* We only load system-images from these sources.
*/
public abstract boolean isSysImgSource();
/**
* Returns the basename of the default URLs to try to download the
* XML manifest.
* E.g. this is typically SdkRepoConstants.URL_DEFAULT_XML_FILE
* or SdkAddonConstants.URL_DEFAULT_XML_FILE
*/
protected abstract String[] getDefaultXmlFileUrls();
/**
* Returns SdkRepoConstants.NS_LATEST_VERSION or SdkAddonConstants.NS_LATEST_VERSION.
*/
protected abstract int getNsLatestVersion();
/**
* Returns SdkRepoConstants.NS_URI or SdkAddonConstants.NS_URI.
*/
protected abstract String getNsUri();
/**
* Returns SdkRepoConstants.NS_PATTERN or SdkAddonConstants.NS_PATTERN.
*/
protected abstract String getNsPattern();
/**
* Returns SdkRepoConstants.getSchemaUri() or SdkAddonConstants.getSchemaUri().
*/
protected abstract String getSchemaUri(int version);
/* Returns SdkRepoConstants.NODE_SDK_REPOSITORY or SdkAddonConstants.NODE_SDK_ADDON. */
protected abstract String getRootElementName();
/**
* Returns SdkRepoConstants.getXsdStream() or SdkAddonConstants.getXsdStream().
*/
protected abstract StreamSource[] getXsdStream(int version);
/**
* In case we fail to load an XML, examine the XML to see if it matches a <b>future</b>
* schema that as at least a <code>tools</code> node that we could load to update the
* SDK Manager.
*
* @param xml The input XML stream. Can be null.
* @return Null on failure, otherwise returns an XML DOM with just the tools we
* need to update this SDK Manager.
* @null Can return null on failure.
*/
protected abstract Document findAlternateToolsXml(@Nullable InputStream xml) throws IOException;
/**
* Two repo source are equal if they have the same URL.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof SdkSource) {
SdkSource rs = (SdkSource)obj;
return rs.getUrl().equals(this.getUrl());
}
return false;
}
@Override
public int hashCode() {
return mUrl.hashCode();
}
/**
* Implementation of the {@link Comparable} interface.
* Simply compares the URL using the string's default ordering.
*/
@Override
public int compareTo(SdkSource rhs) {
return this.getUrl().compareTo(rhs.getUrl());
}
/**
* Returns the UI-visible name of the source. Can be null.
*/
public String getUiName() {
return mUiName;
}
/**
* Returns the URL of the 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 -- caller should
* then call {@link #load} to load the packages.
*/
public RemotePkgInfo[] getPackages() {
return mPackages;
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
protected void setPackages(RemotePkgInfo[] packages) {
mPackages = packages;
if (mPackages != null) {
// Order the packages.
Arrays.sort(mPackages, null);
}
}
/**
* Clear the internal packages list. After this call, {@link #getPackages()} will return
* null till load() is called.
*/
public void clearPackages() {
setPackages(null);
}
/**
* Indicates if the source is enabled.
* <p/>
* A 3rd-party add-on source can be disabled by the user to prevent from loading it.
*
* @return True if the source is enabled (default is true).
*/
public boolean isEnabled() {
// A URL is enabled if it's not in the disabled list.
return sSourcesProps.getProperty(SdkSourceProperties.KEY_DISABLED, mUrl, null) == null;
}
/**
* Changes whether the source is marked as enabled.
* <p/>
* When <em>changing</em> the enable state, the current package list is purged
* and the next {@code load} will either return an empty list (if disabled) or
* the actual package list (if enabled.)
*
* @param enabled True for the source to be enabled (can be loaded), false otherwise.
*/
public void setEnabled(boolean enabled) {
if (enabled != isEnabled()) {
// First we clear the current package list, which will force the
// next load() to actually set the package list as desired.
clearPackages();
sSourcesProps.setProperty(SdkSourceProperties.KEY_DISABLED, mUrl, enabled ? null /*remove*/ : "disabled"); //$NON-NLS-1$
}
}
/**
* Returns the short description of the source, if not null.
* Otherwise returns the default Object toString result.
* <p/>
* This is mostly helpful for debugging.
* For UI display, use the {@link IDescription} interface.
*/
@Override
public String toString() {
String s = getShortDescription();
if (s != null) {
return s;
}
return super.toString();
}
@Override
public String getShortDescription() {
if (mUiName != null && mUiName.length() > 0) {
String host = "malformed URL";
try {
URL u = new URL(mUrl);
host = u.getHost();
}
catch (MalformedURLException e) {
}
return String.format("%1$s (%2$s)", mUiName, host);
}
return mUrl;
}
@Override
public String getLongDescription() {
// Note: in a normal workflow, mDescription is filled by setDefaultDescription().
// However for packages made by unit tests or such, this can be null.
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 and updates the package list.
* When a source is disabled, this create an empty non-null package list.
* <p/>
* Callers can get the package list using {@link #getPackages()} after this. It will be
* null in case of error, in which case {@link #getFetchError()} can be used to an
* error message.
*
* TODO(jbakermalone): clean up below once validation in UI can match most restrictive case.
*/
public void load(DownloadCache cache, ITaskMonitor logger, boolean forceHttp) {
setDefaultDescription();
logger.setProgressMax(7);
if (!isEnabled()) {
setPackages(new RemotePkgInfo[0]);
mDescription += "\nSource is disabled.";
logger.incProgress(7);
return;
}
String url = mUrl;
if (forceHttp) {
url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
}
logger.setDescription("Fetching URL: %1$s", url);
logger.incProgress(1);
mFetchError = null;
Boolean[] validatorFound = new Boolean[]{Boolean.FALSE};
String[] validationError = new String[]{null};
Exception[] exception = new Exception[]{null};
Document validatedDoc = null;
boolean usingAlternateXml = false;
boolean usingAlternateUrl = false;
String validatedUri = null;
String[] defaultNames = getDefaultXmlFileUrls();
String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : "";
InputStream xml = fetchXmlUrl(url, cache, logger.createSubMonitor(1), exception);
if (xml != null) {
int version = getXmlSchemaVersion(xml);
if (version == 0) {
closeStream(xml);
xml = null;
}
}
// FIXME: this is a quick fix to support an alternate upgrade path.
// The whole logic below needs to be updated.
if (xml == null && defaultNames.length > 0) {
ITaskMonitor subMonitor = logger.createSubMonitor(1);
subMonitor.setProgressMax(defaultNames.length);
String baseUrl = url;
if (!baseUrl.endsWith("/")) {
int pos = baseUrl.lastIndexOf('/');
if (pos > 0) {
baseUrl = baseUrl.substring(0, pos + 1);
}
}
for (String name : defaultNames) {
String newUrl = baseUrl + name;
if (newUrl.equals(url)) {
continue;
}
xml = fetchXmlUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception);
if (xml != null) {
int version = getXmlSchemaVersion(xml);
if (version == 0) {
closeStream(xml);
xml = null;
}
else {
url = newUrl;
subMonitor.incProgress(subMonitor.getProgressMax() - subMonitor.getProgress());
break;
}
}
}
}
else {
logger.incProgress(1);
}
// If the original URL can't be fetched
// and the URL doesn't explicitly end with our filename
// and it wasn't an HTTP authentication operation canceled by the user
// then make another tentative after changing the URL.
if (xml == null && !url.endsWith(firstDefaultName) && !(exception[0] instanceof CanceledByUserException)) {
if (!url.endsWith("/")) { //$NON-NLS-1$
url += "/"; //$NON-NLS-1$
}
url += firstDefaultName;
xml = fetchXmlUrl(url, cache, logger.createSubMonitor(1), exception);
usingAlternateUrl = true;
}
else {
logger.incProgress(1);
}
// FIXME this needs to revisited.
if (xml != null) {
logger.setDescription("Validate XML: %1$s", url);
ITaskMonitor subMonitor = logger.createSubMonitor(2);
subMonitor.setProgressMax(2);
for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) {
// Explore the XML to find the potential XML schema version
int version = getXmlSchemaVersion(xml);
if (version >= 1 && version <= getNsLatestVersion()) {
// This should be a version we can handle. Try to validate it
// and report any error as invalid XML syntax,
String uri = validateXml(xml, url, version, validationError, validatorFound);
if (uri != null) {
// Validation was successful
validatedDoc = getDocument(xml, logger);
validatedUri = uri;
if (usingAlternateUrl && validatedDoc != null) {
// If the second tentative succeeded, indicate it in the console
// with the URL that worked.
logger.log("Repository found at %1$s", url);
// Keep the modified URL
mUrl = url;
}
}
else if (validatorFound[0].equals(Boolean.FALSE)) {
// Validation failed because this JVM lacks a proper XML Validator
mFetchError = validationError[0];
}
else {
// We got a validator but validation failed. We know there's
// what looks like a suitable root element with a suitable XMLNS
// so it must be a genuine error of an XML not conforming to the schema.
}
}
else if (version > getNsLatestVersion()) {
// The schema used is more recent than what is supported by this tool.
// Tell the user to upgrade, pointing him to the right version of the tool
// package.
try {
validatedDoc = findAlternateToolsXml(xml);
}
catch (IOException e) {
// Failed, will be handled below.
}
if (validatedDoc != null) {
validationError[0] = null; // remove error from XML validation
validatedUri = getNsUri();
usingAlternateXml = true;
}
}
else if (version < 1 && tryOtherUrl == 0 && !usingAlternateUrl) {
// This is obviously not one of our documents.
mFetchError = String.format("Failed to validate the XML for the repository at URL '%1$s'", url);
// If we haven't already tried the alternate URL, let's do it now.
// We don't capture any fetch exception that happen during the second
// fetch in order to avoid hiding any previous fetch errors.
if (!url.endsWith(firstDefaultName)) {
if (!url.endsWith("/")) { //$NON-NLS-1$
url += "/"; //$NON-NLS-1$
}
url += firstDefaultName;
closeStream(xml);
xml = fetchXmlUrl(url, cache, subMonitor.createSubMonitor(1), null /* outException */);
subMonitor.incProgress(1);
// Loop to try the alternative document
if (xml != null) {
usingAlternateUrl = true;
continue;
}
}
}
else if (version < 1 && usingAlternateUrl && mFetchError == null) {
// The alternate URL is obviously not a valid XML either.
// We only report the error if we failed to produce one earlier.
mFetchError = String.format("Failed to validate the XML for the repository at URL '%1$s'", url);
}
// If we get here either we succeeded or we ran out of alternatives.
break;
}
}
// 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].getClass().getSimpleName().replace("Exception", "") //$NON-NLS-1$ //$NON-NLS-2$
+ ' ' + exception[0].getMessage();
}
else {
reason = exception[0].toString();
}
logger.warning("Failed to fetch URL %1$s, reason: %2$s", url, reason);
}
if (validationError[0] != null) {
logger.logError("%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) {
// We found something using the "alternate" XML schema (that is the one made up
// to support schema upgrades). That means the user can only install the tools
// and needs to upgrade them before it download more stuff.
// Is the manager running from inside ADT?
// We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection.
boolean isADT = false;
try {
Class<?> adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin"); //$NON-NLS-1$
isADT = (adt != null);
}
catch (ClassNotFoundException e) {
// pass
}
String info;
if (isADT) {
info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin.";
mDescription =
"This repository requires a more recent version of ADT, the Eclipse Android plugin.\n" +
"You must update it before you can see other new packages.";
}
else {
info = "This repository requires a more recent version of the Tools. Please update.";
mDescription =
"This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages.";
}
mFetchError = mFetchError == null ? info : mFetchError + ". " + info;
}
logger.incProgress(1);
if (xml != null) {
logger.setDescription("Parse XML: %1$s", url);
logger.incProgress(1);
parsePackages(validatedDoc, validatedUri, logger);
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
logger.incProgress(1);
closeStream(xml);
}
private void setDefaultDescription() {
if (isAddonSource()) {
String desc = "";
if (mUiName != null) {
desc += "Add-on Provider: " + mUiName;
desc += "\n";
}
desc += "Add-on URL: " + mUrl;
mDescription = desc;
}
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.
*
* @param urlString The URL to load, as a string.
* @param monitor {@link ITaskMonitor} related to this URL.
* @param outException If non null, where to store any exception that
* happens during the fetch.
*/
private InputStream fetchXmlUrl(String urlString, DownloadCache cache, ITaskMonitor monitor, Exception[] outException) {
try {
InputStream xml = cache.openCachedUrl(urlString, monitor);
if (xml != null) {
xml.mark(500000);
xml = new NonClosingInputStream(xml);
((NonClosingInputStream)xml).setCloseBehavior(CloseBehavior.RESET);
}
return xml;
}
catch (Exception e) {
if (outException != null) {
outException[0] = e;
}
}
return null;
}
/**
* Closes the stream, ignore any exception from InputStream.close().
* If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first.
*/
private void closeStream(InputStream is) {
if (is != null) {
if (is instanceof NonClosingInputStream) {
((NonClosingInputStream)is).setCloseBehavior(CloseBehavior.CLOSE);
}
try {
is.close();
}
catch (IOException ignore) {
}
}
}
/**
* Validates this XML against one of the requested SDK Repository schemas.
* If the XML was correctly validated, returns the schema that worked.
* If it doesn't validate, returns null and stores the error in outError[0].
* If we can't find a validator, returns null and set validatorFound[0] to false.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
protected String validateXml(InputStream xml, String url, int version, String[] outError, Boolean[] validatorFound) {
if (xml == null) {
return null;
}
try {
Validator validator = getValidator(version);
if (validator == null) {
validatorFound[0] = Boolean.FALSE;
outError[0] = String.format(
"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.",
url);
return null;
}
validatorFound[0] = Boolean.TRUE;
// Reset the stream if it supports that operation.
assert xml.markSupported();
xml.reset();
// Validation throws a bunch of possible Exceptions on failure.
validator.validate(new StreamSource(xml));
return getSchemaUri(version);
}
catch (SAXParseException e) {
outError[0] = String
.format("XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", url, e.getLineNumber(), e.getColumnNumber(),
e.toString());
}
catch (Exception e) {
outError[0] = String.format("XML verification failed for %1$s.\nError: %2$s", url, e.toString());
}
return null;
}
/**
* Manually parses the root element of the XML to extract the schema version
* at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N"
* declaration.
*
* @return 1..{@link SdkRepoConstants#NS_LATEST_VERSION} for a valid schema version
* or 0 if no schema could be found.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
protected int getXmlSchemaVersion(InputStream xml) {
if (xml == null) {
return 0;
}
// Get an XML document
// TODO(jbakermalone): no need for full DOM parse here
Document doc = null;
try {
assert xml.markSupported();
xml.reset();
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();
// We don't want the default handler which prints errors to stderr.
builder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException e) throws SAXException {
// pass
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
@Override
public void error(SAXParseException e) throws SAXException {
throw e;
}
});
doc = builder.parse(xml);
// Prepare a new document using a namespace aware builder
factory.setNamespaceAware(true);
}
catch (Exception e) {
// Failed to reset XML stream
// Failed to get builder factor
// Failed to create XML document builder
// Failed to parse XML document
// Failed to read XML document
}
if (doc == null) {
return 0;
}
// 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());
String prefix = null;
for (Node child = doc.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 (getRootElementName().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) {
Matcher m = nsPattern.matcher(uri);
if (m.matches()) {
String version = m.group(1);
try {
return Integer.parseInt(version);
}
catch (NumberFormatException e) {
return 0;
}
}
}
}
}
}
}
return 0;
}
/**
* 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 SdkRepoConstants#getXsdStream(int)}
*/
private Validator getValidator(int version) throws SAXException {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
if (factory == null) {
return null;
}
StreamSource[] xsdStreams = getXsdStream(version);
// This may throw a SAX Exception if the schema itself is not a valid XSD
Schema schema = factory.newSchema(xsdStreams);
Validator validator = schema == null ? null : schema.newValidator();
// We don't want the default handler, which by default dumps errors to stderr.
validator.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException e) throws SAXException {
// pass
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
@Override
public void error(SAXParseException e) throws SAXException {
throw e;
}
});
return validator;
}
/**
* Parse all packages defined in the SDK Repository XML and creates
* a new mPackages array with them.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) {
Node root = getFirstChild(doc, nsUri, getRootElementName());
if (root != null) {
ArrayList<RemotePkgInfo> packages = new ArrayList<RemotePkgInfo>();
// 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(RepoConstants.NODE_LICENSE)) {
Node id = child.getAttributes().getNamedItem(RepoConstants.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();
RemotePkgInfo p = null;
try {
// We can load add-on and extra packages from all sources, either
// internal or user sources.
if (SdkAddonConstants.NODE_ADD_ON.equals(name)) {
p = new RemoteAddonPkgInfo(this, child, nsUri, licenses);
}
else if (SdkAddonConstants.NODE_EXTRA.equals(name)) {
p = new RemoteExtraPkgInfo(this, child, nsUri, licenses);
}
else if (!isAddonSource()) {
// We only load platform, doc and tool packages from internal
// sources, never from user sources.
if (SdkRepoConstants.NODE_PLATFORM.equals(name)) {
p = new RemotePlatformPkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_DOC.equals(name)) {
p = new RemoteDocPkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_TOOL.equals(name)) {
p = new RemoteToolPkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_PLATFORM_TOOL.equals(name)) {
p = new PlatformToolRemotePkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_BUILD_TOOL.equals(name)) {
p = new RemoteBuildToolPkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_SAMPLE.equals(name)) {
p = new RemoteSamplePkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_SYSTEM_IMAGE.equals(name)) {
p = new RemoteSystemImagePkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_SOURCE.equals(name)) {
p = new RemoteSourcePkgInfo(this, child, nsUri, licenses);
}
else if (SdkRepoConstants.NODE_NDK.equals(name)) {
p = new RemoteNdkPkgInfo(this, child, nsUri, licenses);
}
}
if (p != null) {
packages.add(p);
monitor.logVerbose("Found %1$s", p.getShortDescription());
}
}
catch (Exception e) {
// Ignore invalid packages
String msg = String.format("Ignoring invalid %1$s element: %2$s", name, e.toString());
monitor.logError(msg);
Logger.getInstance(getClass()).error(msg, e);
}
}
}
setPackages(packages.toArray(new RemotePkgInfo[packages.size()]));
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.
* <p/>
* On error, returns null and prints a (hopefully) useful message on the monitor.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
protected Document getDocument(InputStream xml, ITaskMonitor monitor) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setIgnoringComments(true);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
assert xml.markSupported();
xml.reset();
Document doc = builder.parse(new InputSource(xml));
return doc;
}
catch (ParserConfigurationException e) {
monitor.logError("Failed to create XML document builder");
}
catch (SAXException e) {
monitor.logError("Failed to parse XML document");
}
catch (IOException e) {
monitor.logError("Failed to read XML document");
}
return null;
}
}