diff --git a/src/main/java/spark/Request.java b/src/main/java/spark/Request.java index 14137be761..8f1aa7c84e 100644 --- a/src/main/java/spark/Request.java +++ b/src/main/java/spark/Request.java @@ -31,9 +31,9 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import org.eclipse.jetty.util.URIUtil; import spark.routematch.RouteMatch; +import spark.utils.urldecoding.UrlDecode; import spark.utils.IOUtils; import spark.utils.SparkUtils; import spark.utils.StringUtils; @@ -286,7 +286,7 @@ public String queryParams(String queryParam) { /** * Gets the query param, or returns default value * - * @param queryParam the query parameter + * @param queryParam the query parameter * @param defaultValue the default value * @return the value of the provided queryParam, or default if value is null * Example: query parameter 'id' from the following request URI: /hello?id=foo @@ -497,12 +497,16 @@ private static Map getParams(List request, List for (int i = 0; (i < request.size()) && (i < matched.size()); i++) { String matchedPart = matched.get(i); + if (SparkUtils.isParam(matchedPart)) { - String decodedReq = URIUtil.decodePath(request.get(i)); + + String decodedReq = UrlDecode.path(request.get(i)); + LOG.debug("matchedPart: " - + matchedPart - + " = " - + decodedReq); + + matchedPart + + " = " + + decodedReq); + params.put(matchedPart.toLowerCase(), decodedReq); } } diff --git a/src/main/java/spark/utils/urldecoding/TypeUtil.java b/src/main/java/spark/utils/urldecoding/TypeUtil.java new file mode 100644 index 0000000000..dbcab94733 --- /dev/null +++ b/src/main/java/spark/utils/urldecoding/TypeUtil.java @@ -0,0 +1,251 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// +package spark.utils.urldecoding; + +import java.lang.reflect.Method; +import java.util.HashMap; + +/* ------------------------------------------------------------ */ + +/** + * TYPE Utilities. + * Provides various static utiltiy methods for manipulating types and their + * string representations. + * + * @since Jetty 4.1 + */ +public class TypeUtil { + + /* ------------------------------------------------------------ */ + private static final HashMap> name2Class = new HashMap<>(); + + static { + name2Class.put("boolean", java.lang.Boolean.TYPE); + name2Class.put("byte", java.lang.Byte.TYPE); + name2Class.put("char", java.lang.Character.TYPE); + name2Class.put("double", java.lang.Double.TYPE); + name2Class.put("float", java.lang.Float.TYPE); + name2Class.put("int", java.lang.Integer.TYPE); + name2Class.put("long", java.lang.Long.TYPE); + name2Class.put("short", java.lang.Short.TYPE); + name2Class.put("void", java.lang.Void.TYPE); + + name2Class.put("java.lang.Boolean.TYPE", java.lang.Boolean.TYPE); + name2Class.put("java.lang.Byte.TYPE", java.lang.Byte.TYPE); + name2Class.put("java.lang.Character.TYPE", java.lang.Character.TYPE); + name2Class.put("java.lang.Double.TYPE", java.lang.Double.TYPE); + name2Class.put("java.lang.Float.TYPE", java.lang.Float.TYPE); + name2Class.put("java.lang.Integer.TYPE", java.lang.Integer.TYPE); + name2Class.put("java.lang.Long.TYPE", java.lang.Long.TYPE); + name2Class.put("java.lang.Short.TYPE", java.lang.Short.TYPE); + name2Class.put("java.lang.Void.TYPE", java.lang.Void.TYPE); + + name2Class.put("java.lang.Boolean", java.lang.Boolean.class); + name2Class.put("java.lang.Byte", java.lang.Byte.class); + name2Class.put("java.lang.Character", java.lang.Character.class); + name2Class.put("java.lang.Double", java.lang.Double.class); + name2Class.put("java.lang.Float", java.lang.Float.class); + name2Class.put("java.lang.Integer", java.lang.Integer.class); + name2Class.put("java.lang.Long", java.lang.Long.class); + name2Class.put("java.lang.Short", java.lang.Short.class); + + name2Class.put("Boolean", java.lang.Boolean.class); + name2Class.put("Byte", java.lang.Byte.class); + name2Class.put("Character", java.lang.Character.class); + name2Class.put("Double", java.lang.Double.class); + name2Class.put("Float", java.lang.Float.class); + name2Class.put("Integer", java.lang.Integer.class); + name2Class.put("Long", java.lang.Long.class); + name2Class.put("Short", java.lang.Short.class); + + name2Class.put(null, java.lang.Void.TYPE); + name2Class.put("string", java.lang.String.class); + name2Class.put("String", java.lang.String.class); + name2Class.put("java.lang.String", java.lang.String.class); + } + + /* ------------------------------------------------------------ */ + private static final HashMap, String> class2Name = new HashMap<>(); + + static { + class2Name.put(java.lang.Boolean.TYPE, "boolean"); + class2Name.put(java.lang.Byte.TYPE, "byte"); + class2Name.put(java.lang.Character.TYPE, "char"); + class2Name.put(java.lang.Double.TYPE, "double"); + class2Name.put(java.lang.Float.TYPE, "float"); + class2Name.put(java.lang.Integer.TYPE, "int"); + class2Name.put(java.lang.Long.TYPE, "long"); + class2Name.put(java.lang.Short.TYPE, "short"); + class2Name.put(java.lang.Void.TYPE, "void"); + + class2Name.put(java.lang.Boolean.class, "java.lang.Boolean"); + class2Name.put(java.lang.Byte.class, "java.lang.Byte"); + class2Name.put(java.lang.Character.class, "java.lang.Character"); + class2Name.put(java.lang.Double.class, "java.lang.Double"); + class2Name.put(java.lang.Float.class, "java.lang.Float"); + class2Name.put(java.lang.Integer.class, "java.lang.Integer"); + class2Name.put(java.lang.Long.class, "java.lang.Long"); + class2Name.put(java.lang.Short.class, "java.lang.Short"); + + class2Name.put(null, "void"); + class2Name.put(java.lang.String.class, "java.lang.String"); + } + + /* ------------------------------------------------------------ */ + private static final HashMap, Method> class2Value = new HashMap<>(); + + static { + try { + Class[] s = {java.lang.String.class}; + + class2Value.put(java.lang.Boolean.TYPE, + java.lang.Boolean.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Byte.TYPE, + java.lang.Byte.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Double.TYPE, + java.lang.Double.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Float.TYPE, + java.lang.Float.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Integer.TYPE, + java.lang.Integer.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Long.TYPE, + java.lang.Long.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Short.TYPE, + java.lang.Short.class.getMethod("valueOf", s)); + + class2Value.put(java.lang.Boolean.class, + java.lang.Boolean.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Byte.class, + java.lang.Byte.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Double.class, + java.lang.Double.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Float.class, + java.lang.Float.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Integer.class, + java.lang.Integer.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Long.class, + java.lang.Long.class.getMethod("valueOf", s)); + class2Value.put(java.lang.Short.class, + java.lang.Short.class.getMethod("valueOf", s)); + } catch (Exception e) { + throw new Error(e); + } + } + + /** + * Parse an int from a substring. + * Negative numbers are not handled. + * + * @param s String + * @param offset Offset within string + * @param length Length of integer or -1 for remainder of string + * @param base base of the integer + * @return the parsed integer + * @throws NumberFormatException if the string cannot be parsed + */ + public static int parseInt(String s, int offset, int length, int base) + throws NumberFormatException { + int value = 0; + + if (length < 0) { + length = s.length() - offset; + } + + for (int i = 0; i < length; i++) { + char c = s.charAt(offset + i); + + int digit = convertHexDigit((int) c); + if (digit < 0 || digit >= base) { + throw new NumberFormatException(s.substring(offset, offset + length)); + } + value = value * base + digit; + } + return value; + } + + /* ------------------------------------------------------------ */ + public static String toString(byte[] bytes, int base) { + StringBuilder buf = new StringBuilder(); + for (byte b : bytes) { + int bi = 0xff & b; + int c = '0' + (bi / base) % base; + if (c > '9') { + c = 'a' + (c - '0' - 10); + } + buf.append((char) c); + c = '0' + bi % base; + if (c > '9') { + c = 'a' + (c - '0' - 10); + } + buf.append((char) c); + } + return buf.toString(); + } + + /* ------------------------------------------------------------ */ + + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static int convertHexDigit(char c) { + int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (d < 0 || d > 15) { + throw new NumberFormatException("!hex " + c); + } + return d; + } + + /* ------------------------------------------------------------ */ + + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static int convertHexDigit(int c) { + int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (d < 0 || d > 15) { + throw new NumberFormatException("!hex " + c); + } + return d; + } + + /* ------------------------------------------------------------ */ + public static String toHexString(byte b) { + return toHexString(new byte[] {b}, 0, 1); + } + + /* ------------------------------------------------------------ */ + public static String toHexString(byte[] b, int offset, int length) { + StringBuilder buf = new StringBuilder(); + for (int i = offset; i < offset + length; i++) { + int bi = 0xff & b[i]; + int c = '0' + (bi / 16) % 16; + if (c > '9') { + c = 'A' + (c - '0' - 10); + } + buf.append((char) c); + c = '0' + bi % 16; + if (c > '9') { + c = 'a' + (c - '0' - 10); + } + buf.append((char) c); + } + return buf.toString(); + } +} diff --git a/src/main/java/spark/utils/urldecoding/UrlDecode.java b/src/main/java/spark/utils/urldecoding/UrlDecode.java new file mode 100644 index 0000000000..2fff57e1b3 --- /dev/null +++ b/src/main/java/spark/utils/urldecoding/UrlDecode.java @@ -0,0 +1,157 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// +package spark.utils.urldecoding; + +public class UrlDecode { + + /* ------------------------------------------------------------ */ + /* Decode a URI path and strip parameters + */ + public static String path(String path) { + return path(path, 0, path.length()); + } + + /* ------------------------------------------------------------ */ + /* Decode a URI path and strip parameters of UTF-8 path + */ + public static String path(String path, int offset, int length) { + try { + Utf8StringBuilder builder = null; + int end = offset + length; + for (int i = offset; i < end; i++) { + char c = path.charAt(i); + switch (c) { + case '%': + if (builder == null) { + builder = new Utf8StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + if ((i + 2) < end) { + char u = path.charAt(i + 1); + if (u == 'u') { + // TODO this is wrong. This is a codepoint not a char + builder.append((char) (0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); + i += 5; + } else { + builder.append((byte) (0xff & (TypeUtil.convertHexDigit(u) * 16 + + TypeUtil.convertHexDigit(path.charAt(i + 2))))); + i += 2; + } + } else { + throw new IllegalArgumentException("Bad URI % encoding"); + } + + break; + + case ';': + if (builder == null) { + builder = new Utf8StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + + while (++i < end) { + if (path.charAt(i) == '/') { + builder.append('/'); + break; + } + } + + break; + + default: + if (builder != null) { + builder.append(c); + } + break; + } + } + + if (builder != null) { + return builder.toString(); + } + if (offset == 0 && length == path.length()) { + return path; + } + return path.substring(offset, end); + } catch (Utf8Appendable.NotUtf8Exception e) { + return decodeISO88591Path(path, offset, length); + } + } + + /* ------------------------------------------------------------ */ + /* Decode a URI path and strip parameters of ISO-8859-1 path + */ + private static String decodeISO88591Path(String path, int offset, int length) { + StringBuilder builder = null; + int end = offset + length; + for (int i = offset; i < end; i++) { + char c = path.charAt(i); + switch (c) { + case '%': + if (builder == null) { + builder = new StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + if ((i + 2) < end) { + char u = path.charAt(i + 1); + if (u == 'u') { + // TODO this is wrong. This is a codepoint not a char + builder.append((char) (0xffff & TypeUtil.parseInt(path, i + 2, 4, 16))); + i += 5; + } else { + builder.append((byte) (0xff & (TypeUtil.convertHexDigit(u) * 16 + + TypeUtil.convertHexDigit(path.charAt(i + 2))))); + i += 2; + } + } else { + throw new IllegalArgumentException(); + } + + break; + + case ';': + if (builder == null) { + builder = new StringBuilder(path.length()); + builder.append(path, offset, i - offset); + } + while (++i < end) { + if (path.charAt(i) == '/') { + builder.append('/'); + break; + } + } + break; + + default: + if (builder != null) { + builder.append(c); + } + break; + } + } + + if (builder != null) { + return builder.toString(); + } + if (offset == 0 && length == path.length()) { + return path; + } + return path.substring(offset, end); + } + +} diff --git a/src/main/java/spark/utils/urldecoding/Utf8Appendable.java b/src/main/java/spark/utils/urldecoding/Utf8Appendable.java new file mode 100644 index 0000000000..359b822838 --- /dev/null +++ b/src/main/java/spark/utils/urldecoding/Utf8Appendable.java @@ -0,0 +1,181 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// +package spark.utils.urldecoding; + +import java.io.IOException; + +/* ------------------------------------------------------------ */ + +/** + * Utf8 Appendable abstract base class + * This abstract class wraps a standard {@link java.lang.Appendable} and provides methods to append UTF-8 encoded bytes, that are converted into characters. + * This class is stateful and up to 4 calls to {@link #append(byte)} may be needed before state a character is appended to the string buffer. + * The UTF-8 decoding is done by this class and no additional buffers or Readers are used. The UTF-8 code was inspired by + * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * License information for Bjoern Hoehrmann's code: + * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de> + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + **/ +public abstract class Utf8Appendable { + public static final char REPLACEMENT = '\ufffd'; + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + + protected final Appendable _appendable; + protected int _state = UTF8_ACCEPT; + + private static final byte[] BYTE_TABLE = + { + // The first part of the table maps bytes to character classes that + // to reduce the size of the transition table and create bitmasks. + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 + }; + + private static final byte[] TRANS_TABLE = + { + // The second part is a transition table that maps a combination + // of a state of the automaton and a character class to a state. + 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, + 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 + }; + + private int _codep; + + public Utf8Appendable(Appendable appendable) { + _appendable = appendable; + } + + public abstract int length(); + + protected void reset() { + _state = UTF8_ACCEPT; + } + + + private void checkCharAppend() throws IOException { + if (_state != UTF8_ACCEPT) { + _appendable.append(REPLACEMENT); + int state = _state; + _state = UTF8_ACCEPT; + throw new org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception("char appended in state " + state); + } + } + + public void append(char c) { + try { + checkCharAppend(); + _appendable.append(c); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void append(String s, int offset, int length) { + try { + checkCharAppend(); + _appendable.append(s, offset, offset + length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public void append(byte b) { + try { + appendByte(b); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void appendByte(byte b) throws IOException { + + if (b > 0 && _state == UTF8_ACCEPT) { + _appendable.append((char) (b & 0xFF)); + } else { + int i = b & 0xFF; + int type = BYTE_TABLE[i]; + _codep = _state == UTF8_ACCEPT ? (0xFF >> type) & i : (i & 0x3F) | (_codep << 6); + int next = TRANS_TABLE[_state + type]; + + switch (next) { + case UTF8_ACCEPT: + _state = next; + if (_codep < Character.MIN_HIGH_SURROGATE) { + _appendable.append((char) _codep); + } else { + for (char c : Character.toChars(_codep)) + _appendable.append(c); + } + break; + + case UTF8_REJECT: + String reason = "byte " + TypeUtil.toHexString(b) + " in state " + (_state / 12); + _codep = 0; + _state = UTF8_ACCEPT; + _appendable.append(REPLACEMENT); + throw new org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception(reason); + + default: + _state = next; + + } + } + } + + public boolean isUtf8SequenceComplete() { + return _state == UTF8_ACCEPT; + } + + @SuppressWarnings("serial") + public static class NotUtf8Exception extends IllegalArgumentException { + public NotUtf8Exception(String reason) { + super("Not valid UTF8! " + reason); + } + } + + protected void checkState() { + if (!isUtf8SequenceComplete()) { + _codep = 0; + _state = UTF8_ACCEPT; + try { + _appendable.append(REPLACEMENT); + } catch (IOException e) { + throw new RuntimeException(e); + } + throw new org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception("incomplete UTF8 sequence"); + } + } + +} diff --git a/src/main/java/spark/utils/urldecoding/Utf8StringBuilder.java b/src/main/java/spark/utils/urldecoding/Utf8StringBuilder.java new file mode 100644 index 0000000000..ca9ec3ff96 --- /dev/null +++ b/src/main/java/spark/utils/urldecoding/Utf8StringBuilder.java @@ -0,0 +1,63 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// +package spark.utils.urldecoding; + +/** UTF-8 StringBuilder. + * + * This class wraps a standard {@link java.lang.StringBuilder} and provides methods to append + * UTF-8 encoded bytes, that are converted into characters. + * + * This class is stateful and up to 4 calls to {@link #append(byte)} may be needed before + * state a character is appended to the string buffer. + * + * The UTF-8 decoding is done by this class and no additional buffers or Readers are used. + * The UTF-8 code was inspired by http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + */ +public class Utf8StringBuilder extends Utf8Appendable +{ + final StringBuilder _buffer; + + public Utf8StringBuilder(int capacity) + { + super(new StringBuilder(capacity)); + _buffer=(StringBuilder)_appendable; + } + + @Override + public int length() + { + return _buffer.length(); + } + + @Override + public void reset() + { + super.reset(); + _buffer.setLength(0); + } + + @Override + public String toString() + { + checkState(); + return _buffer.toString(); + } + + +}