1 /* 2 * Copyright 2014 The gRPC Authors 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 io.grpc; 18 19 import static com.google.common.base.Charsets.US_ASCII; 20 import static com.google.common.base.Charsets.UTF_8; 21 import static com.google.common.base.Preconditions.checkNotNull; 22 import static com.google.common.base.Throwables.getStackTraceAsString; 23 24 import com.google.common.base.MoreObjects; 25 import com.google.common.base.Objects; 26 import io.grpc.Metadata.TrustedAsciiMarshaller; 27 import java.nio.ByteBuffer; 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 import java.util.TreeMap; 32 import javax.annotation.Nullable; 33 import javax.annotation.concurrent.Immutable; 34 35 36 /** 37 * Defines the status of an operation by providing a standard {@link Code} in conjunction with an 38 * optional descriptive message. Instances of {@code Status} are created by starting with the 39 * template for the appropriate {@link Status.Code} and supplementing it with additional 40 * information: {@code Status.NOT_FOUND.withDescription("Could not find 'important_file.txt'");} 41 * 42 * <p>For clients, every remote call will return a status on completion. In the case of errors this 43 * status may be propagated to blocking stubs as a {@link RuntimeException} or to a listener as an 44 * explicit parameter. 45 * 46 * <p>Similarly servers can report a status by throwing {@link StatusRuntimeException} 47 * or by passing the status to a callback. 48 * 49 * <p>Utility functions are provided to convert a status to an exception and to extract them 50 * back out. 51 */ 52 @Immutable 53 public final class Status { 54 55 /** 56 * The set of canonical status codes. If new codes are added over time they must choose 57 * a numerical value that does not collide with any previously used value. 58 */ 59 public enum Code { 60 /** 61 * The operation completed successfully. 62 */ 63 OK(0), 64 65 /** 66 * The operation was cancelled (typically by the caller). 67 */ 68 CANCELLED(1), 69 70 /** 71 * Unknown error. An example of where this error may be returned is 72 * if a Status value received from another address space belongs to 73 * an error-space that is not known in this address space. Also 74 * errors raised by APIs that do not return enough error information 75 * may be converted to this error. 76 */ 77 UNKNOWN(2), 78 79 /** 80 * Client specified an invalid argument. Note that this differs 81 * from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments 82 * that are problematic regardless of the state of the system 83 * (e.g., a malformed file name). 84 */ 85 INVALID_ARGUMENT(3), 86 87 /** 88 * Deadline expired before operation could complete. For operations 89 * that change the state of the system, this error may be returned 90 * even if the operation has completed successfully. For example, a 91 * successful response from a server could have been delayed long 92 * enough for the deadline to expire. 93 */ 94 DEADLINE_EXCEEDED(4), 95 96 /** 97 * Some requested entity (e.g., file or directory) was not found. 98 */ 99 NOT_FOUND(5), 100 101 /** 102 * Some entity that we attempted to create (e.g., file or directory) already exists. 103 */ 104 ALREADY_EXISTS(6), 105 106 /** 107 * The caller does not have permission to execute the specified 108 * operation. PERMISSION_DENIED must not be used for rejections 109 * caused by exhausting some resource (use RESOURCE_EXHAUSTED 110 * instead for those errors). PERMISSION_DENIED must not be 111 * used if the caller cannot be identified (use UNAUTHENTICATED 112 * instead for those errors). 113 */ 114 PERMISSION_DENIED(7), 115 116 /** 117 * Some resource has been exhausted, perhaps a per-user quota, or 118 * perhaps the entire file system is out of space. 119 */ 120 RESOURCE_EXHAUSTED(8), 121 122 /** 123 * Operation was rejected because the system is not in a state 124 * required for the operation's execution. For example, directory 125 * to be deleted may be non-empty, an rmdir operation is applied to 126 * a non-directory, etc. 127 * 128 * <p>A litmus test that may help a service implementor in deciding 129 * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: 130 * (a) Use UNAVAILABLE if the client can retry just the failing call. 131 * (b) Use ABORTED if the client should retry at a higher-level 132 * (e.g., restarting a read-modify-write sequence). 133 * (c) Use FAILED_PRECONDITION if the client should not retry until 134 * the system state has been explicitly fixed. E.g., if an "rmdir" 135 * fails because the directory is non-empty, FAILED_PRECONDITION 136 * should be returned since the client should not retry unless 137 * they have first fixed up the directory by deleting files from it. 138 */ 139 FAILED_PRECONDITION(9), 140 141 /** 142 * The operation was aborted, typically due to a concurrency issue 143 * like sequencer check failures, transaction aborts, etc. 144 * 145 * <p>See litmus test above for deciding between FAILED_PRECONDITION, 146 * ABORTED, and UNAVAILABLE. 147 */ 148 ABORTED(10), 149 150 /** 151 * Operation was attempted past the valid range. E.g., seeking or 152 * reading past end of file. 153 * 154 * <p>Unlike INVALID_ARGUMENT, this error indicates a problem that may 155 * be fixed if the system state changes. For example, a 32-bit file 156 * system will generate INVALID_ARGUMENT if asked to read at an 157 * offset that is not in the range [0,2^32-1], but it will generate 158 * OUT_OF_RANGE if asked to read from an offset past the current 159 * file size. 160 * 161 * <p>There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. 162 * We recommend using OUT_OF_RANGE (the more specific error) when it applies 163 * so that callers who are iterating through 164 * a space can easily look for an OUT_OF_RANGE error to detect when they are done. 165 */ 166 OUT_OF_RANGE(11), 167 168 /** 169 * Operation is not implemented or not supported/enabled in this service. 170 */ 171 UNIMPLEMENTED(12), 172 173 /** 174 * Internal errors. Means some invariants expected by underlying 175 * system has been broken. If you see one of these errors, 176 * something is very broken. 177 */ 178 INTERNAL(13), 179 180 /** 181 * The service is currently unavailable. This is a most likely a 182 * transient condition and may be corrected by retrying with 183 * a backoff. 184 * 185 * <p>See litmus test above for deciding between FAILED_PRECONDITION, 186 * ABORTED, and UNAVAILABLE. 187 */ 188 UNAVAILABLE(14), 189 190 /** 191 * Unrecoverable data loss or corruption. 192 */ 193 DATA_LOSS(15), 194 195 /** 196 * The request does not have valid authentication credentials for the 197 * operation. 198 */ 199 UNAUTHENTICATED(16); 200 201 private final int value; 202 @SuppressWarnings("ImmutableEnumChecker") // we make sure the byte[] can't be modified 203 private final byte[] valueAscii; 204 Code(int value)205 private Code(int value) { 206 this.value = value; 207 this.valueAscii = Integer.toString(value).getBytes(US_ASCII); 208 } 209 210 /** 211 * The numerical value of the code. 212 */ value()213 public int value() { 214 return value; 215 } 216 217 /** 218 * Returns a {@link Status} object corresponding to this status code. 219 */ toStatus()220 public Status toStatus() { 221 return STATUS_LIST.get(value); 222 } 223 valueAscii()224 private byte[] valueAscii() { 225 return valueAscii; 226 } 227 } 228 229 // Create the canonical list of Status instances indexed by their code values. 230 private static final List<Status> STATUS_LIST = buildStatusList(); 231 buildStatusList()232 private static List<Status> buildStatusList() { 233 TreeMap<Integer, Status> canonicalizer = new TreeMap<Integer, Status>(); 234 for (Code code : Code.values()) { 235 Status replaced = canonicalizer.put(code.value(), new Status(code)); 236 if (replaced != null) { 237 throw new IllegalStateException("Code value duplication between " 238 + replaced.getCode().name() + " & " + code.name()); 239 } 240 } 241 return Collections.unmodifiableList(new ArrayList<>(canonicalizer.values())); 242 } 243 244 // A pseudo-enum of Status instances mapped 1:1 with values in Code. This simplifies construction 245 // patterns for derived instances of Status. 246 /** The operation completed successfully. */ 247 public static final Status OK = Code.OK.toStatus(); 248 /** The operation was cancelled (typically by the caller). */ 249 public static final Status CANCELLED = Code.CANCELLED.toStatus(); 250 /** Unknown error. See {@link Code#UNKNOWN}. */ 251 public static final Status UNKNOWN = Code.UNKNOWN.toStatus(); 252 /** Client specified an invalid argument. See {@link Code#INVALID_ARGUMENT}. */ 253 public static final Status INVALID_ARGUMENT = Code.INVALID_ARGUMENT.toStatus(); 254 /** Deadline expired before operation could complete. See {@link Code#DEADLINE_EXCEEDED}. */ 255 public static final Status DEADLINE_EXCEEDED = Code.DEADLINE_EXCEEDED.toStatus(); 256 /** Some requested entity (e.g., file or directory) was not found. */ 257 public static final Status NOT_FOUND = Code.NOT_FOUND.toStatus(); 258 /** Some entity that we attempted to create (e.g., file or directory) already exists. */ 259 public static final Status ALREADY_EXISTS = Code.ALREADY_EXISTS.toStatus(); 260 /** 261 * The caller does not have permission to execute the specified operation. See {@link 262 * Code#PERMISSION_DENIED}. 263 */ 264 public static final Status PERMISSION_DENIED = Code.PERMISSION_DENIED.toStatus(); 265 /** The request does not have valid authentication credentials for the operation. */ 266 public static final Status UNAUTHENTICATED = Code.UNAUTHENTICATED.toStatus(); 267 /** 268 * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system 269 * is out of space. 270 */ 271 public static final Status RESOURCE_EXHAUSTED = Code.RESOURCE_EXHAUSTED.toStatus(); 272 /** 273 * Operation was rejected because the system is not in a state required for the operation's 274 * execution. See {@link Code#FAILED_PRECONDITION}. 275 */ 276 public static final Status FAILED_PRECONDITION = 277 Code.FAILED_PRECONDITION.toStatus(); 278 /** 279 * The operation was aborted, typically due to a concurrency issue like sequencer check failures, 280 * transaction aborts, etc. See {@link Code#ABORTED}. 281 */ 282 public static final Status ABORTED = Code.ABORTED.toStatus(); 283 /** Operation was attempted past the valid range. See {@link Code#OUT_OF_RANGE}. */ 284 public static final Status OUT_OF_RANGE = Code.OUT_OF_RANGE.toStatus(); 285 /** Operation is not implemented or not supported/enabled in this service. */ 286 public static final Status UNIMPLEMENTED = Code.UNIMPLEMENTED.toStatus(); 287 /** Internal errors. See {@link Code#INTERNAL}. */ 288 public static final Status INTERNAL = Code.INTERNAL.toStatus(); 289 /** The service is currently unavailable. See {@link Code#UNAVAILABLE}. */ 290 public static final Status UNAVAILABLE = Code.UNAVAILABLE.toStatus(); 291 /** Unrecoverable data loss or corruption. */ 292 public static final Status DATA_LOSS = Code.DATA_LOSS.toStatus(); 293 294 /** 295 * Return a {@link Status} given a canonical error {@link Code} value. 296 */ fromCodeValue(int codeValue)297 public static Status fromCodeValue(int codeValue) { 298 if (codeValue < 0 || codeValue > STATUS_LIST.size()) { 299 return UNKNOWN.withDescription("Unknown code " + codeValue); 300 } else { 301 return STATUS_LIST.get(codeValue); 302 } 303 } 304 fromCodeValue(byte[] asciiCodeValue)305 private static Status fromCodeValue(byte[] asciiCodeValue) { 306 if (asciiCodeValue.length == 1 && asciiCodeValue[0] == '0') { 307 return Status.OK; 308 } 309 return fromCodeValueSlow(asciiCodeValue); 310 } 311 312 @SuppressWarnings("fallthrough") fromCodeValueSlow(byte[] asciiCodeValue)313 private static Status fromCodeValueSlow(byte[] asciiCodeValue) { 314 int index = 0; 315 int codeValue = 0; 316 switch (asciiCodeValue.length) { 317 case 2: 318 if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') { 319 break; 320 } 321 codeValue += (asciiCodeValue[index++] - '0') * 10; 322 // fall through 323 case 1: 324 if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') { 325 break; 326 } 327 codeValue += asciiCodeValue[index] - '0'; 328 if (codeValue < STATUS_LIST.size()) { 329 return STATUS_LIST.get(codeValue); 330 } 331 break; 332 default: 333 break; 334 } 335 return UNKNOWN.withDescription("Unknown code " + new String(asciiCodeValue, US_ASCII)); 336 } 337 338 /** 339 * Return a {@link Status} given a canonical error {@link Code} object. 340 */ fromCode(Code code)341 public static Status fromCode(Code code) { 342 return code.toStatus(); 343 } 344 345 /** 346 * Key to bind status code to trailing metadata. 347 */ 348 static final Metadata.Key<Status> CODE_KEY 349 = Metadata.Key.of("grpc-status", false /* not pseudo */, new StatusCodeMarshaller()); 350 351 /** 352 * Marshals status messages for ({@link #MESSAGE_KEY}. gRPC does not use binary coding of 353 * status messages by default, which makes sending arbitrary strings difficult. This marshaller 354 * uses ASCII printable characters by default, and percent encodes (e.g. %0A) all non ASCII bytes. 355 * This leads to normal text being mostly readable (especially useful for debugging), and special 356 * text still being sent. 357 * 358 * <p>By default, the HTTP spec says that header values must be encoded using a strict subset of 359 * ASCII (See RFC 7230 section 3.2.6). HTTP/2 HPACK allows use of arbitrary binary headers, but 360 * we do not use them for interoperating with existing HTTP/1.1 code. Since the grpc-message 361 * is encoded to such a header, it needs to not use forbidden characters. 362 * 363 * <p>This marshaller works by converting the passed in string into UTF-8, checking to see if 364 * each individual byte is an allowable byte, and then either percent encoding or passing it 365 * through. When percent encoding, the byte is converted into hexadecimal notation with a '%' 366 * prepended. 367 * 368 * <p>When unmarshalling, bytes are passed through unless they match the "%XX" pattern. If they 369 * do match, the unmarshaller attempts to convert them back into their original UTF-8 byte 370 * sequence. After the input header bytes are converted into UTF-8 bytes, the new byte array is 371 * reinterpretted back as a string. 372 */ 373 private static final TrustedAsciiMarshaller<String> STATUS_MESSAGE_MARSHALLER = 374 new StatusMessageMarshaller(); 375 376 /** 377 * Key to bind status message to trailing metadata. 378 */ 379 static final Metadata.Key<String> MESSAGE_KEY = 380 Metadata.Key.of("grpc-message", false /* not pseudo */, STATUS_MESSAGE_MARSHALLER); 381 382 /** 383 * Extract an error {@link Status} from the causal chain of a {@link Throwable}. 384 * If no status can be found, a status is created with {@link Code#UNKNOWN} as its code and 385 * {@code t} as its cause. 386 * 387 * @return non-{@code null} status 388 */ fromThrowable(Throwable t)389 public static Status fromThrowable(Throwable t) { 390 Throwable cause = checkNotNull(t, "t"); 391 while (cause != null) { 392 if (cause instanceof StatusException) { 393 return ((StatusException) cause).getStatus(); 394 } else if (cause instanceof StatusRuntimeException) { 395 return ((StatusRuntimeException) cause).getStatus(); 396 } 397 cause = cause.getCause(); 398 } 399 // Couldn't find a cause with a Status 400 return UNKNOWN.withCause(t); 401 } 402 403 /** 404 * Extract an error trailers from the causal chain of a {@link Throwable}. 405 * 406 * @return the trailers or {@code null} if not found. 407 */ 408 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") trailersFromThrowable(Throwable t)409 public static Metadata trailersFromThrowable(Throwable t) { 410 Throwable cause = checkNotNull(t, "t"); 411 while (cause != null) { 412 if (cause instanceof StatusException) { 413 return ((StatusException) cause).getTrailers(); 414 } else if (cause instanceof StatusRuntimeException) { 415 return ((StatusRuntimeException) cause).getTrailers(); 416 } 417 cause = cause.getCause(); 418 } 419 return null; 420 } 421 formatThrowableMessage(Status status)422 static String formatThrowableMessage(Status status) { 423 if (status.description == null) { 424 return status.code.toString(); 425 } else { 426 return status.code + ": " + status.description; 427 } 428 } 429 430 private final Code code; 431 private final String description; 432 private final Throwable cause; 433 Status(Code code)434 private Status(Code code) { 435 this(code, null, null); 436 } 437 Status(Code code, @Nullable String description, @Nullable Throwable cause)438 private Status(Code code, @Nullable String description, @Nullable Throwable cause) { 439 this.code = checkNotNull(code, "code"); 440 this.description = description; 441 this.cause = cause; 442 } 443 444 /** 445 * Create a derived instance of {@link Status} with the given cause. 446 * However, the cause is not transmitted from server to client. 447 */ withCause(Throwable cause)448 public Status withCause(Throwable cause) { 449 if (Objects.equal(this.cause, cause)) { 450 return this; 451 } 452 return new Status(this.code, this.description, cause); 453 } 454 455 /** 456 * Create a derived instance of {@link Status} with the given description. Leading and trailing 457 * whitespace may be removed; this may change in the future. 458 */ withDescription(String description)459 public Status withDescription(String description) { 460 if (Objects.equal(this.description, description)) { 461 return this; 462 } 463 return new Status(this.code, description, this.cause); 464 } 465 466 /** 467 * Create a derived instance of {@link Status} augmenting the current description with 468 * additional detail. Leading and trailing whitespace may be removed; this may change in the 469 * future. 470 */ augmentDescription(String additionalDetail)471 public Status augmentDescription(String additionalDetail) { 472 if (additionalDetail == null) { 473 return this; 474 } else if (this.description == null) { 475 return new Status(this.code, additionalDetail, this.cause); 476 } else { 477 return new Status(this.code, this.description + "\n" + additionalDetail, this.cause); 478 } 479 } 480 481 /** 482 * The canonical status code. 483 */ getCode()484 public Code getCode() { 485 return code; 486 } 487 488 /** 489 * A description of this status for human consumption. 490 */ 491 @Nullable getDescription()492 public String getDescription() { 493 return description; 494 } 495 496 /** 497 * The underlying cause of an error. 498 * Note that the cause is not transmitted from server to client. 499 */ 500 @Nullable getCause()501 public Throwable getCause() { 502 return cause; 503 } 504 505 /** 506 * Is this status OK, i.e., not an error. 507 */ isOk()508 public boolean isOk() { 509 return Code.OK == code; 510 } 511 512 /** 513 * Convert this {@link Status} to a {@link RuntimeException}. Use {@link #fromThrowable} 514 * to recover this {@link Status} instance when the returned exception is in the causal chain. 515 */ asRuntimeException()516 public StatusRuntimeException asRuntimeException() { 517 return new StatusRuntimeException(this); 518 } 519 520 /** 521 * Same as {@link #asRuntimeException()} but includes the provided trailers in the returned 522 * exception. 523 */ 524 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") asRuntimeException(@ullable Metadata trailers)525 public StatusRuntimeException asRuntimeException(@Nullable Metadata trailers) { 526 return new StatusRuntimeException(this, trailers); 527 } 528 529 /** 530 * Convert this {@link Status} to an {@link Exception}. Use {@link #fromThrowable} 531 * to recover this {@link Status} instance when the returned exception is in the causal chain. 532 */ asException()533 public StatusException asException() { 534 return new StatusException(this); 535 } 536 537 /** 538 * Same as {@link #asException()} but includes the provided trailers in the returned exception. 539 */ 540 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") asException(@ullable Metadata trailers)541 public StatusException asException(@Nullable Metadata trailers) { 542 return new StatusException(this, trailers); 543 } 544 545 /** A string representation of the status useful for debugging. */ 546 @Override toString()547 public String toString() { 548 return MoreObjects.toStringHelper(this) 549 .add("code", code.name()) 550 .add("description", description) 551 .add("cause", cause != null ? getStackTraceAsString(cause) : cause) 552 .toString(); 553 } 554 555 private static final class StatusCodeMarshaller implements TrustedAsciiMarshaller<Status> { 556 @Override toAsciiString(Status status)557 public byte[] toAsciiString(Status status) { 558 return status.getCode().valueAscii(); 559 } 560 561 @Override parseAsciiString(byte[] serialized)562 public Status parseAsciiString(byte[] serialized) { 563 return fromCodeValue(serialized); 564 } 565 } 566 567 private static final class StatusMessageMarshaller implements TrustedAsciiMarshaller<String> { 568 569 private static final byte[] HEX = 570 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 571 572 @Override toAsciiString(String value)573 public byte[] toAsciiString(String value) { 574 byte[] valueBytes = value.getBytes(UTF_8); 575 for (int i = 0; i < valueBytes.length; i++) { 576 byte b = valueBytes[i]; 577 // If there are only non escaping characters, skip the slow path. 578 if (isEscapingChar(b)) { 579 return toAsciiStringSlow(valueBytes, i); 580 } 581 } 582 return valueBytes; 583 } 584 isEscapingChar(byte b)585 private static boolean isEscapingChar(byte b) { 586 return b < ' ' || b >= '~' || b == '%'; 587 } 588 589 /** 590 * @param valueBytes the UTF-8 bytes 591 * @param ri The reader index, pointed at the first byte that needs escaping. 592 */ toAsciiStringSlow(byte[] valueBytes, int ri)593 private static byte[] toAsciiStringSlow(byte[] valueBytes, int ri) { 594 byte[] escapedBytes = new byte[ri + (valueBytes.length - ri) * 3]; 595 // copy over the good bytes 596 if (ri != 0) { 597 System.arraycopy(valueBytes, 0, escapedBytes, 0, ri); 598 } 599 int wi = ri; 600 for (; ri < valueBytes.length; ri++) { 601 byte b = valueBytes[ri]; 602 // Manually implement URL encoding, per the gRPC spec. 603 if (isEscapingChar(b)) { 604 escapedBytes[wi] = '%'; 605 escapedBytes[wi + 1] = HEX[(b >> 4) & 0xF]; 606 escapedBytes[wi + 2] = HEX[b & 0xF]; 607 wi += 3; 608 continue; 609 } 610 escapedBytes[wi++] = b; 611 } 612 byte[] dest = new byte[wi]; 613 System.arraycopy(escapedBytes, 0, dest, 0, wi); 614 615 return dest; 616 } 617 618 @SuppressWarnings("deprecation") // Use fast but deprecated String ctor 619 @Override parseAsciiString(byte[] value)620 public String parseAsciiString(byte[] value) { 621 for (int i = 0; i < value.length; i++) { 622 byte b = value[i]; 623 if (b < ' ' || b >= '~' || (b == '%' && i + 2 < value.length)) { 624 return parseAsciiStringSlow(value); 625 } 626 } 627 return new String(value, 0); 628 } 629 parseAsciiStringSlow(byte[] value)630 private static String parseAsciiStringSlow(byte[] value) { 631 ByteBuffer buf = ByteBuffer.allocate(value.length); 632 for (int i = 0; i < value.length;) { 633 if (value[i] == '%' && i + 2 < value.length) { 634 try { 635 buf.put((byte)Integer.parseInt(new String(value, i + 1, 2, US_ASCII), 16)); 636 i += 3; 637 continue; 638 } catch (NumberFormatException e) { 639 // ignore, fall through, just push the bytes. 640 } 641 } 642 buf.put(value[i]); 643 i += 1; 644 } 645 return new String(buf.array(), 0, buf.position(), UTF_8); 646 } 647 } 648 649 /** 650 * Equality on Statuses is not well defined. Instead, do comparison based on their Code with 651 * {@link #getCode}. The description and cause of the Status are unlikely to be stable, and 652 * additional fields may be added to Status in the future. 653 */ 654 @Override equals(Object obj)655 public boolean equals(Object obj) { 656 return super.equals(obj); 657 } 658 659 /** 660 * Hash codes on Statuses are not well defined. 661 * 662 * @see #equals 663 */ 664 @Override hashCode()665 public int hashCode() { 666 return super.hashCode(); 667 } 668 } 669