Workaround for 64KB string limit for RPC responses in GWT Hosted Mode client due to Rhino library limitation.
Strings literals will be split into 64KB chunks and concatenated with the '+' operator. This is not strict
JSON format, but is valid Javascript and can be evaluated with negligable performance impact.
Review by: jat@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10888 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
index ca4c183..1114c87 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
@@ -18,6 +18,8 @@
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.js.JsParser;
import com.google.gwt.dev.js.ast.JsArrayLiteral;
+import com.google.gwt.dev.js.ast.JsBinaryOperation;
+import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBooleanLiteral;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExpression;
@@ -194,7 +196,7 @@
* This visitor reverses that transform by reducing all concat invocations into a single array
* literal.
*/
- private static class ConcatEvaler extends JsModVisitor {
+ private static class ArrayConcatEvaler extends JsModVisitor {
@Override
public boolean visit(JsInvocation invoke, JsContext ctx) {
@@ -202,7 +204,7 @@
if (!(expr instanceof JsNameRef)) {
return super.visit(invoke, ctx);
}
-
+
JsNameRef name = (JsNameRef) expr;
if (!name.getIdent().equals("concat")) {
return super.visit(invoke, ctx);
@@ -214,12 +216,93 @@
JsArrayLiteral arg = (JsArrayLiteral) ex;
headElements.getExpressions().addAll(arg.getExpressions());
}
-
+
ctx.replaceMe(headElements);
return true;
}
}
-
+
+ /**
+ * The server splits up string literals into 64KB chunks using '+' operators. For example
+ * ['chunk1chunk2'] is broken up into ['chunk1' + 'chunk2'].
+ * <p>
+ * This visitor reverses that transform by reducing such strings into a single string literal.
+ */
+ private static class StringConcatEvaler extends JsModVisitor {
+
+ @Override public boolean visit(JsBinaryOperation x, JsContext ctx) {
+ if (x.getOperator() != JsBinaryOperator.ADD) {
+ return super.visit(x, ctx);
+ }
+
+ // Do a first pass to get the total string length to avoid dynamically resizing the buffer.
+ int stringLength = getStringLength(x);
+ if (stringLength >= 0) {
+ StringBuilder builder = new StringBuilder(stringLength);
+ if (expressionToString(x, builder)) {
+ ctx.replaceMe(new JsStringLiteral(x.getSourceInfo(), builder.toString()));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Transforms an expression into a string. This will recurse into JsBinaryOperations of type
+ * JsBinaryOperator.ADD, which may have other ADD operations or JsStringLiterals as arguments.
+ *
+ * @param expression the expression to evaluate
+ * @param builder a builder that the string will be appended to
+ * @return true if the expression represents a valid string, or false otherwise
+ */
+ private boolean expressionToString(JsExpression expression, StringBuilder builder) {
+ if (expression instanceof JsStringLiteral) {
+ builder.append(((JsStringLiteral) expression).getValue());
+ return true;
+ }
+
+ if (expression instanceof JsBinaryOperation) {
+ JsBinaryOperation operation = (JsBinaryOperation) expression;
+ if (operation.getOperator() != JsBinaryOperator.ADD) {
+ return false;
+ }
+ return expressionToString(operation.getArg1(), builder)
+ && expressionToString(operation.getArg2(), builder);
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets the total string length of the given expression. This will recurse into
+ * JsBinaryOperations of type JsBinaryOperator.ADD, which may have other ADD operations or
+ * JsStringLiterals as arguments.
+ *
+ * @param expression the expression to evaluate
+ * @return the total string length, or -1 if the given expression does not represent a valid
+ * string
+ */
+ private int getStringLength(JsExpression expression) {
+ if (expression instanceof JsStringLiteral) {
+ return ((JsStringLiteral) expression).getValue().length();
+ }
+
+ if (expression instanceof JsBinaryOperation) {
+ JsBinaryOperation operation = (JsBinaryOperation) expression;
+ if (operation.getOperator() != JsBinaryOperator.ADD) {
+ return -1;
+ }
+
+ int arg1Length = getStringLength(operation.getArg1());
+ int arg2Length = getStringLength(operation.getArg2());
+
+ return (arg1Length >= 0 && arg2Length >= 0) ? (arg1Length + arg2Length) : -1;
+ }
+
+ return -1;
+ }
+ }
+
public ClientSerializationStreamReader(Serializer serializer) {
this.serializer = serializer;
}
@@ -229,8 +312,10 @@
try {
List<JsStatement> stmts = JsParser.parse(SourceOrigin.UNKNOWN, JsRootScope.INSTANCE,
new StringReader(encoded));
- ConcatEvaler concatEvaler = new ConcatEvaler();
- concatEvaler.acceptList(stmts);
+ ArrayConcatEvaler arrayConcatEvaler = new ArrayConcatEvaler();
+ arrayConcatEvaler.acceptList(stmts);
+ StringConcatEvaler stringConcatEvaler = new StringConcatEvaler();
+ stringConcatEvaler.acceptList(stmts);
decoder = new RpcDecoder();
decoder.acceptList(stmts);
} catch (Exception e) {
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
index 1bdf41e..9ea59c1 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
@@ -323,6 +323,14 @@
private static final char NON_BREAKING_HYPHEN = '\u2011';
+ /**
+ * Maximum length of a string node in RPC responses, not including surrounding
+ * quote characters (2 ^ 16 - 1) = 65535.
+ * This exists to work around a Rhino parser bug in the hosted mode client
+ * that limits string node lengths to 64KB.
+ */
+ private static final int MAX_STRING_NODE_LENGTH = 0xFFFF;
+
static {
/*
* NOTE: The JS VM in IE6 & IE7 do not interpret \v correctly. They convert
@@ -370,18 +378,59 @@
* than 1.3 that supports unicode strings.
*/
public static String escapeString(String toEscape) {
- // make output big enough to escape every character (plus the quotes)
+ return escapeString(toEscape, false);
+ }
+
+ /**
+ * This method takes a string and outputs a JavaScript string literal. The
+ * data is surrounded with quotes, and any contained characters that need to
+ * be escaped are mapped onto their escape sequence.
+ *
+ * This splits strings into 64KB chunks to workaround an issue with the hosted mode client where
+ * the Rhino parser can't handle string nodes larger than 64KB, e.g. {@code "longstring"} is
+ * converted to {@code "long" + "string"}.
+ *
+ * Assumptions: We are targeting a version of JavaScript that that is later
+ * than 1.3 that supports unicode strings.
+ */
+ public static String escapeStringSplitNodes(String toEscape) {
+ return escapeString(toEscape, true);
+ }
+
+ private static String escapeString(String toEscape, boolean splitNodes) {
char[] input = toEscape.toCharArray();
- CharVector charVector = new CharVector(input.length * 2 + 2, input.length);
+
+ // Since escaped characters will increase the output size, allocate extra room to start.
+ int capacityIncrement = Math.max(input.length, 16);
+ CharVector charVector = new CharVector(capacityIncrement * 2, capacityIncrement);
+
+ int i = 0;
+ int length = input.length;
charVector.add(JS_QUOTE_CHAR);
- for (int i = 0, n = input.length; i < n; ++i) {
- char c = input[i];
- if (needsUnicodeEscape(c)) {
- unicodeEscape(c, charVector);
- } else {
- charVector.add(c);
+ while (i < length) {
+
+ // Add one segment at a time, up to maxNodeLength characters. Note this always leave room
+ // for at least 6 characters at the end (maximum unicode escaped character size).
+ int maxSegmentVectorSize = splitNodes
+ ? (charVector.getSize() + MAX_STRING_NODE_LENGTH - 5)
+ : Integer.MAX_VALUE;
+
+ while (i < length && charVector.getSize() < maxSegmentVectorSize) {
+ char c = input[i++];
+ if (needsUnicodeEscape(c)) {
+ unicodeEscape(c, charVector);
+ } else {
+ charVector.add(c);
+ }
+ }
+
+ // If there's another segment left, insert a '+' operator.
+ if (splitNodes && i < length) {
+ charVector.add(JS_QUOTE_CHAR);
+ charVector.add('+');
+ charVector.add(JS_QUOTE_CHAR);
}
}
@@ -798,7 +847,7 @@
private void writeStringTable(LengthConstrainedArray stream) {
LengthConstrainedArray tableStream = new LengthConstrainedArray();
for (String s : getStringTable()) {
- tableStream.addToken(escapeString(s));
+ tableStream.addToken(escapeStringSplitNodes(s));
}
stream.addToken(tableStream.toString());
}
diff --git a/user/test/com/google/gwt/user/RPCSuite.java b/user/test/com/google/gwt/user/RPCSuite.java
index dc3f5e6..c2459a2 100644
--- a/user/test/com/google/gwt/user/RPCSuite.java
+++ b/user/test/com/google/gwt/user/RPCSuite.java
@@ -49,6 +49,7 @@
import com.google.gwt.user.client.rpc.ValueTypesTest;
import com.google.gwt.user.client.rpc.ValueTypesTestWithTypeObfuscation;
import com.google.gwt.user.client.rpc.XsrfProtectionTest;
+import com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReaderTest;
import com.google.gwt.user.rebind.rpc.BlacklistTypeFilterTest;
import com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilderTest;
import com.google.gwt.user.rebind.rpc.TypeHierarchyUtilsTest;
@@ -60,6 +61,7 @@
import com.google.gwt.user.server.rpc.RPCTest;
import com.google.gwt.user.server.rpc.SerializationPolicyLoaderTest;
import com.google.gwt.user.server.rpc.impl.LegacySerializationPolicyTest;
+import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriterTest;
import com.google.gwt.user.server.rpc.impl.StandardSerializationPolicyTest;
import junit.framework.Test;
@@ -98,6 +100,8 @@
suite.addTestSuite(Base64Test.class);
suite.addTestSuite(UtilTest.class);
suite.addTestSuite(AbstractXsrfProtectedServiceServletTest.class);
+ suite.addTestSuite(ClientSerializationStreamReaderTest.class);
+ suite.addTestSuite(ServerSerializationStreamWriterTest.class);
// GWTTestCases
suite.addTestSuite(ValueTypesTest.class);
diff --git a/user/test/com/google/gwt/user/client/rpc/ValueTypesTest.java b/user/test/com/google/gwt/user/client/rpc/ValueTypesTest.java
index d602443..ecafa12 100644
--- a/user/test/com/google/gwt/user/client/rpc/ValueTypesTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/ValueTypesTest.java
@@ -594,7 +594,36 @@
}
});
}
-
+
+ public void testString() {
+ assertEcho("test");
+ }
+
+ public void testString_empty() {
+ assertEcho("");
+ }
+
+ public void testString_over64KB() {
+ // Test a string over 64KB of a-z characters repeated.
+ String testString = "";
+ int totalChars = 0xFFFF + 0xFF;
+ for (int i = 0; i < totalChars; i++) {
+ testString += (char) ('a' + (i % 26));
+ }
+ assertEcho(testString);
+ }
+
+ public void testString_over64KBWithUnicode() {
+ // Test a string over64KB string that requires unicode escaping.
+ String testString = "";
+ int totalChars = 0xFFFF + 0xFF;
+ for (int i = 0; i < totalChars; i += 2) {
+ testString += '\u2011';
+ testString += (char) 0x08;
+ }
+ assertEcho(testString);
+ }
+
private void assertEcho(final BigDecimal value) {
ValueTypesTestServiceAsync service = getServiceAsync();
delayTestFinishForRpc();
@@ -625,6 +654,21 @@
});
}
+ private void assertEcho(final String value) {
+ ValueTypesTestServiceAsync service = getServiceAsync();
+ delayTestFinishForRpc();
+ service.echo(value, new AsyncCallback<String>() {
+ public void onFailure(Throwable caught) {
+ TestSetValidator.rethrowException(caught);
+ }
+
+ public void onSuccess(String result) {
+ assertEquals(value, result);
+ finishTest();
+ }
+ });
+ }
+
private ValueTypesTestServiceAsync getServiceAsync() {
if (primitiveTypeTestService == null) {
primitiveTypeTestService = (ValueTypesTestServiceAsync) GWT.create(ValueTypesTestService.class);
diff --git a/user/test/com/google/gwt/user/client/rpc/ValueTypesTestService.java b/user/test/com/google/gwt/user/client/rpc/ValueTypesTestService.java
index cecbf1b..faf3d63 100644
--- a/user/test/com/google/gwt/user/client/rpc/ValueTypesTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/ValueTypesTestService.java
@@ -43,6 +43,8 @@
SerializableGenericWrapperType<Void> echo(
SerializableGenericWrapperType<Void> value);
+ String echo(String value);
+
boolean echo_FALSE(boolean value);
byte echo_MAX_VALUE(byte value);
diff --git a/user/test/com/google/gwt/user/client/rpc/ValueTypesTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/ValueTypesTestServiceAsync.java
index 1a79fda..bf414bf 100644
--- a/user/test/com/google/gwt/user/client/rpc/ValueTypesTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/ValueTypesTestServiceAsync.java
@@ -44,6 +44,8 @@
void echo(SerializableGenericWrapperType<Void> value,
AsyncCallback<SerializableGenericWrapperType<Void>> callback);
+ void echo(String value, AsyncCallback<String> callback);
+
void echo_FALSE(boolean value, AsyncCallback<Boolean> callback);
void echo_MAX_VALUE(byte value, AsyncCallback<Byte> callback);
diff --git a/user/test/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReaderTest.java b/user/test/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReaderTest.java
new file mode 100644
index 0000000..e757339
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReaderTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.user.client.rpc.impl;
+
+import com.google.gwt.user.client.rpc.SerializationException;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests {@link ClientSerializationStreamReader}.
+ */
+public class ClientSerializationStreamReaderTest extends TestCase {
+
+ public void testRead() throws SerializationException {
+ ClientSerializationStreamReader reader = new ClientSerializationStreamReader(null);
+
+ reader.prepareToRead("["
+ + "3.5," // a double
+ + "1," // String table index: "one"
+ + "3," // String table index: "three"
+ + "2," // String table index: "two"
+ + "[\"one\",\"two\",\"three\"],"
+ + "0," // flags
+ + AbstractSerializationStream.SERIALIZATION_STREAM_VERSION // version
+ + "]");
+
+ assertEquals("two", reader.readString());
+ assertEquals("three", reader.readString());
+ assertEquals("one", reader.readString());
+ assertEquals(3.5, reader.readDouble());
+ }
+
+ public void testRead_stringConcats() throws SerializationException {
+ ClientSerializationStreamReader reader = new ClientSerializationStreamReader(null);
+
+ reader.prepareToRead("["
+ + "1," // String table index: "onetwothree"
+ + "[\"one\"+\"two\"+\"three\"],"
+ + "0," // flags
+ + AbstractSerializationStream.SERIALIZATION_STREAM_VERSION // version
+ + "]");
+
+ assertEquals("onetwothree", reader.readString());
+ }
+
+ public void testRead_arrayConcats() throws SerializationException {
+ ClientSerializationStreamReader reader = new ClientSerializationStreamReader(null);
+
+ reader.prepareToRead("["
+ + "1," // String table index: "one"
+ + "2" // String table index: "two"
+ + "].concat(["
+ + "[\"one\"].concat([\"two\"]),"
+ + "0," // flags
+ + AbstractSerializationStream.SERIALIZATION_STREAM_VERSION // version
+ + "])");
+
+ assertEquals("two", reader.readString());
+ assertEquals("one", reader.readString());
+ }
+
+ /*
+ * Note: this test verifies a issue with the Rhino parser that limits the size of a single string
+ * node to 64KB. If this test starts failing, then the Rhino parser may have been fixed to support
+ * larger strings and the string concat workaround could be removed.
+ */
+ public void testRead_stringOver64KB() {
+ ClientSerializationStreamReader reader = new ClientSerializationStreamReader(null);
+
+ int stringLength = 0xFFFF;
+ StringBuilder builder = new StringBuilder(stringLength);
+ for (int i = 0; i < stringLength; i++) {
+ builder.append('y');
+ }
+
+ // Push the string size over 64KB.
+ builder.append('z');
+
+ try {
+ reader.prepareToRead("["
+ + "1," // String table index
+ + "[\"" + builder.toString() + "\"],"
+ + "0," // flags
+ + AbstractSerializationStream.SERIALIZATION_STREAM_VERSION // version
+ + "]");
+ fail("Expected SerializationException");
+ } catch (SerializationException e) {
+ // Expected.
+ }
+ }
+
+ public void testRead_stringOver64KB_concat() throws SerializationException {
+ ClientSerializationStreamReader reader = new ClientSerializationStreamReader(null);
+
+ // First node is maximum allowed 64KB.
+ int node1Length = 0xFFFF;
+ StringBuilder node1Builder = new StringBuilder(node1Length);
+ for (int i = 0; i < node1Length; i++) {
+ node1Builder.append('y');
+ }
+
+ int node2Length = 0xFF;
+ StringBuilder node2Builder = new StringBuilder(0xFF);
+ for (int i = 0; i < node2Length; i++) {
+ node2Builder.append('z');
+ }
+
+ reader.prepareToRead("["
+ + "1," // String table index
+ + "[\"" + node1Builder.toString() + "\"+\"" + node2Builder.toString() + "\"],"
+ + "0," // flags
+ + AbstractSerializationStream.SERIALIZATION_STREAM_VERSION // version
+ + "]");
+
+ assertEquals(node1Builder.toString() + node2Builder.toString(), reader.readString());
+ }
+}
+
diff --git a/user/test/com/google/gwt/user/server/rpc/ValueTypesTestServiceImpl.java b/user/test/com/google/gwt/user/server/rpc/ValueTypesTestServiceImpl.java
index 16604ac..00b8e8f 100644
--- a/user/test/com/google/gwt/user/server/rpc/ValueTypesTestServiceImpl.java
+++ b/user/test/com/google/gwt/user/server/rpc/ValueTypesTestServiceImpl.java
@@ -71,6 +71,10 @@
return value;
}
+ public String echo(String value) {
+ return value;
+ }
+
public boolean echo_FALSE(boolean value) {
if (value != false) {
throw new RuntimeException();
diff --git a/user/test/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriterTest.java b/user/test/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriterTest.java
new file mode 100644
index 0000000..33d4156
--- /dev/null
+++ b/user/test/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriterTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.user.server.rpc.impl;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests {@link ServerSerializationStreamWriter}.
+ */
+public class ServerSerializationStreamWriterTest extends TestCase {
+
+ public void testEscapeString() {
+ // Ensure that when using escapeString, a large string is not split into
+ // separate nodes like escapeStringSplitNodes does.
+ int nodeLength = 0xFFFF * 2;
+ StringBuilder firstNodeBuilder = new StringBuilder(nodeLength);
+ for (int i = 0; i < nodeLength; i++) {
+ firstNodeBuilder.append('1');
+ }
+
+ String escaped = ServerSerializationStreamWriter.escapeString(
+ firstNodeBuilder.toString());
+
+ assertEquals("\"" + firstNodeBuilder.toString() + "\"", escaped);
+ }
+
+ public void testEscapeStringSplitNodes() {
+ String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes("test");
+ assertEquals("\"test\"", escaped);
+ }
+
+ public void testEscapeStringSplitNodes_unicodeEscape() {
+ String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
+ "测试" // Unicode characters
+ + "\"" // JS quote char
+ + "\\" // JS escape char
+ + '\u2011' // Unicode non-breaking hyphen char
+ + (char) 0x8); // Combining spacing mark
+ assertEquals(
+ "\""
+ + "测试" // Unicode characters
+ + "\\\"" // JS quote char
+ + "\\\\" // JS escape char
+ + "\\u2011" // Unicode non-breaking hyphen char
+ + "\\b" // Combining spacing mark
+ + "\"",
+ escaped);
+ }
+
+ public void testEscapeStringSplitNodes_over64KB() {
+ // String node length limit is 64KB.
+ int firstNodeLength = 0xFFFF;
+ StringBuilder firstNodeBuilder = new StringBuilder(firstNodeLength);
+ for (int i = 0; i < firstNodeLength - 5; i++) {
+ firstNodeBuilder.append('1');
+ }
+
+ int secondNodeLength = 0xFF;
+ StringBuilder secondNodeBuilder = new StringBuilder(secondNodeLength);
+ for (int i = 0; i < secondNodeLength; i++) {
+ secondNodeBuilder.append('2');
+ }
+
+ String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
+ firstNodeBuilder.toString() + secondNodeBuilder.toString());
+
+ assertEquals(
+ "\"" + firstNodeBuilder.toString() + "\"+\"" + secondNodeBuilder.toString() + "\"",
+ escaped);
+ }
+
+ public void testEscapeStringSplitNodes_over64KBEscaped() {
+ // Fill the entire 64KB string, but leave 6 characters for an escaped unicode character added
+ // below.
+ int firstNodeLength = 0xFFFF;
+ StringBuilder firstNodeBuilder = new StringBuilder(firstNodeLength);
+ for (int i = 0; i < firstNodeLength - 6; i++) {
+ firstNodeBuilder.append('y');
+ }
+ String firstNodeNoUnicode = firstNodeBuilder.toString();
+
+ // Add a unicode character on the boundary, this should be the last character added to the first
+ // node.
+ firstNodeBuilder.append('\u2011');
+
+ int secondNodeLength = 0xFF;
+ StringBuilder secondNodeBuilder = new StringBuilder(secondNodeLength);
+ for (int i = 0; i < secondNodeLength; i++) {
+ secondNodeBuilder.append('z');
+ }
+ String secondNode = secondNodeBuilder.toString();
+
+ String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
+ firstNodeBuilder.toString() + secondNode);
+
+ assertEquals(
+ "\"" + firstNodeNoUnicode + "\\u2011" // first node (including escaped unicode character)
+ + "\"+\""
+ + secondNode + "\"", // second node
+ escaped);
+ }
+
+}