• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.internal.repository;
18 
19 import com.android.annotations.Nullable;
20 import com.android.annotations.VisibleForTesting;
21 import com.android.annotations.VisibleForTesting.Visibility;
22 import com.android.sdklib.internal.repository.UrlOpener.CanceledByUserException;
23 import com.android.sdklib.repository.RepoConstants;
24 import com.android.sdklib.repository.SdkAddonConstants;
25 import com.android.sdklib.repository.SdkRepoConstants;
26 
27 import org.w3c.dom.Document;
28 import org.w3c.dom.NamedNodeMap;
29 import org.w3c.dom.Node;
30 import org.xml.sax.ErrorHandler;
31 import org.xml.sax.InputSource;
32 import org.xml.sax.SAXException;
33 import org.xml.sax.SAXParseException;
34 
35 import java.io.ByteArrayInputStream;
36 import java.io.FileNotFoundException;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.net.MalformedURLException;
40 import java.net.URL;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 
47 import javax.net.ssl.SSLKeyException;
48 import javax.xml.XMLConstants;
49 import javax.xml.parsers.DocumentBuilder;
50 import javax.xml.parsers.DocumentBuilderFactory;
51 import javax.xml.parsers.ParserConfigurationException;
52 import javax.xml.transform.stream.StreamSource;
53 import javax.xml.validation.Schema;
54 import javax.xml.validation.SchemaFactory;
55 import javax.xml.validation.Validator;
56 
57 /**
58  * An sdk-addon or sdk-repository source, i.e. a download site.
59  * It may be a full repository or an add-on only repository.
60  * A repository describes one or {@link Package}s available for download.
61  */
62 public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
63 
64     private String mUrl;
65 
66     private Package[] mPackages;
67     private String mDescription;
68     private String mFetchError;
69     private final String mUiName;
70 
71     /**
72      * Constructs a new source for the given repository URL.
73      * @param url The source URL. Cannot be null. If the URL ends with a /, the default
74      *            repository.xml filename will be appended automatically.
75      * @param uiName The UI-visible name of the source. Can be null.
76      */
SdkSource(String url, String uiName)77     public SdkSource(String url, String uiName) {
78 
79         // URLs should not be null and should not have whitespace.
80         if (url == null) {
81             url = "";
82         }
83         url = url.trim();
84 
85         // if the URL ends with a /, it must be "directory" resource,
86         // in which case we automatically add the default file that will
87         // looked for. This way it will be obvious to the user which
88         // resource we are actually trying to fetch.
89         if (url.endsWith("/")) {  //$NON-NLS-1$
90             String[] names = getDefaultXmlFileUrls();
91             if (names.length > 0) {
92                 url += names[0];
93             }
94         }
95 
96         mUrl = url;
97         mUiName = uiName;
98         setDefaultDescription();
99     }
100 
101     /**
102      * Returns true if this is an addon source.
103      * We only load addons and extras from these sources.
104      */
isAddonSource()105     public abstract boolean isAddonSource();
106 
107     /**
108      * Returns the basename of the default URLs to try to download the
109      * XML manifest.
110      * E.g. this is typically SdkRepoConstants.URL_DEFAULT_XML_FILE
111      * or SdkAddonConstants.URL_DEFAULT_XML_FILE
112      */
getDefaultXmlFileUrls()113     protected abstract String[] getDefaultXmlFileUrls();
114 
115     /** Returns SdkRepoConstants.NS_LATEST_VERSION or SdkAddonConstants.NS_LATEST_VERSION. */
getNsLatestVersion()116     protected abstract int getNsLatestVersion();
117 
118     /** Returns SdkRepoConstants.NS_URI or SdkAddonConstants.NS_URI. */
getNsUri()119     protected abstract String getNsUri();
120 
121     /** Returns SdkRepoConstants.NS_PATTERN or SdkAddonConstants.NS_PATTERN. */
getNsPattern()122     protected abstract String getNsPattern();
123 
124     /** Returns SdkRepoConstants.getSchemaUri() or SdkAddonConstants.getSchemaUri(). */
getSchemaUri(int version)125     protected abstract String getSchemaUri(int version);
126 
127     /* Returns SdkRepoConstants.NODE_SDK_REPOSITORY or SdkAddonConstants.NODE_SDK_ADDON. */
getRootElementName()128     protected abstract String getRootElementName();
129 
130     /** Returns SdkRepoConstants.getXsdStream() or SdkAddonConstants.getXsdStream(). */
getXsdStream(int version)131     protected abstract InputStream getXsdStream(int version);
132 
133     /**
134      * In case we fail to load an XML, examine the XML to see if it matches a <b>future</b>
135      * schema that as at least a <code>tools</code> node that we could load to update the
136      * SDK Manager.
137      *
138      * @param xml The input XML stream. Can be null.
139      * @return Null on failure, otherwise returns an XML DOM with just the tools we
140      *   need to update this SDK Manager.
141      * @null Can return null on failure.
142      */
findAlternateToolsXml(@ullable InputStream xml)143     protected abstract Document findAlternateToolsXml(@Nullable InputStream xml)
144         throws IOException;
145 
146     /**
147      * Two repo source are equal if they have the same URL.
148      */
149     @Override
equals(Object obj)150     public boolean equals(Object obj) {
151         if (obj instanceof SdkSource) {
152             SdkSource rs = (SdkSource) obj;
153             return  rs.getUrl().equals(this.getUrl());
154         }
155         return false;
156     }
157 
158     @Override
hashCode()159     public int hashCode() {
160         return mUrl.hashCode();
161     }
162 
163     /**
164      * Implementation of the {@link Comparable} interface.
165      * Simply compares the URL using the string's default ordering.
166      */
compareTo(SdkSource rhs)167     public int compareTo(SdkSource rhs) {
168         return this.getUrl().compareTo(rhs.getUrl());
169     }
170 
171     /**
172      * Returns the UI-visible name of the source. Can be null.
173      */
getUiName()174     public String getUiName() {
175         return mUiName;
176     }
177 
178     /** Returns the URL of the XML file for this source. */
getUrl()179     public String getUrl() {
180         return mUrl;
181     }
182 
183     /**
184      * Returns the list of known packages found by the last call to load().
185      * This is null when the source hasn't been loaded yet.
186      */
getPackages()187     public Package[] getPackages() {
188         return mPackages;
189     }
190 
191     @VisibleForTesting(visibility=Visibility.PRIVATE)
setPackages(Package[] packages)192     protected void setPackages(Package[] packages) {
193         mPackages = packages;
194 
195         if (mPackages != null) {
196             // Order the packages.
197             Arrays.sort(mPackages, null);
198         }
199     }
200 
201     /**
202      * Clear the internal packages list. After this call, {@link #getPackages()} will return
203      * null till load() is called.
204      */
clearPackages()205     public void clearPackages() {
206         setPackages(null);
207     }
208 
209     /**
210      * Returns the short description of the source, if not null.
211      * Otherwise returns the default Object toString result.
212      * <p/>
213      * This is mostly helpful for debugging.
214      * For UI display, use the {@link IDescription} interface.
215      */
216     @Override
toString()217     public String toString() {
218         String s = getShortDescription();
219         if (s != null) {
220             return s;
221         }
222         return super.toString();
223     }
224 
getShortDescription()225     public String getShortDescription() {
226 
227         if (mUiName != null && mUiName.length() > 0) {
228 
229             String host = "malformed URL";
230 
231             try {
232                 URL u = new URL(mUrl);
233                 host = u.getHost();
234             } catch (MalformedURLException e) {
235             }
236 
237             return String.format("%1$s (%2$s)", mUiName, host);
238 
239         }
240         return mUrl;
241     }
242 
getLongDescription()243     public String getLongDescription() {
244         // Note: in a normal workflow, mDescription is filled by setDefaultDescription().
245         // However for packages made by unit tests or such, this can be null.
246         return mDescription == null ? "" : mDescription;  //$NON-NLS-1$
247     }
248 
249     /**
250      * Returns the last fetch error description.
251      * If there was no error, returns null.
252      */
getFetchError()253     public String getFetchError() {
254         return mFetchError;
255     }
256 
257     /**
258      * Tries to fetch the repository index for the given URL.
259      */
load(ITaskMonitor monitor, boolean forceHttp)260     public void load(ITaskMonitor monitor, boolean forceHttp) {
261 
262         monitor.setProgressMax(7);
263 
264         setDefaultDescription();
265 
266         String url = mUrl;
267         if (forceHttp) {
268             url = url.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$
269         }
270 
271         monitor.setDescription("Fetching URL: %1$s", url);
272         monitor.incProgress(1);
273 
274         mFetchError = null;
275         Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
276         String[] validationError = new String[] { null };
277         Exception[] exception = new Exception[] { null };
278         Document validatedDoc = null;
279         boolean usingAlternateXml = false;
280         boolean usingAlternateUrl = false;
281         String validatedUri = null;
282 
283         String[] defaultNames = getDefaultXmlFileUrls();
284         String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : "";
285 
286         InputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
287         if (xml != null) {
288             int version = getXmlSchemaVersion(xml);
289             if (version == 0) {
290                 xml = null;
291             }
292         }
293 
294         // FIXME: this is a quick fix to support an alternate upgrade path.
295         // The whole logic below needs to be updated.
296         if (xml == null && defaultNames.length > 0) {
297             ITaskMonitor subMonitor = monitor.createSubMonitor(1);
298             subMonitor.setProgressMax(defaultNames.length);
299 
300             String baseUrl = url;
301             if (!baseUrl.endsWith("/")) {
302                 int pos = baseUrl.lastIndexOf('/');
303                 if (pos > 0) {
304                     baseUrl = baseUrl.substring(0, pos + 1);
305                 }
306             }
307 
308             for(String name : defaultNames) {
309                 String newUrl = baseUrl + name;
310                 if (newUrl.equals(url)) {
311                     continue;
312                 }
313                 xml = fetchUrl(newUrl, subMonitor.createSubMonitor(1), exception);
314                 if (xml != null) {
315                     int version = getXmlSchemaVersion(xml);
316                     if (version == 0) {
317                         xml = null;
318                     } else {
319                         url = newUrl;
320                         subMonitor.incProgress(
321                                 subMonitor.getProgressMax() - subMonitor.getProgress());
322                         break;
323                     }
324                 }
325             }
326         } else {
327             monitor.incProgress(1);
328         }
329 
330         // If the original URL can't be fetched
331         // and the URL doesn't explicitly end with our filename
332         // and it wasn't an HTTP authentication operation canceled by the user
333         // then make another tentative after changing the URL.
334         if (xml == null
335                 && !url.endsWith(firstDefaultName)
336                 && !(exception[0] instanceof CanceledByUserException)) {
337             if (!url.endsWith("/")) {       //$NON-NLS-1$
338                 url += "/";                 //$NON-NLS-1$
339             }
340             url += firstDefaultName;
341 
342             xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
343             usingAlternateUrl = true;
344         } else {
345             monitor.incProgress(1);
346         }
347 
348         // FIXME this needs to revisited.
349         if (xml != null) {
350             monitor.setDescription("Validate XML: %1$s", url);
351 
352             ITaskMonitor subMonitor = monitor.createSubMonitor(2);
353             subMonitor.setProgressMax(2);
354             for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) {
355                 // Explore the XML to find the potential XML schema version
356                 int version = getXmlSchemaVersion(xml);
357 
358                 if (version >= 1 && version <= getNsLatestVersion()) {
359                     // This should be a version we can handle. Try to validate it
360                     // and report any error as invalid XML syntax,
361 
362                     String uri = validateXml(xml, url, version, validationError, validatorFound);
363                     if (uri != null) {
364                         // Validation was successful
365                         validatedDoc = getDocument(xml, monitor);
366                         validatedUri = uri;
367 
368                         if (usingAlternateUrl && validatedDoc != null) {
369                             // If the second tentative succeeded, indicate it in the console
370                             // with the URL that worked.
371                             monitor.log("Repository found at %1$s", url);
372 
373                             // Keep the modified URL
374                             mUrl = url;
375                         }
376                     } else if (validatorFound[0].equals(Boolean.FALSE)) {
377                         // Validation failed because this JVM lacks a proper XML Validator
378                         mFetchError = validationError[0];
379                     } else {
380                         // We got a validator but validation failed. We know there's
381                         // what looks like a suitable root element with a suitable XMLNS
382                         // so it must be a genuine error of an XML not conforming to the schema.
383                     }
384                 } else if (version > getNsLatestVersion()) {
385                     // The schema used is more recent than what is supported by this tool.
386                     // Tell the user to upgrade, pointing him to the right version of the tool
387                     // package.
388 
389                     try {
390                         validatedDoc = findAlternateToolsXml(xml);
391                     } catch (IOException e) {
392                         // Failed, will be handled below.
393                     }
394                     if (validatedDoc != null) {
395                         validationError[0] = null;  // remove error from XML validation
396                         validatedUri = getNsUri();
397                         usingAlternateXml = true;
398                     }
399 
400                 } else if (version < 1 && tryOtherUrl == 0 && !usingAlternateUrl) {
401                     // This is obviously not one of our documents.
402                     mFetchError = String.format(
403                             "Failed to validate the XML for the repository at URL '%1$s'",
404                             url);
405 
406                     // If we haven't already tried the alternate URL, let's do it now.
407                     // We don't capture any fetch exception that happen during the second
408                     // fetch in order to avoid hidding any previous fetch errors.
409                     if (!url.endsWith(firstDefaultName)) {
410                         if (!url.endsWith("/")) {       //$NON-NLS-1$
411                             url += "/";                 //$NON-NLS-1$
412                         }
413                         url += firstDefaultName;
414 
415                         xml = fetchUrl(url, subMonitor.createSubMonitor(1), null /* outException */);
416                         subMonitor.incProgress(1);
417                         // Loop to try the alternative document
418                         if (xml != null) {
419                             usingAlternateUrl = true;
420                             continue;
421                         }
422                     }
423                 } else if (version < 1 && usingAlternateUrl && mFetchError == null) {
424                     // The alternate URL is obviously not a valid XML either.
425                     // We only report the error if we failed to produce one earlier.
426                     mFetchError = String.format(
427                             "Failed to validate the XML for the repository at URL '%1$s'",
428                             url);
429                 }
430 
431                 // If we get here either we succeeded or we ran out of alternatives.
432                 break;
433             }
434         }
435 
436         // If any exception was handled during the URL fetch, display it now.
437         if (exception[0] != null) {
438             mFetchError = "Failed to fetch URL";
439 
440             String reason = null;
441             if (exception[0] instanceof FileNotFoundException) {
442                 // FNF has no useful getMessage, so we need to special handle it.
443                 reason = "File not found";
444                 mFetchError += ": " + reason;
445             } else if (exception[0] instanceof SSLKeyException) {
446                 // That's a common error and we have a pref for it.
447                 reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
448                 mFetchError += ": HTTPS SSL error";
449             } else if (exception[0].getMessage() != null) {
450                 reason = exception[0].getMessage();
451             } else {
452                 // We don't know what's wrong. Let's give the exception class at least.
453                 reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
454             }
455 
456             monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason);
457         }
458 
459         if (validationError[0] != null) {
460             monitor.logError("%s", validationError[0]);  //$NON-NLS-1$
461         }
462 
463         // Stop here if we failed to validate the XML. We don't want to load it.
464         if (validatedDoc == null) {
465             return;
466         }
467 
468         if (usingAlternateXml) {
469             // We found something using the "alternate" XML schema (that is the one made up
470             // to support schema upgrades). That means the user can only install the tools
471             // and needs to upgrade them before it download more stuff.
472 
473             // Is the manager running from inside ADT?
474             // We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection.
475 
476             boolean isADT = false;
477             try {
478                 Class<?> adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin");  //$NON-NLS-1$
479                 isADT = (adt != null);
480             } catch (ClassNotFoundException e) {
481                 // pass
482             }
483 
484             String info;
485             if (isADT) {
486                 info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin.";
487                 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.";
488 
489             } else {
490                 info = "This repository requires a more recent version of the Tools. Please update.";
491                 mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages.";
492             }
493 
494             mFetchError = mFetchError == null ? info : mFetchError + ". " + info;
495         }
496 
497         monitor.incProgress(1);
498 
499         if (xml != null) {
500             monitor.setDescription("Parse XML:    %1$s", url);
501             monitor.incProgress(1);
502             parsePackages(validatedDoc, validatedUri, monitor);
503             if (mPackages == null || mPackages.length == 0) {
504                 mDescription += "\nNo packages found.";
505             } else if (mPackages.length == 1) {
506                 mDescription += "\nOne package found.";
507             } else {
508                 mDescription += String.format("\n%1$d packages found.", mPackages.length);
509             }
510         }
511 
512         // done
513         monitor.incProgress(1);
514     }
515 
setDefaultDescription()516     private void setDefaultDescription() {
517         if (isAddonSource()) {
518             String desc = "";
519 
520             if (mUiName != null) {
521                 desc += "Add-on Provider: " + mUiName;
522                 desc += "\n";
523             }
524             desc += "Add-on URL: " + mUrl;
525 
526             mDescription = desc;
527         } else {
528             mDescription = String.format("SDK Source: %1$s", mUrl);
529         }
530     }
531 
532     /**
533      * Fetches the document at the given URL and returns it as a string. Returns
534      * null if anything wrong happens and write errors to the monitor.
535      * References: <br/>
536      * URL Connection:
537      *
538      * @param urlString The URL to load, as a string.
539      * @param monitor {@link ITaskMonitor} related to this URL.
540      * @param outException If non null, where to store any exception that
541      *            happens during the fetch.
542      * @see UrlOpener UrlOpener, which handles all URL logic.
543      */
fetchUrl(String urlString, ITaskMonitor monitor, Exception[] outException)544     private InputStream fetchUrl(String urlString, ITaskMonitor monitor, Exception[] outException) {
545         try {
546 
547             InputStream is = null;
548 
549             int inc = 65536;
550             int curr = 0;
551             byte[] result = new byte[inc];
552 
553             try {
554                 is = UrlOpener.openUrl(urlString, monitor);
555 
556                 int n;
557                 while ((n = is.read(result, curr, result.length - curr)) != -1) {
558                     curr += n;
559                     if (curr == result.length) {
560                         byte[] temp = new byte[curr + inc];
561                         System.arraycopy(result, 0, temp, 0, curr);
562                         result = temp;
563                     }
564                 }
565 
566                 return new ByteArrayInputStream(result, 0, curr);
567 
568             } finally {
569                 if (is != null) {
570                     try {
571                         is.close();
572                     } catch (IOException e) {
573                         // pass
574                     }
575                 }
576             }
577 
578         } catch (Exception e) {
579             if (outException != null) {
580                 outException[0] = e;
581             }
582         }
583 
584         return null;
585     }
586 
587     /**
588      * Validates this XML against one of the requested SDK Repository schemas.
589      * If the XML was correctly validated, returns the schema that worked.
590      * If it doesn't validate, returns null and stores the error in outError[0].
591      * If we can't find a validator, returns null and set validatorFound[0] to false.
592      */
593     @VisibleForTesting(visibility=Visibility.PRIVATE)
validateXml(InputStream xml, String url, int version, String[] outError, Boolean[] validatorFound)594     protected String validateXml(InputStream xml, String url, int version,
595             String[] outError, Boolean[] validatorFound) {
596 
597         if (xml == null) {
598             return null;
599         }
600 
601         try {
602             Validator validator = getValidator(version);
603 
604             if (validator == null) {
605                 validatorFound[0] = Boolean.FALSE;
606                 outError[0] = String.format(
607                         "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.",
608                         url);
609                 return null;
610             }
611 
612             validatorFound[0] = Boolean.TRUE;
613 
614             // Reset the stream if it supports that operation.
615             xml.reset();
616 
617             // Validation throws a bunch of possible Exceptions on failure.
618             validator.validate(new StreamSource(xml));
619             return getSchemaUri(version);
620 
621         } catch (SAXParseException e) {
622             outError[0] = String.format(
623                     "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s",
624                     url,
625                     e.getLineNumber(),
626                     e.getColumnNumber(),
627                     e.toString());
628 
629         } catch (Exception e) {
630             outError[0] = String.format(
631                     "XML verification failed for %1$s.\nError: %2$s",
632                     url,
633                     e.toString());
634         }
635         return null;
636     }
637 
638     /**
639      * Manually parses the root element of the XML to extract the schema version
640      * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N"
641      * declaration.
642      *
643      * @return 1..{@link SdkRepoConstants#NS_LATEST_VERSION} for a valid schema version
644      *         or 0 if no schema could be found.
645      */
646     @VisibleForTesting(visibility=Visibility.PRIVATE)
getXmlSchemaVersion(InputStream xml)647     protected int getXmlSchemaVersion(InputStream xml) {
648         if (xml == null) {
649             return 0;
650         }
651 
652         // Get an XML document
653         Document doc = null;
654         try {
655             xml.reset();
656 
657             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
658             factory.setIgnoringComments(false);
659             factory.setValidating(false);
660 
661             // Parse the old document using a non namespace aware builder
662             factory.setNamespaceAware(false);
663             DocumentBuilder builder = factory.newDocumentBuilder();
664 
665             // We don't want the default handler which prints errors to stderr.
666             builder.setErrorHandler(new ErrorHandler() {
667                 public void warning(SAXParseException e) throws SAXException {
668                     // pass
669                 }
670                 public void fatalError(SAXParseException e) throws SAXException {
671                     throw e;
672                 }
673                 public void error(SAXParseException e) throws SAXException {
674                     throw e;
675                 }
676             });
677 
678             doc = builder.parse(xml);
679 
680             // Prepare a new document using a namespace aware builder
681             factory.setNamespaceAware(true);
682             builder = factory.newDocumentBuilder();
683 
684         } catch (Exception e) {
685             // Failed to reset XML stream
686             // Failed to get builder factor
687             // Failed to create XML document builder
688             // Failed to parse XML document
689             // Failed to read XML document
690         }
691 
692         if (doc == null) {
693             return 0;
694         }
695 
696         // Check the root element is an XML with at least the following properties:
697         // <sdk:sdk-repository
698         //    xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N">
699         //
700         // Note that we don't have namespace support enabled, we just do it manually.
701 
702         Pattern nsPattern = Pattern.compile(getNsPattern());
703 
704         String prefix = null;
705         for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
706             if (child.getNodeType() == Node.ELEMENT_NODE) {
707                 prefix = null;
708                 String name = child.getNodeName();
709                 int pos = name.indexOf(':');
710                 if (pos > 0 && pos < name.length() - 1) {
711                     prefix = name.substring(0, pos);
712                     name = name.substring(pos + 1);
713                 }
714                 if (getRootElementName().equals(name)) {
715                     NamedNodeMap attrs = child.getAttributes();
716                     String xmlns = "xmlns";                                         //$NON-NLS-1$
717                     if (prefix != null) {
718                         xmlns += ":" + prefix;                                      //$NON-NLS-1$
719                     }
720                     Node attr = attrs.getNamedItem(xmlns);
721                     if (attr != null) {
722                         String uri = attr.getNodeValue();
723                         if (uri != null) {
724                             Matcher m = nsPattern.matcher(uri);
725                             if (m.matches()) {
726                                 String version = m.group(1);
727                                 try {
728                                     return Integer.parseInt(version);
729                                 } catch (NumberFormatException e) {
730                                     return 0;
731                                 }
732                             }
733                         }
734                     }
735                 }
736             }
737         }
738 
739         return 0;
740     }
741 
742     /**
743      * Helper method that returns a validator for our XSD, or null if the current Java
744      * implementation can't process XSD schemas.
745      *
746      * @param version The version of the XML Schema.
747      *        See {@link SdkRepoConstants#getXsdStream(int)}
748      */
getValidator(int version)749     private Validator getValidator(int version) throws SAXException {
750         InputStream xsdStream = getXsdStream(version);
751         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
752 
753         if (factory == null) {
754             return null;
755         }
756 
757         // This may throw a SAX Exception if the schema itself is not a valid XSD
758         Schema schema = factory.newSchema(new StreamSource(xsdStream));
759 
760         Validator validator = schema == null ? null : schema.newValidator();
761 
762         // We don't want the default handler, which by default dumps errors to stderr.
763         validator.setErrorHandler(new ErrorHandler() {
764             public void warning(SAXParseException e) throws SAXException {
765                 // pass
766             }
767             public void fatalError(SAXParseException e) throws SAXException {
768                 throw e;
769             }
770             public void error(SAXParseException e) throws SAXException {
771                 throw e;
772             }
773         });
774 
775         return validator;
776     }
777 
778     /**
779      * Parse all packages defined in the SDK Repository XML and creates
780      * a new mPackages array with them.
781      */
782     @VisibleForTesting(visibility=Visibility.PRIVATE)
parsePackages(Document doc, String nsUri, ITaskMonitor monitor)783     protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) {
784 
785         Node root = getFirstChild(doc, nsUri, getRootElementName());
786         if (root != null) {
787 
788             ArrayList<Package> packages = new ArrayList<Package>();
789 
790             // Parse license definitions
791             HashMap<String, String> licenses = new HashMap<String, String>();
792             for (Node child = root.getFirstChild();
793                  child != null;
794                  child = child.getNextSibling()) {
795                 if (child.getNodeType() == Node.ELEMENT_NODE &&
796                         nsUri.equals(child.getNamespaceURI()) &&
797                         child.getLocalName().equals(RepoConstants.NODE_LICENSE)) {
798                     Node id = child.getAttributes().getNamedItem(RepoConstants.ATTR_ID);
799                     if (id != null) {
800                         licenses.put(id.getNodeValue(), child.getTextContent());
801                     }
802                 }
803             }
804 
805             // Parse packages
806             for (Node child = root.getFirstChild();
807                  child != null;
808                  child = child.getNextSibling()) {
809                 if (child.getNodeType() == Node.ELEMENT_NODE &&
810                         nsUri.equals(child.getNamespaceURI())) {
811                     String name = child.getLocalName();
812                     Package p = null;
813 
814                     try {
815                         // We can load addon and extra packages from all sources, either
816                         // internal or user sources.
817                         if (SdkAddonConstants.NODE_ADD_ON.equals(name)) {
818                             p = new AddonPackage(this, child, nsUri, licenses);
819 
820                         } else if (RepoConstants.NODE_EXTRA.equals(name)) {
821                             p = new ExtraPackage(this, child, nsUri, licenses);
822 
823                         } else if (!isAddonSource()) {
824                             // We only load platform, doc and tool packages from internal
825                             // sources, never from user sources.
826                             if (SdkRepoConstants.NODE_PLATFORM.equals(name)) {
827                                 p = new PlatformPackage(this, child, nsUri, licenses);
828                             } else if (SdkRepoConstants.NODE_DOC.equals(name)) {
829                                 p = new DocPackage(this, child, nsUri, licenses);
830                             } else if (SdkRepoConstants.NODE_TOOL.equals(name)) {
831                                 p = new ToolPackage(this, child, nsUri, licenses);
832                             } else if (SdkRepoConstants.NODE_PLATFORM_TOOL.equals(name)) {
833                                 p = new PlatformToolPackage(this, child, nsUri, licenses);
834                             } else if (SdkRepoConstants.NODE_SAMPLE.equals(name)) {
835                                 p = new SamplePackage(this, child, nsUri, licenses);
836                             } else if (SdkRepoConstants.NODE_SYSTEM_IMAGE.equals(name)) {
837                                 p = new SystemImagePackage(this, child, nsUri, licenses);
838                             } else if (SdkRepoConstants.NODE_SOURCE.equals(name)) {
839                                 p = new SourcePackage(this, child, nsUri, licenses);
840                             }
841                         }
842 
843                         if (p != null) {
844                             packages.add(p);
845                             monitor.logVerbose("Found %1$s", p.getShortDescription());
846                         }
847                     } catch (Exception e) {
848                         // Ignore invalid packages
849                         monitor.logError("Ignoring invalid %1$s element: %2$s", name, e.toString());
850                     }
851                 }
852             }
853 
854             setPackages(packages.toArray(new Package[packages.size()]));
855 
856             return true;
857         }
858 
859         return false;
860     }
861 
862     /**
863      * Returns the first child element with the given XML local name.
864      * If xmlLocalName is null, returns the very first child element.
865      */
getFirstChild(Node node, String nsUri, String xmlLocalName)866     private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
867 
868         for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
869             if (child.getNodeType() == Node.ELEMENT_NODE &&
870                     nsUri.equals(child.getNamespaceURI())) {
871                 if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
872                     return child;
873                 }
874             }
875         }
876 
877         return null;
878     }
879 
880     /**
881      * Takes an XML document as a string as parameter and returns a DOM for it.
882      *
883      * On error, returns null and prints a (hopefully) useful message on the monitor.
884      */
885     @VisibleForTesting(visibility=Visibility.PRIVATE)
getDocument(InputStream xml, ITaskMonitor monitor)886     protected Document getDocument(InputStream xml, ITaskMonitor monitor) {
887         try {
888             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
889             factory.setIgnoringComments(true);
890             factory.setNamespaceAware(true);
891 
892             DocumentBuilder builder = factory.newDocumentBuilder();
893             xml.reset();
894             Document doc = builder.parse(new InputSource(xml));
895 
896             return doc;
897         } catch (ParserConfigurationException e) {
898             monitor.logError("Failed to create XML document builder");
899 
900         } catch (SAXException e) {
901             monitor.logError("Failed to parse XML document");
902 
903         } catch (IOException e) {
904             monitor.logError("Failed to read XML document");
905         }
906 
907         return null;
908     }
909 }
910