OSDN Git Service

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