OSDN Git Service

ab074cedb5ba840a2a8e6e448dc24de89899dbfa
[android-x86/sdk.git] / sdkmanager / libs / sdklib / src / com / android / sdklib / internal / repository / ExtraPackage.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.VisibleForTesting;\r
20 import com.android.annotations.VisibleForTesting.Visibility;\r
21 import com.android.sdklib.NullSdkLog;\r
22 import com.android.sdklib.SdkConstants;\r
23 import com.android.sdklib.SdkManager;\r
24 import com.android.sdklib.internal.repository.Archive.Arch;\r
25 import com.android.sdklib.internal.repository.Archive.Os;\r
26 import com.android.sdklib.repository.RepoConstants;\r
27 \r
28 import org.w3c.dom.Node;\r
29 \r
30 import java.io.File;\r
31 import java.util.ArrayList;\r
32 import java.util.Map;\r
33 import java.util.Properties;\r
34 import java.util.regex.Pattern;\r
35 \r
36 /**\r
37  * Represents a extra XML node in an SDK repository.\r
38  */\r
39 public class ExtraPackage extends MinToolsPackage\r
40     implements IMinApiLevelDependency {\r
41 \r
42     static final String PROP_PATH          = "Extra.Path";         //$NON-NLS-1$\r
43     static final String PROP_VENDOR        = "Extra.Vendor";       //$NON-NLS-1$\r
44     static final String PROP_MIN_API_LEVEL = "Extra.MinApiLevel";  //$NON-NLS-1$\r
45     static final String PROP_PROJECT_FILES = "Extra.ProjectFiles"; //$NON-NLS-1$\r
46 \r
47     /**\r
48      * The vendor folder name. It must be a non-empty single-segment path.\r
49      * <p/>\r
50      * The paths "add-ons", "platforms", "platform-tools", "tools" and "docs" are reserved and\r
51      * cannot be used.\r
52      * This limitation cannot be written in the XML Schema and must be enforced here by using\r
53      * the method {@link #isPathValid()} *before* installing the package.\r
54      */\r
55     private final String mVendor;\r
56 \r
57     /**\r
58      * The sub-folder name. It must be a non-empty single-segment path and has the same\r
59      * rules as {@link #mVendor}.\r
60      */\r
61     private final String mPath;\r
62 \r
63     /**\r
64      * The minimal API level required by this extra package, if > 0,\r
65      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
66      */\r
67     private final int mMinApiLevel;\r
68 \r
69     /**\r
70      * The project-files listed by this extra package.\r
71      * The array can be empty but not null.\r
72      */\r
73     private final String[] mProjectFiles;\r
74 \r
75     /**\r
76      * Creates a new tool package from the attributes and elements of the given XML node.\r
77      * This constructor should throw an exception if the package cannot be created.\r
78      *\r
79      * @param source The {@link SdkSource} where this is loaded from.\r
80      * @param packageNode The XML element being parsed.\r
81      * @param nsUri The namespace URI of the originating XML document, to be able to deal with\r
82      *          parameters that vary according to the originating XML schema.\r
83      * @param licenses The licenses loaded from the XML originating document.\r
84      */\r
85     ExtraPackage(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) {\r
86         super(source, packageNode, nsUri, licenses);\r
87 \r
88         mPath   = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_PATH);\r
89         mVendor = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR);\r
90 \r
91         mMinApiLevel = XmlParserUtils.getXmlInt(packageNode, RepoConstants.NODE_MIN_API_LEVEL,\r
92                 MIN_API_LEVEL_NOT_SPECIFIED);\r
93 \r
94         mProjectFiles = parseProjectFiles(\r
95                 XmlParserUtils.getFirstChild(packageNode, RepoConstants.NODE_PROJECT_FILES));\r
96     }\r
97 \r
98     private String[] parseProjectFiles(Node projectFilesNode) {\r
99         ArrayList<String> paths = new ArrayList<String>();\r
100 \r
101         if (projectFilesNode != null) {\r
102             String nsUri = projectFilesNode.getNamespaceURI();\r
103             for(Node child = projectFilesNode.getFirstChild();\r
104                      child != null;\r
105                      child = child.getNextSibling()) {\r
106 \r
107                 if (child.getNodeType() == Node.ELEMENT_NODE &&\r
108                         nsUri.equals(child.getNamespaceURI()) &&\r
109                         RepoConstants.NODE_PATH.equals(child.getLocalName())) {\r
110                     String path = child.getTextContent();\r
111                     if (path != null) {\r
112                         path = path.trim();\r
113                         if (path.length() > 0) {\r
114                             paths.add(path);\r
115                         }\r
116                     }\r
117                 }\r
118             }\r
119         }\r
120 \r
121         return paths.toArray(new String[paths.size()]);\r
122     }\r
123 \r
124     /**\r
125      * Manually create a new package with one archive and the given attributes or properties.\r
126      * This is used to create packages from local directories in which case there must be\r
127      * one archive which URL is the actual target location.\r
128      * <p/>\r
129      * By design, this creates a package with one and only one archive.\r
130      */\r
131     static Package create(SdkSource source,\r
132             Properties props,\r
133             String vendor,\r
134             String path,\r
135             int revision,\r
136             String license,\r
137             String description,\r
138             String descUrl,\r
139             Os archiveOs,\r
140             Arch archiveArch,\r
141             String archiveOsPath) {\r
142         ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license,\r
143                 description, descUrl, archiveOs, archiveArch, archiveOsPath);\r
144 \r
145         if (ep.isPathValid()) {\r
146             return ep;\r
147         } else {\r
148             String shortDesc = ep.getShortDescription() + " [*]";  //$NON-NLS-1$\r
149 \r
150             String longDesc = String.format(\r
151                     "Broken Extra Package: %1$s\n" +\r
152                     "[*] Package cannot be used due to error: Invalid install path %2$s",\r
153                     description,\r
154                     ep.getPath());\r
155 \r
156             BrokenPackage ba = new BrokenPackage(props, shortDesc, longDesc,\r
157                     ep.getMinApiLevel(),\r
158                     IExactApiLevelDependency.API_LEVEL_INVALID,\r
159                     archiveOsPath);\r
160             return ba;\r
161         }\r
162     }\r
163 \r
164     @VisibleForTesting(visibility=Visibility.PRIVATE)\r
165     protected ExtraPackage(SdkSource source,\r
166             Properties props,\r
167             String vendor,\r
168             String path,\r
169             int revision,\r
170             String license,\r
171             String description,\r
172             String descUrl,\r
173             Os archiveOs,\r
174             Arch archiveArch,\r
175             String archiveOsPath) {\r
176         super(source,\r
177                 props,\r
178                 revision,\r
179                 license,\r
180                 description,\r
181                 descUrl,\r
182                 archiveOs,\r
183                 archiveArch,\r
184                 archiveOsPath);\r
185 \r
186         // The vendor argument is not supposed to be empty. However this attribute did not\r
187         // exist prior to schema repo-v3 and tools r8, which means we need to cope with a\r
188         // lack of it when reading back old local repositories. In this case we allow an\r
189         // empty string.\r
190         mVendor = vendor != null ? vendor : getProperty(props, PROP_VENDOR, "");\r
191 \r
192         // The path argument comes before whatever could be in the properties\r
193         mPath   = path != null ? path : getProperty(props, PROP_PATH, path);\r
194 \r
195         mMinApiLevel = Integer.parseInt(\r
196             getProperty(props, PROP_MIN_API_LEVEL, Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));\r
197 \r
198         String projectFiles = getProperty(props, PROP_PROJECT_FILES, null);\r
199         ArrayList<String> filePaths = new ArrayList<String>();\r
200         if (projectFiles != null && projectFiles.length() > 0) {\r
201             for (String filePath : projectFiles.split(Pattern.quote(File.pathSeparator))) {\r
202                 filePath = filePath.trim();\r
203                 if (filePath.length() > 0) {\r
204                     filePaths.add(filePath);\r
205                 }\r
206             }\r
207         }\r
208         mProjectFiles = filePaths.toArray(new String[filePaths.size()]);\r
209     }\r
210 \r
211     /**\r
212      * Save the properties of the current packages in the given {@link Properties} object.\r
213      * These properties will later be give the constructor that takes a {@link Properties} object.\r
214      */\r
215     @Override\r
216     void saveProperties(Properties props) {\r
217         super.saveProperties(props);\r
218 \r
219         props.setProperty(PROP_PATH, mPath);\r
220         if (mVendor != null) {\r
221             props.setProperty(PROP_VENDOR, mVendor);\r
222         }\r
223 \r
224         if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {\r
225             props.setProperty(PROP_MIN_API_LEVEL, Integer.toString(getMinApiLevel()));\r
226         }\r
227 \r
228         if (mProjectFiles.length > 0) {\r
229             StringBuilder sb = new StringBuilder();\r
230             for (int i = 0; i < mProjectFiles.length; i++) {\r
231                 if (i > 0) {\r
232                     sb.append(File.pathSeparatorChar);\r
233                 }\r
234                 sb.append(mProjectFiles[i]);\r
235             }\r
236             props.setProperty(PROP_PROJECT_FILES, sb.toString());\r
237         }\r
238     }\r
239 \r
240     /**\r
241      * Returns the minimal API level required by this extra package, if > 0,\r
242      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
243      */\r
244     public int getMinApiLevel() {\r
245         return mMinApiLevel;\r
246     }\r
247 \r
248     /**\r
249      * The project-files listed by this extra package.\r
250      * The array can be empty but not null.\r
251      * <p/>\r
252      * IMPORTANT: directory separators are NOT translated and may not match\r
253      * the {@link File#separatorChar} of the current platform. It's up to the\r
254      * user to adequately interpret the paths.\r
255      * Similarly, no guarantee is made on the validity of the paths.\r
256      * Users are expected to apply all usual sanity checks such as removing\r
257      * "./" and "../" and making sure these paths don't reference files outside\r
258      * of the installed archive.\r
259      *\r
260      * @since sdk-repository-4.xsd or sdk-addon-2.xsd\r
261      */\r
262     public String[] getProjectFiles() {\r
263         return mProjectFiles;\r
264     }\r
265 \r
266     /**\r
267      * Static helper to check if a given vendor and path is acceptable for an "extra" package.\r
268      */\r
269     public boolean isPathValid() {\r
270         return isSegmentValid(mVendor) && isSegmentValid(mPath);\r
271     }\r
272 \r
273     private boolean isSegmentValid(String segment) {\r
274         if (SdkConstants.FD_ADDONS.equals(segment) ||\r
275                 SdkConstants.FD_PLATFORMS.equals(segment) ||\r
276                 SdkConstants.FD_PLATFORM_TOOLS.equals(segment) ||\r
277                 SdkConstants.FD_TOOLS.equals(segment) ||\r
278                 SdkConstants.FD_DOCS.equals(segment) ||\r
279                 RepoConstants.FD_TEMP.equals(segment)) {\r
280             return false;\r
281         }\r
282         return segment != null && segment.indexOf('/') == -1 && segment.indexOf('\\') == -1;\r
283     }\r
284 \r
285     /**\r
286      * Returns the sanitized path folder name. It is a single-segment path.\r
287      * <p/>\r
288      * The package is installed in SDK/extras/vendor_name/path_name.\r
289      * <p/>\r
290      * The paths "add-ons", "platforms", "tools" and "docs" are reserved and cannot be used.\r
291      * This limitation cannot be written in the XML Schema and must be enforced here by using\r
292      * the method {@link #isPathValid()} *before* installing the package.\r
293      */\r
294     public String getPath() {\r
295         // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+\r
296         // and cannot be empty. Let's be defensive and enforce that anyway since things\r
297         // like "____" are still valid values that we don't want to allow.\r
298 \r
299         // Sanitize the path\r
300         String path = mPath.replaceAll("[^a-zA-Z0-9-]+", "_");      //$NON-NLS-1$\r
301         if (path.length() == 0 || path.equals("_")) {               //$NON-NLS-1$\r
302             int h = path.hashCode();\r
303             path = String.format("extra%08x", h);                   //$NON-NLS-1$\r
304         }\r
305 \r
306         return path;\r
307     }\r
308 \r
309     /**\r
310      * Returns the sanitized vendor folder name. It is a single-segment path.\r
311      * <p/>\r
312      * The package is installed in SDK/extras/vendor_name/path_name.\r
313      * <p/>\r
314      * An empty string is returned in case of error.\r
315      */\r
316     public String getVendor() {\r
317 \r
318         // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+\r
319         // and cannot be empty. Let's be defensive and enforce that anyway since things\r
320         // like "____" are still valid values that we don't want to allow.\r
321 \r
322         if (mVendor != null && mVendor.length() > 0) {\r
323             String vendor = mVendor;\r
324             // Sanitize the vendor\r
325             vendor = vendor.replaceAll("[^a-zA-Z0-9-]+", "_");      //$NON-NLS-1$\r
326             if (vendor.equals("_")) {                               //$NON-NLS-1$\r
327                 int h = vendor.hashCode();\r
328                 vendor = String.format("vendor%08x", h);            //$NON-NLS-1$\r
329             }\r
330 \r
331             return vendor;\r
332         }\r
333 \r
334         return ""; //$NON-NLS-1$\r
335     }\r
336 \r
337     private String getPrettyName() {\r
338         String name = mPath;\r
339 \r
340         // In the past, we used to save the extras in a folder vendor-path,\r
341         // and that "vendor" would end up in the path when we reload the extra from\r
342         // disk. Detect this and compensate.\r
343         if (mVendor != null && mVendor.length() > 0) {\r
344             if (name.startsWith(mVendor + "-")) {  //$NON-NLS-1$\r
345                 name = name.substring(mVendor.length() + 1);\r
346             }\r
347         }\r
348 \r
349         // Uniformize all spaces in the name\r
350         if (name != null) {\r
351             name = name.replaceAll("[ _\t\f-]+", " ").trim();   //$NON-NLS-1$ //$NON-NLS-2$\r
352         }\r
353         if (name == null || name.length() == 0) {   //$NON-NLS-1$\r
354             name = "Unkown Extra";\r
355         }\r
356 \r
357         if (mVendor != null && mVendor.length() > 0) {\r
358             name = mVendor + " " + name;  //$NON-NLS-1$\r
359             name = name.replaceAll("[ _\t\f-]+", " ").trim();   //$NON-NLS-1$ //$NON-NLS-2$\r
360         }\r
361 \r
362         // Look at all lower case characters in range [1..n-1] and replace them by an upper\r
363         // case if they are preceded by a space. Also upper cases the first character of the\r
364         // string.\r
365         boolean changed = false;\r
366         char[] chars = name.toCharArray();\r
367         for (int n = chars.length - 1, i = 0; i < n; i++) {\r
368             if (Character.isLowerCase(chars[i]) && (i == 0 || chars[i - 1] == ' ')) {\r
369                 chars[i] = Character.toUpperCase(chars[i]);\r
370                 changed = true;\r
371             }\r
372         }\r
373         if (changed) {\r
374             name = new String(chars);\r
375         }\r
376 \r
377         // Special case: reformat a few typical acronyms.\r
378         name = name.replaceAll(" Usb ", " USB ");   //$NON-NLS-1$\r
379         name = name.replaceAll(" Api ", " API ");   //$NON-NLS-1$\r
380 \r
381         return name;\r
382     }\r
383 \r
384     /**\r
385      * Returns a description of this package that is suitable for a list display.\r
386      * <p/>\r
387      * {@inheritDoc}\r
388      */\r
389     @Override\r
390     public String getListDescription() {\r
391         String s = String.format("%1$s package%2$s",\r
392                 getPrettyName(),\r
393                 isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
394 \r
395         return s;\r
396     }\r
397 \r
398     /**\r
399      * Returns a short description for an {@link IDescription}.\r
400      */\r
401     @Override\r
402     public String getShortDescription() {\r
403 \r
404         String s = String.format("%1$s package, revision %2$d%3$s",\r
405                 getPrettyName(),\r
406                 getRevision(),\r
407                 isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
408 \r
409         return s;\r
410     }\r
411 \r
412     /**\r
413      * Returns a long description for an {@link IDescription}.\r
414      *\r
415      * The long description is whatever the XML contains for the &lt;description&gt; field,\r
416      * or the short description if the former is empty.\r
417      */\r
418     @Override\r
419     public String getLongDescription() {\r
420         String s = getDescription();\r
421         if (s == null || s.length() == 0) {\r
422             s = String.format("Extra %1$s package by %2$s", getPath(), getVendor());\r
423         }\r
424 \r
425         if (s.indexOf("revision") == -1) {\r
426             s += String.format("\nRevision %1$d%2$s",\r
427                     getRevision(),\r
428                     isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
429         }\r
430 \r
431         if (getMinToolsRevision() != MIN_TOOLS_REV_NOT_SPECIFIED) {\r
432             s += String.format("\nRequires tools revision %1$d", getMinToolsRevision());\r
433         }\r
434 \r
435         if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {\r
436             s += String.format("\nRequires SDK Platform Android API %1$s", getMinApiLevel());\r
437         }\r
438 \r
439         // For a local archive, also put the install path in the long description.\r
440         // This should help users locate the extra on their drive.\r
441         File localPath = getLocalArchivePath();\r
442         if (localPath != null) {\r
443             s += String.format("\nLocation: %1$s", localPath.getAbsolutePath());\r
444         }\r
445 \r
446         return s;\r
447     }\r
448 \r
449     /**\r
450      * Computes a potential installation folder if an archive of this package were\r
451      * to be installed right away in the given SDK root.\r
452      * <p/>\r
453      * A "tool" package should always be located in SDK/tools.\r
454      *\r
455      * @param osSdkRoot The OS path of the SDK root folder.\r
456      * @param sdkManager An existing SDK manager to list current platforms and addons.\r
457      *                   Not used in this implementation.\r
458      * @return A new {@link File} corresponding to the directory to use to install this package.\r
459      */\r
460     @Override\r
461     public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) {\r
462 \r
463         // First find if this extra is already installed. If so, reuse the same directory.\r
464         LocalSdkParser localParser = new LocalSdkParser();\r
465         Package[] pkgs = localParser.parseSdk(osSdkRoot, sdkManager, new NullSdkLog());\r
466 \r
467         for (Package pkg : pkgs) {\r
468             if (sameItemAs(pkg) && pkg instanceof ExtraPackage) {\r
469                 File localPath = ((ExtraPackage) pkg).getLocalArchivePath();\r
470                 if (localPath != null) {\r
471                     return localPath;\r
472                 }\r
473             }\r
474         }\r
475 \r
476         // The /extras dir at the root of the SDK\r
477         File path = new File(osSdkRoot, SdkConstants.FD_EXTRAS);\r
478 \r
479         String vendor = getVendor();\r
480         if (vendor != null && vendor.length() > 0) {\r
481             path = new File(path, vendor);\r
482         }\r
483 \r
484         String name = getPath();\r
485         if (name != null && name.length() > 0) {\r
486             path = new File(path, name);\r
487         }\r
488 \r
489         return path;\r
490     }\r
491 \r
492     @Override\r
493     public boolean sameItemAs(Package pkg) {\r
494         // Extra packages are similar if they have the same path and vendor\r
495         if (pkg instanceof ExtraPackage) {\r
496             ExtraPackage ep = (ExtraPackage) pkg;\r
497 \r
498             // To be backward compatible, we need to support the old vendor-path form\r
499             // in either the current or the remote package.\r
500             //\r
501             // The vendor test below needs to account for an old installed package\r
502             // (e.g. with an install path of vendor-name) that has then beeen updated\r
503             // in-place and thus when reloaded contains the vendor name in both the\r
504             // path and the vendor attributes.\r
505             if (ep.mPath != null && mPath != null && mVendor != null) {\r
506                 if (ep.mPath.equals(mVendor + "-" + mPath) &&  //$NON-NLS-1$\r
507                         (ep.mVendor == null || ep.mVendor.length() == 0\r
508                                 || ep.mVendor.equals(mVendor))) {\r
509                     return true;\r
510                 }\r
511             }\r
512             if (mPath != null && ep.mPath != null && ep.mVendor != null) {\r
513                 if (mPath.equals(ep.mVendor + "-" + ep.mPath) &&  //$NON-NLS-1$\r
514                         (mVendor == null || mVendor.length() == 0 || mVendor.equals(ep.mVendor))) {\r
515                     return true;\r
516                 }\r
517             }\r
518 \r
519 \r
520             if (!mPath.equals(ep.mPath)) {\r
521                 return false;\r
522             }\r
523             if ((mVendor == null && ep.mVendor == null) ||\r
524                 (mVendor != null && mVendor.equals(ep.mVendor))) {\r
525                 return true;\r
526             }\r
527         }\r
528 \r
529         return false;\r
530     }\r
531 \r
532     /**\r
533      * For extra packages, we want to add vendor|path to the sorting key\r
534      * <em>before<em/> the revision number.\r
535      * <p/>\r
536      * {@inheritDoc}\r
537      */\r
538     @Override\r
539     protected String comparisonKey() {\r
540         String s = super.comparisonKey();\r
541         int pos = s.indexOf("|r:");         //$NON-NLS-1$\r
542         assert pos > 0;\r
543         s = s.substring(0, pos) +\r
544             "|ve:" + getVendor() +          //$NON-NLS-1$\r
545             "|pa:" + getPath() +            //$NON-NLS-1$\r
546             s.substring(pos);\r
547         return s;\r
548     }\r
549 \r
550     // ---\r
551 \r
552     /**\r
553      * If this package is installed, returns the install path of the archive if valid.\r
554      * Returns null if not installed or if the path does not exist.\r
555      */\r
556     private File getLocalArchivePath() {\r
557         Archive[] archives = getArchives();\r
558         if (archives.length == 1 && archives[0].isLocal()) {\r
559             File path = new File(archives[0].getLocalOsPath());\r
560             if (path.isDirectory()) {\r
561                 return path;\r
562             }\r
563         }\r
564 \r
565         return null;\r
566     }\r
567 }\r