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