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.sdklib.internal.repository.Archive.Arch;
     20 import com.android.sdklib.internal.repository.Archive.Os;
     21 import com.android.sdklib.repository.SdkRepository;
     22 
     23 import org.w3c.dom.Document;
     24 import org.w3c.dom.Element;
     25 import org.w3c.dom.NamedNodeMap;
     26 import org.w3c.dom.Node;
     27 import org.xml.sax.InputSource;
     28 import org.xml.sax.SAXException;
     29 
     30 import java.io.ByteArrayInputStream;
     31 import java.io.FileNotFoundException;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.net.URL;
     35 import java.util.ArrayList;
     36 import java.util.Arrays;
     37 import java.util.HashMap;
     38 import java.util.regex.Pattern;
     39 
     40 import javax.net.ssl.SSLKeyException;
     41 import javax.xml.XMLConstants;
     42 import javax.xml.parsers.DocumentBuilder;
     43 import javax.xml.parsers.DocumentBuilderFactory;
     44 import javax.xml.parsers.ParserConfigurationException;
     45 import javax.xml.transform.stream.StreamSource;
     46 import javax.xml.validation.Schema;
     47 import javax.xml.validation.SchemaFactory;
     48 import javax.xml.validation.Validator;
     49 
     50 /**
     51  * An sdk-repository source, i.e. a download site.
     52  * It may be a full repository or an add-on only repository.
     53  * A repository describes one or {@link Package}s available for download.
     54  */
     55 public class RepoSource implements IDescription {
     56 
     57     private String mUrl;
     58     private final boolean mUserSource;
     59 
     60     private Package[] mPackages;
     61     private String mDescription;
     62     private String mFetchError;
     63 
     64     /**
     65      * Constructs a new source for the given repository URL.
     66      * @param url The source URL. Cannot be null. If the URL ends with a /, the default
     67      *            repository.xml filename will be appended automatically.
     68      * @param userSource True if this a user source (add-ons & packages only.)
     69      */
     70     public RepoSource(String url, boolean userSource) {
     71 
     72         // if the URL ends with a /, it must be "directory" resource,
     73         // in which case we automatically add the default file that will
     74         // looked for. This way it will be obvious to the user which
     75         // resource we are actually trying to fetch.
     76         if (url.endsWith("/")) {  //$NON-NLS-1$
     77             url += SdkRepository.URL_DEFAULT_XML_FILE;
     78         }
     79 
     80         mUrl = url;
     81         mUserSource = userSource;
     82         setDefaultDescription();
     83     }
     84 
     85     /**
     86      * Two repo source are equal if they have the same userSource flag and the same URL.
     87      */
     88     @Override
     89     public boolean equals(Object obj) {
     90         if (obj instanceof RepoSource) {
     91             RepoSource rs = (RepoSource) obj;
     92             return  rs.isUserSource() == this.isUserSource() && rs.getUrl().equals(this.getUrl());
     93         }
     94         return false;
     95     }
     96 
     97     @Override
     98     public int hashCode() {
     99         return mUrl.hashCode() ^ Boolean.valueOf(mUserSource).hashCode();
    100     }
    101 
    102     /** Returns true if this is a user source. We only load addon and extra packages
    103      * from a user source and ignore the rest. */
    104     public boolean isUserSource() {
    105         return mUserSource;
    106     }
    107 
    108     /** Returns the URL of the repository.xml file for this source. */
    109     public String getUrl() {
    110         return mUrl;
    111     }
    112 
    113     /**
    114      * Returns the list of known packages found by the last call to load().
    115      * This is null when the source hasn't been loaded yet.
    116      */
    117     public Package[] getPackages() {
    118         return mPackages;
    119     }
    120 
    121     /**
    122      * Clear the internal packages list. After this call, {@link #getPackages()} will return
    123      * null till load() is called.
    124      */
    125     public void clearPackages() {
    126         mPackages = null;
    127     }
    128 
    129     public String getShortDescription() {
    130         return mUrl;
    131     }
    132 
    133     public String getLongDescription() {
    134         return mDescription == null ? "" : mDescription;  //$NON-NLS-1$
    135     }
    136 
    137     /**
    138      * Returns the last fetch error description.
    139      * If there was no error, returns null.
    140      */
    141     public String getFetchError() {
    142         return mFetchError;
    143     }
    144 
    145     /**
    146      * Tries to fetch the repository index for the given URL.
    147      */
    148     public void load(ITaskMonitor monitor, boolean forceHttp) {
    149 
    150         monitor.setProgressMax(4);
    151 
    152         setDefaultDescription();
    153 
    154         String url = mUrl;
    155         if (forceHttp) {
    156             url = url.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$
    157         }
    158 
    159         monitor.setDescription("Fetching %1$s", url);
    160         monitor.incProgress(1);
    161 
    162         mFetchError = null;
    163         Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
    164         String[] validationError = new String[] { null };
    165         Exception[] exception = new Exception[] { null };
    166         ByteArrayInputStream xml = fetchUrl(url, exception);
    167         Document validatedDoc = null;
    168         boolean usingAlternateXml = false;
    169         String validatedUri = null;
    170         if (xml != null) {
    171             monitor.setDescription("Validate XML");
    172             String uri = validateXml(xml, url, validationError, validatorFound);
    173             if (uri != null) {
    174                 // Validation was successful
    175                 validatedDoc = getDocument(xml, monitor);
    176                 validatedUri = uri;
    177             } else if (validatorFound[0].equals(Boolean.TRUE)) {
    178                 // An XML validator was found and the XML failed to validate.
    179                 // Let's see if we can find an alternate tools upgrade to perform.
    180                 validatedDoc = findAlternateToolsXml(xml);
    181                 if (validatedDoc != null) {
    182                     validationError[0] = null;  // remove error from XML validation
    183                     validatedUri = SdkRepository.NS_SDK_REPOSITORY;
    184                     usingAlternateXml = true;
    185                 }
    186             } else {
    187                 // Validation failed because this JVM lacks a proper XML Validator
    188                 mFetchError = "No suitable XML Schema Validator could be found in your Java environment. Please update your version of Java.";
    189             }
    190         }
    191 
    192         // If we failed the first time and the URL doesn't explicitly end with
    193         // our filename, make another tentative after changing the URL.
    194         if (validatedDoc == null && !url.endsWith(SdkRepository.URL_DEFAULT_XML_FILE)) {
    195             if (!url.endsWith("/")) {       //$NON-NLS-1$
    196                 url += "/";                 //$NON-NLS-1$
    197             }
    198             url += SdkRepository.URL_DEFAULT_XML_FILE;
    199 
    200             xml = fetchUrl(url, exception);
    201             if (xml != null) {
    202                 String uri = validateXml(xml, url, validationError, validatorFound);
    203                 if (uri != null) {
    204                     // Validation was successful
    205                     validationError[0] = null;  // remove error from previous XML validation
    206                     validatedDoc = getDocument(xml, monitor);
    207                     validatedUri = uri;
    208                 } else if (validatorFound[0].equals(Boolean.TRUE)) {
    209                     // An XML validator was found and the XML failed to validate.
    210                     // Let's see if we can find an alternate tools upgrade to perform.
    211                     validatedDoc = findAlternateToolsXml(xml);
    212                     if (validatedDoc != null) {
    213                         validationError[0] = null;  // remove error from XML validation
    214                         validatedUri = SdkRepository.NS_SDK_REPOSITORY;
    215                         usingAlternateXml = true;
    216                     }
    217                 } else {
    218                     // Validation failed because this JVM lacks a proper XML Validator
    219                     mFetchError = "No suitable XML Schema Validator could be found in your Java environment. Please update your version of Java.";
    220                 }
    221             }
    222 
    223             if (validatedDoc != null) {
    224                 // If the second tentative succeeded, indicate it in the console
    225                 // with the URL that worked.
    226                 monitor.setResult("Repository found at %1$s", url);
    227 
    228                 // Keep the modified URL
    229                 mUrl = url;
    230             }
    231         }
    232 
    233         // If any exception was handled during the URL fetch, display it now.
    234         if (exception[0] != null) {
    235             mFetchError = "Failed to fetch URL";
    236 
    237             String reason = null;
    238             if (exception[0] instanceof FileNotFoundException) {
    239                 // FNF has no useful getMessage, so we need to special handle it.
    240                 reason = "File not found";
    241                 mFetchError += ": " + reason;
    242             } else if (exception[0] instanceof SSLKeyException) {
    243                 // That's a common error and we have a pref for it.
    244                 reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
    245                 mFetchError += ": HTTPS SSL error";
    246             } else if (exception[0].getMessage() != null) {
    247                 reason = exception[0].getMessage();
    248             } else {
    249                 // We don't know what's wrong. Let's give the exception class at least.
    250                 reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
    251             }
    252 
    253             monitor.setResult("Failed to fetch URL %1$s, reason: %2$s", url, reason);
    254         }
    255 
    256         if(validationError[0] != null) {
    257             monitor.setResult("%s", validationError[0]);  //$NON-NLS-1$
    258         }
    259 
    260         // Stop here if we failed to validate the XML. We don't want to load it.
    261         if (validatedDoc == null) {
    262             return;
    263         }
    264 
    265         if (usingAlternateXml) {
    266 
    267             // Is the manager running from inside ADT?
    268             // We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection.
    269 
    270             boolean isADT = false;
    271             try {
    272                 Class<?> adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin");  //$NON-NLS-1$
    273                 isADT = (adt != null);
    274             } catch (ClassNotFoundException e) {
    275                 // pass
    276             }
    277 
    278             String info;
    279             if (isADT) {
    280                 info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin.";
    281                 mDescription = "This repository requires a more recent version of ADT, the Eclipse Android plugin.\nYou must update it before you can see other new packages.";
    282 
    283             } else {
    284                 info = "This repository requires a more recent version of the Tools. Please update.";
    285                 mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages.";
    286             }
    287 
    288             mFetchError = mFetchError == null ? info : mFetchError + ". " + info;
    289         }
    290 
    291         monitor.incProgress(1);
    292 
    293         if (xml != null) {
    294             monitor.setDescription("Parse XML");
    295             monitor.incProgress(1);
    296             parsePackages(validatedDoc, validatedUri, monitor);
    297             if (mPackages == null || mPackages.length == 0) {
    298                 mDescription += "\nNo packages found.";
    299             } else if (mPackages.length == 1) {
    300                 mDescription += "\nOne package found.";
    301             } else {
    302                 mDescription += String.format("\n%1$d packages found.", mPackages.length);
    303             }
    304         }
    305 
    306         // done
    307         monitor.incProgress(1);
    308     }
    309 
    310     private void setDefaultDescription() {
    311         if (mUserSource) {
    312             mDescription = String.format("Add-on Source: %1$s", mUrl);
    313         } else {
    314             mDescription = String.format("SDK Source: %1$s", mUrl);
    315         }
    316     }
    317 
    318     /**
    319      * Fetches the document at the given URL and returns it as a string.
    320      * Returns null if anything wrong happens and write errors to the monitor.
    321      *
    322      * References:
    323      * Java URL Connection: http://java.sun.com/docs/books/tutorial/networking/urls/readingWriting.html
    324      * Java URL Reader: http://java.sun.com/docs/books/tutorial/networking/urls/readingURL.html
    325      * Java set Proxy: http://java.sun.com/docs/books/tutorial/networking/urls/_setProxy.html
    326      */
    327     private ByteArrayInputStream fetchUrl(String urlString, Exception[] outException) {
    328         URL url;
    329         try {
    330             url = new URL(urlString);
    331 
    332             InputStream is = null;
    333 
    334             int inc = 65536;
    335             int curr = 0;
    336             byte[] result = new byte[inc];
    337 
    338             try {
    339                 is = url.openStream();
    340 
    341                 int n;
    342                 while ((n = is.read(result, curr, result.length - curr)) != -1) {
    343                     curr += n;
    344                     if (curr == result.length) {
    345                         byte[] temp = new byte[curr + inc];
    346                         System.arraycopy(result, 0, temp, 0, curr);
    347                         result = temp;
    348                     }
    349                 }
    350 
    351                 return new ByteArrayInputStream(result, 0, curr);
    352 
    353             } finally {
    354                 if (is != null) {
    355                     try {
    356                         is.close();
    357                     } catch (IOException e) {
    358                         // pass
    359                     }
    360                 }
    361             }
    362 
    363         } catch (Exception e) {
    364             outException[0] = e;
    365         }
    366 
    367         return null;
    368     }
    369 
    370     /**
    371      * Validates this XML against one of the possible SDK Repository schema, starting
    372      * by the most recent one.
    373      * If the XML was correctly validated, returns the schema that worked.
    374      * If no schema validated the XML, returns null.
    375      */
    376     private String validateXml(ByteArrayInputStream xml, String url,
    377             String[] outError, Boolean[] validatorFound) {
    378 
    379         String lastError = null;
    380         String extraError = null;
    381         for (int version = SdkRepository.NS_LATEST_VERSION; version >= 1; version--) {
    382             try {
    383                 Validator validator = getValidator(version);
    384 
    385                 if (validator == null) {
    386                     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.";
    387                     validatorFound[0] = Boolean.FALSE;
    388                     continue;
    389                 }
    390 
    391                 validatorFound[0] = Boolean.TRUE;
    392                 xml.reset();
    393                 // Validation throws a bunch of possible Exceptions on failure.
    394                 validator.validate(new StreamSource(xml));
    395                 return SdkRepository.getSchemaUri(version);
    396 
    397             } catch (Exception e) {
    398                 lastError = "XML verification failed for %1$s.\nError: %2$s";
    399                 extraError = e.getMessage();
    400                 if (extraError == null) {
    401                     extraError = e.getClass().getName();
    402                 }
    403             }
    404         }
    405 
    406         if (lastError != null) {
    407             outError[0] = String.format(lastError, url, extraError);
    408         }
    409         return null;
    410     }
    411 
    412     /**
    413      * The purpose of this method is to support forward evolution of our schema.
    414      * <p/>
    415      * At this point, we know that xml does not point to any schema that this version of
    416      * the tool know how to process, so it's not one of the possible 1..N versions of our
    417      * XSD schema.
    418      * <p/>
    419      * We thus try to interpret the byte stream as a possible XML stream. It may not be
    420      * one at all in the first place. If it looks anything line an XML schema, we try to
    421      * find its &lt;tool&gt; elements. If we find any, we recreate a suitable document
    422      * that conforms to what we expect from our XSD schema with only those elements.
    423      * To be valid, the &lt;tool&gt; element must have at least one &lt;archive&gt;
    424      * compatible with this platform.
    425      *
    426      * If we don't find anything suitable, we drop the whole thing.
    427      *
    428      * @param xml The input XML stream. Can be null.
    429      * @return Either a new XML document conforming to our schema with at least one &lt;tool&gt;
    430      *         element or null.
    431      */
    432     protected Document findAlternateToolsXml(InputStream xml) {
    433         // Note: protected for unit-test access
    434 
    435         if (xml == null) {
    436             return null;
    437         }
    438 
    439         // Reset the stream if it supports that operation.
    440         // At runtime we use a ByteArrayInputStream which can be reset; however for unit tests
    441         // we use a FileInputStream that doesn't support resetting and is read-once.
    442         try {
    443             xml.reset();
    444         } catch (IOException e1) {
    445             // ignore if not supported
    446         }
    447 
    448         // Get an XML document
    449 
    450         Document oldDoc = null;
    451         Document newDoc = null;
    452         try {
    453             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    454             factory.setIgnoringComments(false);
    455             factory.setValidating(false);
    456 
    457             // Parse the old document using a non namespace aware builder
    458             factory.setNamespaceAware(false);
    459             DocumentBuilder builder = factory.newDocumentBuilder();
    460             oldDoc = builder.parse(xml);
    461 
    462             // Prepare a new document using a namespace aware builder
    463             factory.setNamespaceAware(true);
    464             builder = factory.newDocumentBuilder();
    465             newDoc = builder.newDocument();
    466 
    467         } catch (Exception e) {
    468             // Failed to get builder factor
    469             // Failed to create XML document builder
    470             // Failed to parse XML document
    471             // Failed to read XML document
    472         }
    473 
    474         if (oldDoc == null || newDoc == null) {
    475             return null;
    476         }
    477 
    478 
    479         // Check the root element is an xsd-schema with at least the following properties:
    480         // <sdk:sdk-repository
    481         //    xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N">
    482         //
    483         // Note that we don't have namespace support enabled, we just do it manually.
    484 
    485         Pattern nsPattern = Pattern.compile(SdkRepository.NS_SDK_REPOSITORY_PATTERN);
    486 
    487         Node oldRoot = null;
    488         String prefix = null;
    489         for (Node child = oldDoc.getFirstChild(); child != null; child = child.getNextSibling()) {
    490             if (child.getNodeType() == Node.ELEMENT_NODE) {
    491                 prefix = null;
    492                 String name = child.getNodeName();
    493                 int pos = name.indexOf(':');
    494                 if (pos > 0 && pos < name.length() - 1) {
    495                     prefix = name.substring(0, pos);
    496                     name = name.substring(pos + 1);
    497                 }
    498                 if (SdkRepository.NODE_SDK_REPOSITORY.equals(name)) {
    499                     NamedNodeMap attrs = child.getAttributes();
    500                     String xmlns = "xmlns";                                         //$NON-NLS-1$
    501                     if (prefix != null) {
    502                         xmlns += ":" + prefix;                                      //$NON-NLS-1$
    503                     }
    504                     Node attr = attrs.getNamedItem(xmlns);
    505                     if (attr != null) {
    506                         String uri = attr.getNodeValue();
    507                         if (uri != null && nsPattern.matcher(uri).matches()) {
    508                             oldRoot = child;
    509                             break;
    510                         }
    511                     }
    512                 }
    513             }
    514         }
    515 
    516         // we must have found the root node, and it must have an XML namespace prefix.
    517         if (oldRoot == null || prefix == null || prefix.length() == 0) {
    518             return null;
    519         }
    520 
    521         final String ns = SdkRepository.NS_SDK_REPOSITORY;
    522         Element newRoot = newDoc.createElementNS(ns, SdkRepository.NODE_SDK_REPOSITORY);
    523         newRoot.setPrefix(prefix);
    524         newDoc.appendChild(newRoot);
    525         int numTool = 0;
    526 
    527         // Find an inner <tool> node and extract its required parameters
    528 
    529         Node tool = null;
    530         while ((tool = findChild(oldRoot, tool, prefix, SdkRepository.NODE_TOOL)) != null) {
    531             // To be valid, the tool element must have:
    532             // - a <revision> element with a number
    533             // - an optional <uses-license> node, which we'll skip right now.
    534             //   (if we add it later, we must find the license declaration element too)
    535             // - an <archives> element with one or more <archive> elements inside
    536             // - one of the <archive> elements must have an "os" and "arch" attributes
    537             //   compatible with the current platform. Only keep the first such element found.
    538             // - the <archive> element must contain a <size>, a <checksum> and a <url>.
    539 
    540             try {
    541                 Node revision = findChild(tool, null, prefix, SdkRepository.NODE_REVISION);
    542                 Node archives = findChild(tool, null, prefix, SdkRepository.NODE_ARCHIVES);
    543 
    544                 if (revision == null || archives == null) {
    545                     continue;
    546                 }
    547 
    548                 int rev = 0;
    549                 try {
    550                     String content = revision.getTextContent();
    551                     content = content.trim();
    552                     rev = Integer.parseInt(content);
    553                     if (rev < 1) {
    554                         continue;
    555                     }
    556                 } catch (NumberFormatException ignore) {
    557                     continue;
    558                 }
    559 
    560                 Element newTool = newDoc.createElementNS(ns, SdkRepository.NODE_TOOL);
    561                 newTool.setPrefix(prefix);
    562                 appendChild(newTool, ns, prefix,
    563                         SdkRepository.NODE_REVISION, Integer.toString(rev));
    564                 Element newArchives = appendChild(newTool, ns, prefix,
    565                                                   SdkRepository.NODE_ARCHIVES, null);
    566                 int numArchives = 0;
    567 
    568                 Node archive = null;
    569                 while ((archive = findChild(archives,
    570                                             archive,
    571                                             prefix,
    572                                             SdkRepository.NODE_ARCHIVE)) != null) {
    573                     try {
    574                         Os os = (Os) XmlParserUtils.getEnumAttribute(archive,
    575                                 SdkRepository.ATTR_OS,
    576                                 Os.values(),
    577                                 null /*default*/);
    578                         Arch arch = (Arch) XmlParserUtils.getEnumAttribute(archive,
    579                                 SdkRepository.ATTR_ARCH,
    580                                 Arch.values(),
    581                                 Arch.ANY);
    582                         if (os == null || !os.isCompatible() ||
    583                                 arch == null || !arch.isCompatible()) {
    584                             continue;
    585                         }
    586 
    587                         Node node = findChild(archive, null, prefix, SdkRepository.NODE_URL);
    588                         String url = node == null ? null : node.getTextContent().trim();
    589                         if (url == null || url.length() == 0) {
    590                             continue;
    591                         }
    592 
    593                         node = findChild(archive, null, prefix, SdkRepository.NODE_SIZE);
    594                         long size = 0;
    595                         try {
    596                             size = Long.parseLong(node.getTextContent());
    597                         } catch (Exception e) {
    598                             // pass
    599                         }
    600                         if (size < 1) {
    601                             continue;
    602                         }
    603 
    604                         node = findChild(archive, null, prefix, SdkRepository.NODE_CHECKSUM);
    605                         // double check that the checksum element contains a type=sha1 attribute
    606                         if (node == null) {
    607                             continue;
    608                         }
    609                         NamedNodeMap attrs = node.getAttributes();
    610                         Node typeNode = attrs.getNamedItem(SdkRepository.ATTR_TYPE);
    611                         if (typeNode == null ||
    612                                 !SdkRepository.ATTR_TYPE.equals(typeNode.getNodeName()) ||
    613                                 !SdkRepository.SHA1_TYPE.equals(typeNode.getNodeValue())) {
    614                             continue;
    615                         }
    616                         String sha1 = node == null ? null : node.getTextContent().trim();
    617                         if (sha1 == null || sha1.length() != SdkRepository.SHA1_CHECKSUM_LEN) {
    618                             continue;
    619                         }
    620 
    621                         // Use that archive for the new tool element
    622                         Element ar = appendChild(newArchives, ns, prefix,
    623                                                  SdkRepository.NODE_ARCHIVE, null);
    624                         ar.setAttributeNS(ns, SdkRepository.ATTR_OS, os.getXmlName());
    625                         ar.setAttributeNS(ns, SdkRepository.ATTR_ARCH, arch.getXmlName());
    626 
    627                         appendChild(ar, ns, prefix, SdkRepository.NODE_URL, url);
    628                         appendChild(ar, ns, prefix, SdkRepository.NODE_SIZE, Long.toString(size));
    629                         Element cs = appendChild(ar, ns, prefix, SdkRepository.NODE_CHECKSUM, sha1);
    630                         cs.setAttributeNS(ns, SdkRepository.ATTR_TYPE, SdkRepository.SHA1_TYPE);
    631 
    632                         numArchives++;
    633 
    634                     } catch (Exception ignore1) {
    635                         // pass
    636                     }
    637                 } // while <archive>
    638 
    639                 if (numArchives > 0) {
    640                     newRoot.appendChild(newTool);
    641                     numTool++;
    642                 }
    643             } catch (Exception ignore2) {
    644                 // pass
    645             }
    646         } // while <tool>
    647 
    648         return numTool > 0 ? newDoc : null;
    649     }
    650 
    651     /**
    652      * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given
    653      * element child in a root XML node.
    654      */
    655     private Node findChild(Node rootNode, Node after, String prefix, String nodeName) {
    656         nodeName = prefix + ":" + nodeName;
    657         Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling();
    658         for(; child != null; child = child.getNextSibling()) {
    659             if (child.getNodeType() == Node.ELEMENT_NODE && nodeName.equals(child.getNodeName())) {
    660                 return child;
    661             }
    662         }
    663         return null;
    664     }
    665 
    666     /**
    667      * Helper method used by {@link #findAlternateToolsXml(InputStream)} to create a new
    668      * XML element into a parent element.
    669      */
    670     private Element appendChild(Element rootNode, String namespaceUri,
    671             String prefix, String nodeName,
    672             String nodeValue) {
    673         Element node = rootNode.getOwnerDocument().createElementNS(namespaceUri, nodeName);
    674         node.setPrefix(prefix);
    675         if (nodeValue != null) {
    676             node.setTextContent(nodeValue);
    677         }
    678         rootNode.appendChild(node);
    679         return node;
    680     }
    681 
    682 
    683     /**
    684      * Helper method that returns a validator for our XSD, or null if the current Java
    685      * implementation can't process XSD schemas.
    686      *
    687      * @param version The version of the XML Schema.
    688      *        See {@link SdkRepository#getXsdStream(int)}
    689      */
    690     private Validator getValidator(int version) throws SAXException {
    691         InputStream xsdStream = SdkRepository.getXsdStream(version);
    692         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    693 
    694         if (factory == null) {
    695             return null;
    696         }
    697 
    698         // This may throw a SAX Exception if the schema itself is not a valid XSD
    699         Schema schema = factory.newSchema(new StreamSource(xsdStream));
    700 
    701         Validator validator = schema == null ? null : schema.newValidator();
    702 
    703         return validator;
    704     }
    705 
    706 
    707     /**
    708      * Parse all packages defined in the SDK Repository XML and creates
    709      * a new mPackages array with them.
    710      */
    711     protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) {
    712         // protected for unit-test acces
    713 
    714         assert doc != null;
    715 
    716         Node root = getFirstChild(doc, nsUri, SdkRepository.NODE_SDK_REPOSITORY);
    717         if (root != null) {
    718 
    719             ArrayList<Package> packages = new ArrayList<Package>();
    720 
    721             // Parse license definitions
    722             HashMap<String, String> licenses = new HashMap<String, String>();
    723             for (Node child = root.getFirstChild();
    724                  child != null;
    725                  child = child.getNextSibling()) {
    726                 if (child.getNodeType() == Node.ELEMENT_NODE &&
    727                         nsUri.equals(child.getNamespaceURI()) &&
    728                         child.getLocalName().equals(SdkRepository.NODE_LICENSE)) {
    729                     Node id = child.getAttributes().getNamedItem(SdkRepository.ATTR_ID);
    730                     if (id != null) {
    731                         licenses.put(id.getNodeValue(), child.getTextContent());
    732                     }
    733                 }
    734             }
    735 
    736             // Parse packages
    737             for (Node child = root.getFirstChild();
    738                  child != null;
    739                  child = child.getNextSibling()) {
    740                 if (child.getNodeType() == Node.ELEMENT_NODE &&
    741                         nsUri.equals(child.getNamespaceURI())) {
    742                     String name = child.getLocalName();
    743                     Package p = null;
    744 
    745                     try {
    746                         // We can load addon and extra packages from all sources, either
    747                         // internal or user sources.
    748                         if (SdkRepository.NODE_ADD_ON.equals(name)) {
    749                             p = new AddonPackage(this, child, licenses);
    750 
    751                         } else if (SdkRepository.NODE_EXTRA.equals(name)) {
    752                             p = new ExtraPackage(this, child, licenses);
    753 
    754                         } else if (!mUserSource) {
    755                             // We only load platform, doc and tool packages from internal
    756                             // sources, never from user sources.
    757                             if (SdkRepository.NODE_PLATFORM.equals(name)) {
    758                                 p = new PlatformPackage(this, child, licenses);
    759                             } else if (SdkRepository.NODE_DOC.equals(name)) {
    760                                 p = new DocPackage(this, child, licenses);
    761                             } else if (SdkRepository.NODE_TOOL.equals(name)) {
    762                                 p = new ToolPackage(this, child, licenses);
    763                             } else if (SdkRepository.NODE_SAMPLE.equals(name)) {
    764                                 p = new SamplePackage(this, child, licenses);
    765                             }
    766                         }
    767 
    768                         if (p != null) {
    769                             packages.add(p);
    770                             monitor.setDescription("Found %1$s", p.getShortDescription());
    771                         }
    772                     } catch (Exception e) {
    773                         // Ignore invalid packages
    774                     }
    775                 }
    776             }
    777 
    778             mPackages = packages.toArray(new Package[packages.size()]);
    779 
    780             // Order the packages.
    781             Arrays.sort(mPackages, null);
    782 
    783             return true;
    784         }
    785 
    786         return false;
    787     }
    788 
    789     /**
    790      * Returns the first child element with the given XML local name.
    791      * If xmlLocalName is null, returns the very first child element.
    792      */
    793     private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
    794 
    795         for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
    796             if (child.getNodeType() == Node.ELEMENT_NODE &&
    797                     nsUri.equals(child.getNamespaceURI())) {
    798                 if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
    799                     return child;
    800                 }
    801             }
    802         }
    803 
    804         return null;
    805     }
    806 
    807     /**
    808      * Takes an XML document as a string as parameter and returns a DOM for it.
    809      *
    810      * On error, returns null and prints a (hopefully) useful message on the monitor.
    811      */
    812     private Document getDocument(ByteArrayInputStream xml, ITaskMonitor monitor) {
    813         try {
    814             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    815             factory.setIgnoringComments(true);
    816             factory.setNamespaceAware(true);
    817 
    818             DocumentBuilder builder = factory.newDocumentBuilder();
    819             xml.reset();
    820             Document doc = builder.parse(new InputSource(xml));
    821 
    822             return doc;
    823         } catch (ParserConfigurationException e) {
    824             monitor.setResult("Failed to create XML document builder");
    825 
    826         } catch (SAXException e) {
    827             monitor.setResult("Failed to parse XML document");
    828 
    829         } catch (IOException e) {
    830             monitor.setResult("Failed to read XML document");
    831         }
    832 
    833         return null;
    834     }
    835 }
    836