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.annotations.VisibleForTesting; 20 import com.android.annotations.VisibleForTesting.Visibility; 21 import com.android.sdklib.AndroidVersion; 22 import com.android.sdklib.SdkManager; 23 import com.android.sdklib.internal.repository.Archive.Arch; 24 import com.android.sdklib.internal.repository.Archive.Os; 25 import com.android.sdklib.repository.PkgProps; 26 import com.android.sdklib.repository.SdkAddonConstants; 27 import com.android.sdklib.repository.SdkRepoConstants; 28 29 import org.w3c.dom.Node; 30 31 import java.io.File; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Map; 35 import java.util.Properties; 36 37 /** 38 * A {@link Package} is the base class for "something" that can be downloaded from 39 * the SDK repository. 40 * <p/> 41 * A package has some attributes (revision, description) and a list of archives 42 * which represent the downloadable bits. 43 * <p/> 44 * Packages are contained by a {@link SdkSource} (a download site). 45 * <p/> 46 * Derived classes must implement the {@link IDescription} methods. 47 */ 48 public abstract class Package implements IDescription, Comparable<Package> { 49 50 private final int mRevision; 51 private final String mObsolete; 52 private final String mLicense; 53 private final String mDescription; 54 private final String mDescUrl; 55 private final String mReleaseNote; 56 private final String mReleaseUrl; 57 private final Archive[] mArchives; 58 private final SdkSource mSource; 59 60 /** 61 * Enum for the result of {@link Package#canBeUpdatedBy(Package)}. This used so that we can 62 * differentiate between a package that is totally incompatible, and one that is the same item 63 * but just not an update. 64 * @see #canBeUpdatedBy(Package) 65 */ 66 public static enum UpdateInfo { 67 /** Means that the 2 packages are not the same thing */ 68 INCOMPATIBLE, 69 /** Means that the 2 packages are the same thing but one does not upgrade the other. 70 * </p> 71 * TODO: this name is confusing. We need to dig deeper. */ 72 NOT_UPDATE, 73 /** Means that the 2 packages are the same thing, and one is the upgrade of the other */ 74 UPDATE; 75 } 76 77 /** 78 * Creates a new package from the attributes and elements of the given XML node. 79 * This constructor should throw an exception if the package cannot be created. 80 * 81 * @param source The {@link SdkSource} where this is loaded from. 82 * @param packageNode The XML element being parsed. 83 * @param nsUri The namespace URI of the originating XML document, to be able to deal with 84 * parameters that vary according to the originating XML schema. 85 * @param licenses The licenses loaded from the XML originating document. 86 */ 87 Package(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) { 88 mSource = source; 89 mRevision = XmlParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_REVISION, 0); 90 mDescription = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESCRIPTION); 91 mDescUrl = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESC_URL); 92 mReleaseNote = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_NOTE); 93 mReleaseUrl = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_URL); 94 mObsolete = XmlParserUtils.getOptionalXmlString( 95 packageNode, SdkRepoConstants.NODE_OBSOLETE); 96 97 mLicense = parseLicense(packageNode, licenses); 98 mArchives = parseArchives(XmlParserUtils.getFirstChild( 99 packageNode, SdkRepoConstants.NODE_ARCHIVES)); 100 } 101 102 /** 103 * Manually create a new package with one archive and the given attributes. 104 * This is used to create packages from local directories in which case there must be 105 * one archive which URL is the actual target location. 106 * <p/> 107 * Properties from props are used first when possible, e.g. if props is non null. 108 * <p/> 109 * By design, this creates a package with one and only one archive. 110 */ 111 public Package( 112 SdkSource source, 113 Properties props, 114 int revision, 115 String license, 116 String description, 117 String descUrl, 118 Os archiveOs, 119 Arch archiveArch, 120 String archiveOsPath) { 121 122 if (description == null) { 123 description = ""; 124 } 125 if (descUrl == null) { 126 descUrl = ""; 127 } 128 129 mRevision = Integer.parseInt( 130 getProperty(props, PkgProps.PKG_REVISION, Integer.toString(revision))); 131 mLicense = getProperty(props, PkgProps.PKG_LICENSE, license); 132 mDescription = getProperty(props, PkgProps.PKG_DESC, description); 133 mDescUrl = getProperty(props, PkgProps.PKG_DESC_URL, descUrl); 134 mReleaseNote = getProperty(props, PkgProps.PKG_RELEASE_NOTE, ""); 135 mReleaseUrl = getProperty(props, PkgProps.PKG_RELEASE_URL, ""); 136 mObsolete = getProperty(props, PkgProps.PKG_OBSOLETE, null); 137 138 // If source is null and we can find a source URL in the properties, generate 139 // a dummy source just to store the URL. This allows us to easily remember where 140 // a package comes from. 141 String srcUrl = getProperty(props, PkgProps.PKG_SOURCE_URL, null); 142 if (props != null && source == null && srcUrl != null) { 143 // Both Addon and Extra packages can come from an addon source. 144 // For Extras, we can tell by looking at the source URL. 145 if (this instanceof AddonPackage || 146 ((this instanceof ExtraPackage) && 147 srcUrl.endsWith(SdkAddonConstants.URL_DEFAULT_FILENAME))) { 148 source = new SdkAddonSource(srcUrl, null /*uiName*/); 149 } else { 150 source = new SdkRepoSource(srcUrl, null /*uiName*/); 151 } 152 } 153 mSource = source; 154 155 assert archiveOsPath != null; 156 mArchives = initializeArchives(props, archiveOs, archiveArch, archiveOsPath); 157 } 158 159 /** 160 * Called by the constructor to get the initial {@link #mArchives} array. 161 * <p/> 162 * This is invoked by the local-package constructor and allows mock testing 163 * classes to override the archives created. 164 * This is an <em>implementation</em> details and clients must <em>not</em> 165 * rely on this. 166 * 167 * @return Always return a non-null array. The array may be empty. 168 */ 169 @VisibleForTesting(visibility=Visibility.PRIVATE) 170 protected Archive[] initializeArchives( 171 Properties props, 172 Os archiveOs, 173 Arch archiveArch, 174 String archiveOsPath) { 175 return new Archive[] { 176 new Archive(this, 177 props, 178 archiveOs, 179 archiveArch, 180 archiveOsPath) }; 181 } 182 183 /** 184 * Utility method that returns a property from a {@link Properties} object. 185 * Returns the default value if props is null or if the property is not defined. 186 * 187 * @param props The {@link Properties} to search into. 188 * If null, the default value is returned. 189 * @param propKey The name of the property. Must not be null. 190 * @param defaultValue The default value to return if {@code props} is null or if the 191 * key is not found. Can be null. 192 * @return The string value of the given key in the properties, or null if the key 193 * isn't found or if {@code props} is null. 194 */ 195 static String getProperty(Properties props, String propKey, String defaultValue) { 196 if (props == null) { 197 return defaultValue; 198 } 199 return props.getProperty(propKey, defaultValue); 200 } 201 202 /** 203 * Save the properties of the current packages in the given {@link Properties} object. 204 * These properties will later be give the constructor that takes a {@link Properties} object. 205 */ 206 void saveProperties(Properties props) { 207 props.setProperty(PkgProps.PKG_REVISION, Integer.toString(mRevision)); 208 if (mLicense != null && mLicense.length() > 0) { 209 props.setProperty(PkgProps.PKG_LICENSE, mLicense); 210 } 211 212 if (mDescription != null && mDescription.length() > 0) { 213 props.setProperty(PkgProps.PKG_DESC, mDescription); 214 } 215 if (mDescUrl != null && mDescUrl.length() > 0) { 216 props.setProperty(PkgProps.PKG_DESC_URL, mDescUrl); 217 } 218 219 if (mReleaseNote != null && mReleaseNote.length() > 0) { 220 props.setProperty(PkgProps.PKG_RELEASE_NOTE, mReleaseNote); 221 } 222 if (mReleaseUrl != null && mReleaseUrl.length() > 0) { 223 props.setProperty(PkgProps.PKG_RELEASE_URL, mReleaseUrl); 224 } 225 if (mObsolete != null) { 226 props.setProperty(PkgProps.PKG_OBSOLETE, mObsolete); 227 } 228 229 if (mSource != null) { 230 props.setProperty(PkgProps.PKG_SOURCE_URL, mSource.getUrl()); 231 } 232 } 233 234 /** 235 * Parses the uses-licence node of this package, if any, and returns the license 236 * definition if there's one. Returns null if there's no uses-license element or no 237 * license of this name defined. 238 */ 239 private String parseLicense(Node packageNode, Map<String, String> licenses) { 240 Node usesLicense = XmlParserUtils.getFirstChild( 241 packageNode, SdkRepoConstants.NODE_USES_LICENSE); 242 if (usesLicense != null) { 243 Node ref = usesLicense.getAttributes().getNamedItem(SdkRepoConstants.ATTR_REF); 244 if (ref != null) { 245 String licenseRef = ref.getNodeValue(); 246 return licenses.get(licenseRef); 247 } 248 } 249 return null; 250 } 251 252 /** 253 * Parses an XML node to process the <archives> element. 254 * Always return a non-null array. The array may be empty. 255 */ 256 private Archive[] parseArchives(Node archivesNode) { 257 ArrayList<Archive> archives = new ArrayList<Archive>(); 258 259 if (archivesNode != null) { 260 String nsUri = archivesNode.getNamespaceURI(); 261 for(Node child = archivesNode.getFirstChild(); 262 child != null; 263 child = child.getNextSibling()) { 264 265 if (child.getNodeType() == Node.ELEMENT_NODE && 266 nsUri.equals(child.getNamespaceURI()) && 267 SdkRepoConstants.NODE_ARCHIVE.equals(child.getLocalName())) { 268 archives.add(parseArchive(child)); 269 } 270 } 271 } 272 273 return archives.toArray(new Archive[archives.size()]); 274 } 275 276 /** 277 * Parses one <archive> element from an <archives> container. 278 */ 279 private Archive parseArchive(Node archiveNode) { 280 Archive a = new Archive( 281 this, 282 (Os) XmlParserUtils.getEnumAttribute(archiveNode, SdkRepoConstants.ATTR_OS, 283 Os.values(), null), 284 (Arch) XmlParserUtils.getEnumAttribute(archiveNode, SdkRepoConstants.ATTR_ARCH, 285 Arch.values(), Arch.ANY), 286 XmlParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_URL), 287 XmlParserUtils.getXmlLong (archiveNode, SdkRepoConstants.NODE_SIZE, 0), 288 XmlParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_CHECKSUM) 289 ); 290 291 return a; 292 } 293 294 /** 295 * Returns the source that created (and owns) this package. Can be null. 296 */ 297 public SdkSource getParentSource() { 298 return mSource; 299 } 300 301 /** 302 * Returns true if the package is deemed obsolete, that is it contains an 303 * actual <code><obsolete></code> element. 304 */ 305 public boolean isObsolete() { 306 return mObsolete != null; 307 } 308 309 /** 310 * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). 311 * Can be 0 if this is a local package of unknown revision. 312 */ 313 public int getRevision() { 314 return mRevision; 315 } 316 317 /** 318 * Returns the optional description for all packages (platform, add-on, tool, doc) or 319 * for a lib. It is null if the element has not been specified in the repository XML. 320 */ 321 public String getLicense() { 322 return mLicense; 323 } 324 325 /** 326 * Returns the optional description for all packages (platform, add-on, tool, doc) or 327 * for a lib. Can be empty but not null. 328 */ 329 public String getDescription() { 330 return mDescription; 331 } 332 333 /** 334 * Returns the optional description URL for all packages (platform, add-on, tool, doc). 335 * Can be empty but not null. 336 */ 337 public String getDescUrl() { 338 return mDescUrl; 339 } 340 341 /** 342 * Returns the optional release note for all packages (platform, add-on, tool, doc) or 343 * for a lib. Can be empty but not null. 344 */ 345 public String getReleaseNote() { 346 return mReleaseNote; 347 } 348 349 /** 350 * Returns the optional release note URL for all packages (platform, add-on, tool, doc). 351 * Can be empty but not null. 352 */ 353 public String getReleaseNoteUrl() { 354 return mReleaseUrl; 355 } 356 357 /** 358 * Returns the archives defined in this package. 359 * Can be an empty array but not null. 360 */ 361 public Archive[] getArchives() { 362 return mArchives; 363 } 364 365 /** 366 * Returns true if this package contains the exact given archive. 367 * Important: This compares object references, not object equality. 368 */ 369 public boolean hasArchive(Archive archive) { 370 for (Archive a : mArchives) { 371 if (a == archive) { 372 return true; 373 } 374 } 375 return false; 376 } 377 378 /** 379 * Returns whether the {@link Package} has at least one {@link Archive} compatible with 380 * the host platform. 381 */ 382 public boolean hasCompatibleArchive() { 383 for (Archive archive : mArchives) { 384 if (archive.isCompatible()) { 385 return true; 386 } 387 } 388 389 return false; 390 } 391 392 /** 393 * Returns a short, reasonably unique string identifier that can be used 394 * to identify this package when installing from the command-line interface. 395 * {@code 'android list sdk'} will show these IDs and then in turn they can 396 * be provided to {@code 'android update sdk --no-ui --filter'} to select 397 * some specific packages. 398 * <p/> 399 * The identifiers must have the following properties: <br/> 400 * - They must contain only simple alphanumeric characters. <br/> 401 * - Commas, whitespace and any special character that could be obviously problematic 402 * to a shell interface should be avoided (so dash/underscore are OK, but things 403 * like colon, pipe or dollar should be avoided.) <br/> 404 * - The name must be consistent across calls and reasonably unique for the package 405 * type. Collisions can occur but should be rare. <br/> 406 * - Different package types should have a clearly different name pattern. <br/> 407 * - The revision number should not be included, as this would prevent updates 408 * from being automated (which is the whole point.) <br/> 409 * - It must remain reasonably human readable. <br/> 410 * - If no such id can exist (for example for a local package that cannot be installed) 411 * then an empty string should be returned. Don't return null. 412 * <p/> 413 * Important: This is <em>not</em> a strong unique identifier for the package. 414 * If you need a strong unique identifier, you should use {@link #comparisonKey()} 415 * and the {@link Comparable} interface. 416 */ 417 public abstract String installId(); 418 419 /** 420 * Returns the short description of the source, if not null. 421 * Otherwise returns the default Object toString result. 422 * <p/> 423 * This is mostly helpful for debugging. 424 * For UI display, use the {@link IDescription} interface. 425 */ 426 @Override 427 public String toString() { 428 String s = getShortDescription(); 429 if (s != null) { 430 return s; 431 } 432 return super.toString(); 433 } 434 435 /** 436 * Returns a description of this package that is suitable for a list display. 437 * Should not be empty. Must never be null. 438 * <p/> 439 * Note that this is the "base" name for the package 440 * with no specific revision nor API mentionned. 441 * In contrast, {@link #getShortDescription()} should be used if you want more details 442 * such as the package revision number or the API, if applicable. 443 */ 444 public abstract String getListDescription(); 445 446 /** 447 * Returns a short description for an {@link IDescription}. 448 * Can be empty but not null. 449 */ 450 public abstract String getShortDescription(); 451 452 /** 453 * Returns a long description for an {@link IDescription}. 454 * Can be empty but not null. 455 */ 456 public String getLongDescription() { 457 StringBuilder sb = new StringBuilder(); 458 459 String s = getDescription(); 460 if (s != null) { 461 sb.append(s); 462 } 463 if (sb.length() > 0) { 464 sb.append("\n"); 465 } 466 467 sb.append(String.format("Revision %1$d%2$s", 468 getRevision(), 469 isObsolete() ? " (Obsolete)" : "")); 470 471 s = getDescUrl(); 472 if (s != null && s.length() > 0) { 473 sb.append(String.format("\n\nMore information at %1$s", s)); 474 } 475 476 s = getReleaseNote(); 477 if (s != null && s.length() > 0) { 478 sb.append("\n\nRelease note:\n").append(s); 479 } 480 481 s = getReleaseNoteUrl(); 482 if (s != null && s.length() > 0) { 483 sb.append("\nRelease note URL: ").append(s); 484 } 485 486 return sb.toString(); 487 } 488 489 /** 490 * A package is local (that is 'installed locally') if it contains a single 491 * archive that is local. If not local, it's a remote package, only available 492 * on a remote source for download and installation. 493 */ 494 public boolean isLocal() { 495 return mArchives.length == 1 && mArchives[0].isLocal(); 496 } 497 498 /** 499 * Computes a potential installation folder if an archive of this package were 500 * to be installed right away in the given SDK root. 501 * <p/> 502 * Some types of packages install in a fix location, for example docs and tools. 503 * In this case the returned folder may already exist with a different archive installed 504 * at the desired location. <br/> 505 * For other packages types, such as add-on or platform, the folder name is only partially 506 * relevant to determine the content and thus a real check will be done to provide an 507 * existing or new folder depending on the current content of the SDK. 508 * <p/> 509 * Note that the installer *will* create all directories returned here just before 510 * installation so this method must not attempt to create them. 511 * 512 * @param osSdkRoot The OS path of the SDK root folder. 513 * @param sdkManager An existing SDK manager to list current platforms and addons. 514 * @return A new {@link File} corresponding to the directory to use to install this package. 515 */ 516 public abstract File getInstallFolder(String osSdkRoot, SdkManager sdkManager); 517 518 /** 519 * Hook called right before an archive is installed. The archive has already 520 * been downloaded successfully and will be installed in the directory specified by 521 * <var>installFolder</var> when this call returns. 522 * <p/> 523 * The hook lets the package decide if installation of this specific archive should 524 * be continue. The installer will still install the remaining packages if possible. 525 * <p/> 526 * The base implementation always return true. 527 * <p/> 528 * Note that the installer *will* create all directories specified by 529 * {@link #getInstallFolder} just before installation, so they must not be 530 * created here. This is also called before the previous install dir is removed 531 * so the previous content is still there during upgrade. 532 * 533 * @param archive The archive that will be installed 534 * @param monitor The {@link ITaskMonitor} to display errors. 535 * @param osSdkRoot The OS path of the SDK root folder. 536 * @param installFolder The folder where the archive will be installed. Note that this 537 * is <em>not</em> the folder where the archive was temporary 538 * unzipped. The installFolder, if it exists, contains the old 539 * archive that will soon be replaced by the new one. 540 * @return True if installing this archive shall continue, false if it should be skipped. 541 */ 542 public boolean preInstallHook(Archive archive, ITaskMonitor monitor, 543 String osSdkRoot, File installFolder) { 544 // Nothing to do in base class. 545 return true; 546 } 547 548 /** 549 * Hook called right after an archive has been installed. 550 * 551 * @param archive The archive that has been installed. 552 * @param monitor The {@link ITaskMonitor} to display errors. 553 * @param installFolder The folder where the archive was successfully installed. 554 * Null if the installation failed, in case the archive needs to 555 * do some cleanup after <code>preInstallHook</code>. 556 */ 557 public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { 558 // Nothing to do in base class. 559 } 560 561 /** 562 * Returns whether the give package represents the same item as the current package. 563 * <p/> 564 * Two packages are considered the same if they represent the same thing, except for the 565 * revision number. 566 * @param pkg the package to compare 567 * @return true if the item 568 */ 569 public abstract boolean sameItemAs(Package pkg); 570 571 /** 572 * Computes whether the given package is a suitable update for the current package. 573 * <p/> 574 * An update is just that: a new package that supersedes the current one. If the new 575 * package does not represent the same item or if it has the same or lower revision as the 576 * current one, it's not an update. 577 * 578 * @param replacementPackage The potential replacement package. 579 * @return One of the {@link UpdateInfo} values. 580 * 581 * @see #sameItemAs(Package) 582 */ 583 public UpdateInfo canBeUpdatedBy(Package replacementPackage) { 584 if (replacementPackage == null) { 585 return UpdateInfo.INCOMPATIBLE; 586 } 587 588 // check they are the same item. 589 if (sameItemAs(replacementPackage) == false) { 590 return UpdateInfo.INCOMPATIBLE; 591 } 592 593 // check revision number 594 if (replacementPackage.getRevision() > this.getRevision()) { 595 return UpdateInfo.UPDATE; 596 } 597 598 // not an upgrade but not incompatible either. 599 return UpdateInfo.NOT_UPDATE; 600 } 601 602 /** 603 * Returns an ordering like this: <br/> 604 * - Tools <br/> 605 * - Platform-Tools <br/> 606 * - Docs. <br/> 607 * - Platform n preview <br/> 608 * - Platform n <br/> 609 * - Platform n-1 <br/> 610 * - Samples packages <br/> 611 * - Add-on based on n preview <br/> 612 * - Add-on based on n <br/> 613 * - Add-on based on n-1 <br/> 614 * - Extra packages <br/> 615 * <p/> 616 * Important: this must NOT be used to compare if two packages are the same thing. 617 * This is achieved by {@link #sameItemAs(Package)} or {@link #canBeUpdatedBy(Package)}. 618 * <p/> 619 * This {@link #compareTo(Package)} method is purely an implementation detail to 620 * perform the right ordering of the packages in the list of available or installed packages. 621 * <p/> 622 * <em>Important</em>: Derived classes should consider overriding {@link #comparisonKey()} 623 * instead of this method. 624 */ 625 public int compareTo(Package other) { 626 String s1 = this.comparisonKey(); 627 String s2 = other.comparisonKey(); 628 629 return s1.compareTo(s2); 630 } 631 632 /** 633 * Computes a comparison key for each package used by {@link #compareTo(Package)}. 634 * The key is a string. 635 * The base package class return a string that encodes the package type, 636 * the revision number and the platform version, if applicable, in the form: 637 * <pre> 638 * t:N|v:NNNN.P|r:NNNN| 639 * </pre> 640 * All fields must start by a "letter colon" prefix and end with a vertical pipe (|, ASCII 124). 641 * <p/> 642 * The string format <em>may</em> change between releases and clients should not 643 * store them outside of the session or expect them to be consistent between 644 * different releases. They are purely an internal implementation details of the 645 * {@link #compareTo(Package)} method. 646 * <p/> 647 * Derived classes should get the string from the super class and then append 648 * or <em>insert</em> their own |-separated content. 649 * For example an extra vendor name & path can be inserted before the revision 650 * number, since it has more sorting weight. 651 */ 652 protected String comparisonKey() { 653 654 StringBuilder sb = new StringBuilder(); 655 656 sb.append("t:"); //$NON-NLS-1$ 657 if (this instanceof ToolPackage) { 658 sb.append(0); 659 } else if (this instanceof PlatformToolPackage) { 660 sb.append(1); 661 } else if (this instanceof DocPackage) { 662 sb.append(2); 663 } else if (this instanceof PlatformPackage) { 664 sb.append(3); 665 } else if (this instanceof SamplePackage) { 666 sb.append(4); 667 } else if (this instanceof SystemImagePackage) { 668 sb.append(5); 669 } else if (this instanceof AddonPackage) { 670 sb.append(6); 671 } else { 672 // extras and everything else 673 sb.append(9); 674 } 675 sb.append("|v:"); //$NON-NLS-1$ 676 677 678 // We insert the package version here because it is more important 679 // than the revision number. We want package version to be sorted 680 // top-down, so we'll use 10k-api as the sorting key. The day we 681 // get reach 10k APIs, we'll need to revisit this. 682 683 if (this instanceof IPackageVersion) { 684 AndroidVersion v = ((IPackageVersion) this).getVersion(); 685 686 sb.append(String.format("%1$04d.%2$d", //$NON-NLS-1$ 687 10000 - v.getApiLevel(), 688 v.isPreview() ? 1 : 0 689 )); 690 } 691 sb.append("|r:"); //$NON-NLS-1$ 692 693 694 // Append revision number 695 696 sb.append(String.format("%1$04d", getRevision())); //$NON-NLS-1$ 697 sb.append('|'); 698 699 return sb.toString(); 700 } 701 702 @Override 703 public int hashCode() { 704 final int prime = 31; 705 int result = 1; 706 result = prime * result + Arrays.hashCode(mArchives); 707 result = prime * result + ((mObsolete == null) ? 0 : mObsolete.hashCode()); 708 result = prime * result + mRevision; 709 result = prime * result + ((mSource == null) ? 0 : mSource.hashCode()); 710 return result; 711 } 712 713 @Override 714 public boolean equals(Object obj) { 715 if (this == obj) { 716 return true; 717 } 718 if (obj == null) { 719 return false; 720 } 721 if (!(obj instanceof Package)) { 722 return false; 723 } 724 Package other = (Package) obj; 725 if (!Arrays.equals(mArchives, other.mArchives)) { 726 return false; 727 } 728 if (mObsolete == null) { 729 if (other.mObsolete != null) { 730 return false; 731 } 732 } else if (!mObsolete.equals(other.mObsolete)) { 733 return false; 734 } 735 if (mRevision != other.mRevision) { 736 return false; 737 } 738 if (mSource == null) { 739 if (other.mSource != null) { 740 return false; 741 } 742 } else if (!mSource.equals(other.mSource)) { 743 return false; 744 } 745 return true; 746 } 747 } 748