• 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.packages;
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.NullSdkLog;
23 import com.android.sdklib.SdkConstants;
24 import com.android.sdklib.SdkManager;
25 import com.android.sdklib.internal.repository.IDescription;
26 import com.android.sdklib.internal.repository.LocalSdkParser;
27 import com.android.sdklib.internal.repository.NullTaskMonitor;
28 import com.android.sdklib.internal.repository.XmlParserUtils;
29 import com.android.sdklib.internal.repository.archives.Archive;
30 import com.android.sdklib.internal.repository.archives.Archive.Arch;
31 import com.android.sdklib.internal.repository.archives.Archive.Os;
32 import com.android.sdklib.internal.repository.sources.SdkSource;
33 import com.android.sdklib.repository.PkgProps;
34 import com.android.sdklib.repository.RepoConstants;
35 
36 import org.w3c.dom.Node;
37 
38 import java.io.File;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Map;
42 import java.util.Properties;
43 import java.util.regex.Pattern;
44 
45 /**
46  * Represents a extra XML node in an SDK repository.
47  */
48 public class ExtraPackage extends MinToolsPackage
49     implements IMinApiLevelDependency {
50 
51     /**
52      * The extra display name. Used in the UI to represent the package. It can be anything.
53      */
54     private final String mDisplayName;
55 
56     /**
57      * The vendor id name. It is a simple alphanumeric string [a-zA-Z0-9_-].
58      */
59     private final String mVendorId;
60 
61     /**
62      * The vendor display name. Used in the UI to represent the vendor. It can be anything.
63      */
64     private final String mVendorDisplay;
65 
66     /**
67      * The sub-folder name. It must be a non-empty single-segment path.
68      */
69     private final String mPath;
70 
71     /**
72      * The optional old_paths, if any. If present, this is a list of old "path" values that
73      * we'd like to migrate to the current "path" name for this extra.
74      */
75     private final String mOldPaths;
76 
77     /**
78      * The minimal API level required by this extra package, if > 0,
79      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.
80      */
81     private final int mMinApiLevel;
82 
83     /**
84      * The project-files listed by this extra package.
85      * The array can be empty but not null.
86      */
87     private final String[] mProjectFiles;
88 
89     /**
90      * Creates a new tool package from the attributes and elements of the given XML node.
91      * This constructor should throw an exception if the package cannot be created.
92      *
93      * @param source The {@link SdkSource} where this is loaded from.
94      * @param packageNode The XML element being parsed.
95      * @param nsUri The namespace URI of the originating XML document, to be able to deal with
96      *          parameters that vary according to the originating XML schema.
97      * @param licenses The licenses loaded from the XML originating document.
98      */
ExtraPackage( SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses)99     public ExtraPackage(
100             SdkSource source,
101             Node packageNode,
102             String nsUri,
103             Map<String,String> licenses) {
104         super(source, packageNode, nsUri, licenses);
105 
106         mPath   = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_PATH);
107 
108         // Read name-display, vendor-display and vendor-id, introduced in addon-4.xsd.
109         // These are not optional, they are mandatory in addon-4 but we still treat them
110         // as optional so that we can fallback on using <vendor> which was the only one
111         // defined in addon-3.xsd.
112         String name  = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_NAME_DISPLAY);
113         String vname = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_DISPLAY);
114         String vid   = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_ID);
115 
116         if (vid.length() == 0) {
117             // If vid is missing, use the old <vendor> attribute.
118             // Note that in a valid XML, vendor-id cannot be an empty string.
119             // The only reason vid can be empty is when <vendor-id> is missing, which
120             // happens in an addon-3 schema, in which case the old <vendor> needs to be used.
121             String vendor = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR);
122             vid = sanitizeLegacyVendor(vendor);
123             if (vname.length() == 0) {
124                 vname = vendor;
125             }
126         }
127         if (vname.length() == 0) {
128             // The vendor-display name can be empty, in which case we use the vendor-id.
129             vname = vid;
130         }
131         mVendorDisplay = vname.trim();
132         mVendorId      = vid.trim();
133 
134         if (name.length() == 0) {
135             // If name is missing, use the <path> attribute as done in an addon-3 schema.
136             name = getPrettyName();
137         }
138         mDisplayName   = name.trim();
139 
140         mMinApiLevel = XmlParserUtils.getXmlInt(
141                 packageNode, RepoConstants.NODE_MIN_API_LEVEL, MIN_API_LEVEL_NOT_SPECIFIED);
142 
143         mProjectFiles = parseProjectFiles(
144                 XmlParserUtils.getFirstChild(packageNode, RepoConstants.NODE_PROJECT_FILES));
145 
146         mOldPaths = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_OLD_PATHS);
147     }
148 
parseProjectFiles(Node projectFilesNode)149     private String[] parseProjectFiles(Node projectFilesNode) {
150         ArrayList<String> paths = new ArrayList<String>();
151 
152         if (projectFilesNode != null) {
153             String nsUri = projectFilesNode.getNamespaceURI();
154             for(Node child = projectFilesNode.getFirstChild();
155                      child != null;
156                      child = child.getNextSibling()) {
157 
158                 if (child.getNodeType() == Node.ELEMENT_NODE &&
159                         nsUri.equals(child.getNamespaceURI()) &&
160                         RepoConstants.NODE_PATH.equals(child.getLocalName())) {
161                     String path = child.getTextContent();
162                     if (path != null) {
163                         path = path.trim();
164                         if (path.length() > 0) {
165                             paths.add(path);
166                         }
167                     }
168                 }
169             }
170         }
171 
172         return paths.toArray(new String[paths.size()]);
173     }
174 
175     /**
176      * Manually create a new package with one archive and the given attributes or properties.
177      * This is used to create packages from local directories in which case there must be
178      * one archive which URL is the actual target location.
179      * <p/>
180      * By design, this creates a package with one and only one archive.
181      */
create(SdkSource source, Properties props, String vendor, String path, int revision, String license, String description, String descUrl, Os archiveOs, Arch archiveArch, String archiveOsPath)182     public static Package create(SdkSource source,
183             Properties props,
184             String vendor,
185             String path,
186             int revision,
187             String license,
188             String description,
189             String descUrl,
190             Os archiveOs,
191             Arch archiveArch,
192             String archiveOsPath) {
193         ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license,
194                 description, descUrl, archiveOs, archiveArch, archiveOsPath);
195         return ep;
196     }
197 
198     /**
199      * Constructor used to create a mock {@link ExtraPackage}.
200      * Most of the attributes here are optional.
201      * When not defined, they will be extracted from the {@code props} properties.
202      */
203     @VisibleForTesting(visibility=Visibility.PRIVATE)
ExtraPackage(SdkSource source, Properties props, String vendorId, String path, int revision, String license, String description, String descUrl, Os archiveOs, Arch archiveArch, String archiveOsPath)204     protected ExtraPackage(SdkSource source,
205             Properties props,
206             String vendorId,
207             String path,
208             int revision,
209             String license,
210             String description,
211             String descUrl,
212             Os archiveOs,
213             Arch archiveArch,
214             String archiveOsPath) {
215         super(source,
216                 props,
217                 revision,
218                 license,
219                 description,
220                 descUrl,
221                 archiveOs,
222                 archiveArch,
223                 archiveOsPath);
224 
225         // The path argument comes before whatever could be in the properties
226         mPath   = path != null ? path : getProperty(props, PkgProps.EXTRA_PATH, path);
227 
228         String name  = getProperty(props, PkgProps.EXTRA_NAME_DISPLAY, "");     //$NON-NLS-1$
229         String vname = getProperty(props, PkgProps.EXTRA_VENDOR_DISPLAY, "");   //$NON-NLS-1$
230         String vid   = vendorId != null ? vendorId :
231                               getProperty(props, PkgProps.EXTRA_VENDOR_ID, ""); //$NON-NLS-1$
232 
233         if (vid.length() == 0) {
234             // If vid is missing, use the old <vendor> attribute.
235             // <vendor> did not exist prior to schema repo-v3 and tools r8.
236             String vendor = getProperty(props, PkgProps.EXTRA_VENDOR, "");      //$NON-NLS-1$
237             vid = sanitizeLegacyVendor(vendor);
238             if (vname.length() == 0) {
239                 vname = vendor;
240             }
241         }
242         if (vname.length() == 0) {
243             // The vendor-display name can be empty, in which case we use the vendor-id.
244             vname = vid;
245         }
246         mVendorDisplay = vname.trim();
247         mVendorId      = vid.trim();
248 
249         if (name.length() == 0) {
250             // If name is missing, use the <path> attribute as done in an addon-3 schema.
251             name = getPrettyName();
252         }
253         mDisplayName   = name.trim();
254 
255         mOldPaths = getProperty(props, PkgProps.EXTRA_OLD_PATHS, null);
256 
257         mMinApiLevel = Integer.parseInt(
258             getProperty(props,
259                     PkgProps.EXTRA_MIN_API_LEVEL,
260                     Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));
261 
262         String projectFiles = getProperty(props, PkgProps.EXTRA_PROJECT_FILES, null);
263         ArrayList<String> filePaths = new ArrayList<String>();
264         if (projectFiles != null && projectFiles.length() > 0) {
265             for (String filePath : projectFiles.split(Pattern.quote(File.pathSeparator))) {
266                 filePath = filePath.trim();
267                 if (filePath.length() > 0) {
268                     filePaths.add(filePath);
269                 }
270             }
271         }
272         mProjectFiles = filePaths.toArray(new String[filePaths.size()]);
273     }
274 
275     /**
276      * Save the properties of the current packages in the given {@link Properties} object.
277      * These properties will later be give the constructor that takes a {@link Properties} object.
278      */
279     @Override
saveProperties(Properties props)280     public void saveProperties(Properties props) {
281         super.saveProperties(props);
282 
283         props.setProperty(PkgProps.EXTRA_PATH, mPath);
284         props.setProperty(PkgProps.EXTRA_NAME_DISPLAY, mDisplayName);
285         props.setProperty(PkgProps.EXTRA_VENDOR_DISPLAY, mVendorDisplay);
286         props.setProperty(PkgProps.EXTRA_VENDOR_ID, mVendorId);
287 
288         if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {
289             props.setProperty(PkgProps.EXTRA_MIN_API_LEVEL, Integer.toString(getMinApiLevel()));
290         }
291 
292         if (mProjectFiles.length > 0) {
293             StringBuilder sb = new StringBuilder();
294             for (int i = 0; i < mProjectFiles.length; i++) {
295                 if (i > 0) {
296                     sb.append(File.pathSeparatorChar);
297                 }
298                 sb.append(mProjectFiles[i]);
299             }
300             props.setProperty(PkgProps.EXTRA_PROJECT_FILES, sb.toString());
301         }
302 
303         if (mOldPaths != null && mOldPaths.length() > 0) {
304             props.setProperty(PkgProps.EXTRA_OLD_PATHS, mOldPaths);
305         }
306     }
307 
308     /**
309      * Returns the minimal API level required by this extra package, if > 0,
310      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.
311      */
312     @Override
getMinApiLevel()313     public int getMinApiLevel() {
314         return mMinApiLevel;
315     }
316 
317     /**
318      * The project-files listed by this extra package.
319      * The array can be empty but not null.
320      * <p/>
321      * IMPORTANT: directory separators are NOT translated and may not match
322      * the {@link File#separatorChar} of the current platform. It's up to the
323      * user to adequately interpret the paths.
324      * Similarly, no guarantee is made on the validity of the paths.
325      * Users are expected to apply all usual sanity checks such as removing
326      * "./" and "../" and making sure these paths don't reference files outside
327      * of the installed archive.
328      *
329      * @since sdk-repository-4.xsd or sdk-addon-2.xsd
330      */
getProjectFiles()331     public String[] getProjectFiles() {
332         return mProjectFiles;
333     }
334 
335     /**
336      * Returns the old_paths, a list of obsolete path names for the extra package.
337      * <p/>
338      * These can be used by the installer to migrate an extra package using one of the
339      * old paths into the new path.
340      * <p/>
341      * These can also be used to recognize "old" renamed packages as the same as
342      * the current one.
343      *
344      * @return A list of old paths. Can be empty but not null.
345      */
getOldPaths()346     public String[] getOldPaths() {
347         if (mOldPaths == null || mOldPaths.length() == 0) {
348             return new String[0];
349         }
350         return mOldPaths.split(";");  //$NON-NLS-1$
351     }
352 
353     /**
354      * Returns the sanitized path folder name. It is a single-segment path.
355      * <p/>
356      * The package is installed in SDK/extras/vendor_name/path_name.
357      */
getPath()358     public String getPath() {
359         // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+
360         // and cannot be empty. Let's be defensive and enforce that anyway since things
361         // like "____" are still valid values that we don't want to allow.
362 
363         // Sanitize the path
364         String path = mPath.replaceAll("[^a-zA-Z0-9-]+", "_");      //$NON-NLS-1$
365         if (path.length() == 0 || path.equals("_")) {               //$NON-NLS-1$
366             int h = path.hashCode();
367             path = String.format("extra%08x", h);                   //$NON-NLS-1$
368         }
369 
370         return path;
371     }
372 
373     /**
374      * Returns the vendor id.
375      */
getVendorId()376     public String getVendorId() {
377         return mVendorId;
378     }
379 
getVendorDisplay()380     public String getVendorDisplay() {
381         return mVendorDisplay;
382     }
383 
getDisplayName()384     public String getDisplayName() {
385         return mDisplayName;
386     }
387 
388     /** Transforms the legacy vendor name into a usable vendor id. */
sanitizeLegacyVendor(String vendorDisplay)389     private String sanitizeLegacyVendor(String vendorDisplay) {
390         // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+
391         // and cannot be empty. Let's be defensive and enforce that anyway since things
392         // like "____" are still valid values that we don't want to allow.
393 
394         if (vendorDisplay != null && vendorDisplay.length() > 0) {
395             String vendor = vendorDisplay.trim();
396             // Sanitize the vendor
397             vendor = vendor.replaceAll("[^a-zA-Z0-9-]+", "_");      //$NON-NLS-1$
398             if (vendor.equals("_")) {                               //$NON-NLS-1$
399                 int h = vendor.hashCode();
400                 vendor = String.format("vendor%08x", h);            //$NON-NLS-1$
401             }
402 
403             return vendor;
404         }
405 
406         return ""; //$NON-NLS-1$
407 
408     }
409 
410     /**
411      * Used to produce a suitable name-display based on the current {@link #mPath}
412      * and {@link #mVendorDisplay} in addon-3 schemas.
413      */
getPrettyName()414     private String getPrettyName() {
415         String name = mPath;
416 
417         // In the past, we used to save the extras in a folder vendor-path,
418         // and that "vendor" would end up in the path when we reload the extra from
419         // disk. Detect this and compensate.
420         if (mVendorDisplay != null && mVendorDisplay.length() > 0) {
421             if (name.startsWith(mVendorDisplay + "-")) {  //$NON-NLS-1$
422                 name = name.substring(mVendorDisplay.length() + 1);
423             }
424         }
425 
426         // Uniformize all spaces in the name
427         if (name != null) {
428             name = name.replaceAll("[ _\t\f-]+", " ").trim();   //$NON-NLS-1$ //$NON-NLS-2$
429         }
430         if (name == null || name.length() == 0) {
431             name = "Unknown Extra";
432         }
433 
434         if (mVendorDisplay != null && mVendorDisplay.length() > 0) {
435             name = mVendorDisplay + " " + name;  //$NON-NLS-1$
436             name = name.replaceAll("[ _\t\f-]+", " ").trim();   //$NON-NLS-1$ //$NON-NLS-2$
437         }
438 
439         // Look at all lower case characters in range [1..n-1] and replace them by an upper
440         // case if they are preceded by a space. Also upper cases the first character of the
441         // string.
442         boolean changed = false;
443         char[] chars = name.toCharArray();
444         for (int n = chars.length - 1, i = 0; i < n; i++) {
445             if (Character.isLowerCase(chars[i]) && (i == 0 || chars[i - 1] == ' ')) {
446                 chars[i] = Character.toUpperCase(chars[i]);
447                 changed = true;
448             }
449         }
450         if (changed) {
451             name = new String(chars);
452         }
453 
454         // Special case: reformat a few typical acronyms.
455         name = name.replaceAll(" Usb ", " USB ");   //$NON-NLS-1$
456         name = name.replaceAll(" Api ", " API ");   //$NON-NLS-1$
457 
458         return name;
459     }
460 
461     /**
462      * Returns a string identifier to install this package from the command line.
463      * For extras, we use "extra-vendor-path".
464      * <p/>
465      * {@inheritDoc}
466      */
467     @Override
installId()468     public String installId() {
469         return String.format("extra-%1$s-%2$s",     //$NON-NLS-1$
470                 getVendorId(),
471                 getPath());
472     }
473 
474     /**
475      * Returns a description of this package that is suitable for a list display.
476      * <p/>
477      * {@inheritDoc}
478      */
479     @Override
getListDescription()480     public String getListDescription() {
481         String s = String.format("%1$s%2$s",
482                 getDisplayName(),
483                 isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$
484 
485         return s;
486     }
487 
488     /**
489      * Returns a short description for an {@link IDescription}.
490      */
491     @Override
getShortDescription()492     public String getShortDescription() {
493         String s = String.format("%1$s, revision %2$d%3$s",
494                 getDisplayName(),
495                 getRevision(),
496                 isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$
497 
498         return s;
499     }
500 
501     /**
502      * Returns a long description for an {@link IDescription}.
503      *
504      * The long description is whatever the XML contains for the &lt;description&gt; field,
505      * or the short description if the former is empty.
506      */
507     @Override
getLongDescription()508     public String getLongDescription() {
509         String s = String.format("%1$s, revision %2$d%3$s\nBy %4$s",
510                 getDisplayName(),
511                 getRevision(),
512                 isObsolete() ? " (Obsolete)" : "",  //$NON-NLS-2$
513                 getVendorDisplay());
514 
515         String d = getDescription();
516         if (d != null && d.length() > 0) {
517             s += '\n' + d;
518         }
519 
520         if (getMinToolsRevision() != MIN_TOOLS_REV_NOT_SPECIFIED) {
521             s += String.format("\nRequires tools revision %1$d", getMinToolsRevision());
522         }
523 
524         if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {
525             s += String.format("\nRequires SDK Platform Android API %1$s", getMinApiLevel());
526         }
527 
528         File localPath = getLocalArchivePath();
529         if (localPath != null) {
530             // For a local archive, also put the install path in the long description.
531             // This should help users locate the extra on their drive.
532             s += String.format("\nLocation: %1$s", localPath.getAbsolutePath());
533         } else {
534             // For a non-installed archive, indicate where it would be installed.
535             s += String.format("\nInstall path: %1$s",
536                     getInstallSubFolder(null/*sdk root*/).getPath());
537         }
538 
539         return s;
540     }
541 
542     /**
543      * Computes a potential installation folder if an archive of this package were
544      * to be installed right away in the given SDK root.
545      * <p/>
546      * A "tool" package should always be located in SDK/tools.
547      *
548      * @param osSdkRoot The OS path of the SDK root folder. Must NOT be null.
549      * @param sdkManager An existing SDK manager to list current platforms and addons.
550      *                   Not used in this implementation.
551      * @return A new {@link File} corresponding to the directory to use to install this package.
552      */
553     @Override
getInstallFolder(String osSdkRoot, SdkManager sdkManager)554     public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) {
555 
556         // First find if this extra is already installed. If so, reuse the same directory.
557         LocalSdkParser localParser = new LocalSdkParser();
558         Package[] pkgs = localParser.parseSdk(
559                 osSdkRoot,
560                 sdkManager,
561                 LocalSdkParser.PARSE_EXTRAS,
562                 new NullTaskMonitor(new NullSdkLog()));
563 
564         for (Package pkg : pkgs) {
565             if (sameItemAs(pkg) && pkg instanceof ExtraPackage) {
566                 File localPath = ((ExtraPackage) pkg).getLocalArchivePath();
567                 if (localPath != null) {
568                     return localPath;
569                 }
570             }
571         }
572 
573         return getInstallSubFolder(osSdkRoot);
574     }
575 
576     /**
577      * Computes the "sub-folder" install path, relative to the given SDK root.
578      * For an extra package, this is generally ".../extra/vendor-id/path".
579      *
580      * @param osSdkRoot The OS path of the SDK root folder if known.
581      *   This CAN be null, in which case the path will start at /extra.
582      * @return Either /extra/vendor/path or sdk-root/extra/vendor-id/path.
583      */
getInstallSubFolder(@ullable String osSdkRoot)584     private File getInstallSubFolder(@Nullable String osSdkRoot) {
585         // The /extras dir at the root of the SDK
586         File path = new File(osSdkRoot, SdkConstants.FD_EXTRAS);
587 
588         String vendor = getVendorId();
589         if (vendor != null && vendor.length() > 0) {
590             path = new File(path, vendor);
591         }
592 
593         String name = getPath();
594         if (name != null && name.length() > 0) {
595             path = new File(path, name);
596         }
597 
598         return path;
599     }
600 
601     @Override
sameItemAs(Package pkg)602     public boolean sameItemAs(Package pkg) {
603         // Extra packages are similar if they have the same path and vendor
604         if (pkg instanceof ExtraPackage) {
605             ExtraPackage ep = (ExtraPackage) pkg;
606 
607             String[] epOldPaths = ep.getOldPaths();
608             int lenEpOldPaths = epOldPaths.length;
609             for (int indexEp = -1; indexEp < lenEpOldPaths; indexEp++) {
610                 if (sameVendorAndPath(
611                         mVendorId,    mPath,
612                         ep.mVendorId, indexEp   < 0 ? ep.mPath : epOldPaths[indexEp])) {
613                     return true;
614                 }
615             }
616 
617             String[] thisOldPaths = getOldPaths();
618             int lenThisOldPaths = thisOldPaths.length;
619             for (int indexThis = -1; indexThis < lenThisOldPaths; indexThis++) {
620                 if (sameVendorAndPath(
621                         mVendorId,    indexThis < 0 ? mPath    : thisOldPaths[indexThis],
622                         ep.mVendorId, ep.mPath)) {
623                     return true;
624                 }
625             }
626         }
627 
628         return false;
629     }
630 
sameVendorAndPath( String thisVendor, String thisPath, String otherVendor, String otherPath)631     private static boolean sameVendorAndPath(
632             String thisVendor, String thisPath,
633             String otherVendor, String otherPath) {
634         // To be backward compatible, we need to support the old vendor-path form
635         // in either the current or the remote package.
636         //
637         // The vendor test below needs to account for an old installed package
638         // (e.g. with an install path of vendor-name) that has then been updated
639         // in-place and thus when reloaded contains the vendor name in both the
640         // path and the vendor attributes.
641         if (otherPath != null && thisPath != null && thisVendor != null) {
642             if (otherPath.equals(thisVendor + '-' + thisPath) &&
643                     (otherVendor == null ||
644                      otherVendor.length() == 0 ||
645                      otherVendor.equals(thisVendor))) {
646                 return true;
647             }
648         }
649         if (thisPath != null && otherPath != null && otherVendor != null) {
650             if (thisPath.equals(otherVendor + '-' + otherPath) &&
651                     (thisVendor == null ||
652                      thisVendor.length() == 0 ||
653                      thisVendor.equals(otherVendor))) {
654                 return true;
655             }
656         }
657 
658 
659         if (thisPath != null && thisPath.equals(otherPath)) {
660             if ((thisVendor == null && otherVendor == null) ||
661                 (thisVendor != null && thisVendor.equals(otherVendor))) {
662                 return true;
663             }
664         }
665 
666         return false;
667     }
668 
669     /**
670      * For extra packages, we want to add vendor|path to the sorting key
671      * <em>before<em/> the revision number.
672      * <p/>
673      * {@inheritDoc}
674      */
675     @Override
comparisonKey()676     protected String comparisonKey() {
677         String s = super.comparisonKey();
678         int pos = s.indexOf("|r:");         //$NON-NLS-1$
679         assert pos > 0;
680         s = s.substring(0, pos) +
681             "|ve:" + getVendorId() +          //$NON-NLS-1$
682             "|pa:" + getPath() +            //$NON-NLS-1$
683             s.substring(pos);
684         return s;
685     }
686 
687     // ---
688 
689     /**
690      * If this package is installed, returns the install path of the archive if valid.
691      * Returns null if not installed or if the path does not exist.
692      */
getLocalArchivePath()693     private File getLocalArchivePath() {
694         Archive[] archives = getArchives();
695         if (archives.length == 1 && archives[0].isLocal()) {
696             File path = new File(archives[0].getLocalOsPath());
697             if (path.isDirectory()) {
698                 return path;
699             }
700         }
701 
702         return null;
703     }
704 
705     @Override
hashCode()706     public int hashCode() {
707         final int prime = 31;
708         int result = super.hashCode();
709         result = prime * result + mMinApiLevel;
710         result = prime * result + ((mPath == null) ? 0 : mPath.hashCode());
711         result = prime * result + Arrays.hashCode(mProjectFiles);
712         result = prime * result + ((mVendorDisplay == null) ? 0 : mVendorDisplay.hashCode());
713         return result;
714     }
715 
716     @Override
equals(Object obj)717     public boolean equals(Object obj) {
718         if (this == obj) {
719             return true;
720         }
721         if (!super.equals(obj)) {
722             return false;
723         }
724         if (!(obj instanceof ExtraPackage)) {
725             return false;
726         }
727         ExtraPackage other = (ExtraPackage) obj;
728         if (mMinApiLevel != other.mMinApiLevel) {
729             return false;
730         }
731         if (mPath == null) {
732             if (other.mPath != null) {
733                 return false;
734             }
735         } else if (!mPath.equals(other.mPath)) {
736             return false;
737         }
738         if (!Arrays.equals(mProjectFiles, other.mProjectFiles)) {
739             return false;
740         }
741         if (mVendorDisplay == null) {
742             if (other.mVendorDisplay != null) {
743                 return false;
744             }
745         } else if (!mVendorDisplay.equals(other.mVendorDisplay)) {
746             return false;
747         }
748         return true;
749     }
750 }
751