• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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