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