Home | History | Annotate | Download | only in grpc
      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 
    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      */
    213     public int value() {
    214       return value;
    215     }
    216 
    217     /**
    218      * Returns a {@link Status} object corresponding to this status code.
    219      */
    220     public Status toStatus() {
    221       return STATUS_LIST.get(value);
    222     }
    223 
    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 
    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    */
    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 
    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")
    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    */
    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    */
    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")
    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 
    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 
    434   private Status(Code code) {
    435     this(code, null, null);
    436   }
    437 
    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    */
    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    */
    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    */
    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    */
    484   public Code getCode() {
    485     return code;
    486   }
    487 
    488   /**
    489    * A description of this status for human consumption.
    490    */
    491   @Nullable
    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
    501   public Throwable getCause() {
    502     return cause;
    503   }
    504 
    505   /**
    506    * Is this status OK, i.e., not an error.
    507    */
    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    */
    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")
    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    */
    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")
    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
    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
    557     public byte[] toAsciiString(Status status) {
    558       return status.getCode().valueAscii();
    559     }
    560 
    561     @Override
    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
    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 
    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      */
    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
    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 
    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
    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
    665   public int hashCode() {
    666     return super.hashCode();
    667   }
    668 }
    669