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