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