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