001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.server.rpc.impl;
017:
018: import com.google.gwt.user.client.rpc.SerializationException;
019: import com.google.gwt.user.client.rpc.SerializationStreamWriter;
020: import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter;
021: import com.google.gwt.user.server.rpc.SerializationPolicy;
022:
023: import java.lang.reflect.Field;
024: import java.lang.reflect.InvocationTargetException;
025: import java.lang.reflect.Method;
026: import java.lang.reflect.Modifier;
027: import java.util.ArrayList;
028: import java.util.HashMap;
029: import java.util.IdentityHashMap;
030: import java.util.Map;
031:
032: /**
033: * For internal use only. Used for server call serialization. This class is
034: * carefully matched with the client-side version.
035: */
036: public final class ServerSerializationStreamWriter extends
037: AbstractSerializationStreamWriter {
038:
039: /**
040: * Enumeration used to provided typed instance writers.
041: */
042: private enum ValueWriter {
043: BOOLEAN {
044: @Override
045: void write(ServerSerializationStreamWriter stream,
046: Object instance) {
047: stream
048: .writeBoolean(((Boolean) instance)
049: .booleanValue());
050: }
051: },
052: BYTE {
053: @Override
054: void write(ServerSerializationStreamWriter stream,
055: Object instance) {
056: stream.writeByte(((Byte) instance).byteValue());
057: }
058: },
059: CHAR {
060: @Override
061: void write(ServerSerializationStreamWriter stream,
062: Object instance) {
063: stream.writeChar(((Character) instance).charValue());
064: }
065: },
066: DOUBLE {
067: @Override
068: void write(ServerSerializationStreamWriter stream,
069: Object instance) {
070: stream.writeDouble(((Double) instance).doubleValue());
071: }
072: },
073: FLOAT {
074: @Override
075: void write(ServerSerializationStreamWriter stream,
076: Object instance) {
077: stream.writeFloat(((Float) instance).floatValue());
078: }
079: },
080: INT {
081: @Override
082: void write(ServerSerializationStreamWriter stream,
083: Object instance) {
084: stream.writeInt(((Integer) instance).intValue());
085: }
086: },
087: LONG {
088: @Override
089: void write(ServerSerializationStreamWriter stream,
090: Object instance) {
091: stream.writeLong(((Long) instance).longValue());
092: }
093: },
094: OBJECT {
095: @Override
096: void write(ServerSerializationStreamWriter stream,
097: Object instance) throws SerializationException {
098: stream.writeObject(instance);
099: }
100: },
101: SHORT {
102: @Override
103: void write(ServerSerializationStreamWriter stream,
104: Object instance) {
105: stream.writeShort(((Short) instance).shortValue());
106: }
107: },
108: STRING {
109: @Override
110: void write(ServerSerializationStreamWriter stream,
111: Object instance) {
112: stream.writeString((String) instance);
113: }
114: };
115:
116: abstract void write(ServerSerializationStreamWriter stream,
117: Object instance) throws SerializationException;
118: }
119:
120: /**
121: * Enumeration used to provided typed vector writers.
122: */
123: private enum VectorWriter {
124: BOOLEAN_VECTOR {
125: @Override
126: void write(ServerSerializationStreamWriter stream,
127: Object instance) {
128: boolean[] vector = (boolean[]) instance;
129: stream.writeInt(vector.length);
130: for (int i = 0, n = vector.length; i < n; ++i) {
131: stream.writeBoolean(vector[i]);
132: }
133: }
134: },
135: BYTE_VECTOR {
136: @Override
137: void write(ServerSerializationStreamWriter stream,
138: Object instance) {
139: byte[] vector = (byte[]) instance;
140: stream.writeInt(vector.length);
141: for (int i = 0, n = vector.length; i < n; ++i) {
142: stream.writeByte(vector[i]);
143: }
144: }
145: },
146: CHAR_VECTOR {
147: @Override
148: void write(ServerSerializationStreamWriter stream,
149: Object instance) {
150: char[] vector = (char[]) instance;
151: stream.writeInt(vector.length);
152: for (int i = 0, n = vector.length; i < n; ++i) {
153: stream.writeChar(vector[i]);
154: }
155: }
156: },
157: DOUBLE_VECTOR {
158: @Override
159: void write(ServerSerializationStreamWriter stream,
160: Object instance) {
161: double[] vector = (double[]) instance;
162: stream.writeInt(vector.length);
163: for (int i = 0, n = vector.length; i < n; ++i) {
164: stream.writeDouble(vector[i]);
165: }
166: }
167: },
168: FLOAT_VECTOR {
169: @Override
170: void write(ServerSerializationStreamWriter stream,
171: Object instance) {
172: float[] vector = (float[]) instance;
173: stream.writeInt(vector.length);
174: for (int i = 0, n = vector.length; i < n; ++i) {
175: stream.writeFloat(vector[i]);
176: }
177: }
178: },
179: INT_VECTOR {
180: @Override
181: void write(ServerSerializationStreamWriter stream,
182: Object instance) {
183: int[] vector = (int[]) instance;
184: stream.writeInt(vector.length);
185: for (int i = 0, n = vector.length; i < n; ++i) {
186: stream.writeInt(vector[i]);
187: }
188: }
189: },
190: LONG_VECTOR {
191: @Override
192: void write(ServerSerializationStreamWriter stream,
193: Object instance) {
194: long[] vector = (long[]) instance;
195: stream.writeInt(vector.length);
196: for (int i = 0, n = vector.length; i < n; ++i) {
197: stream.writeLong(vector[i]);
198: }
199: }
200: },
201: OBJECT_VECTOR {
202: @Override
203: void write(ServerSerializationStreamWriter stream,
204: Object instance) throws SerializationException {
205: Object[] vector = (Object[]) instance;
206: stream.writeInt(vector.length);
207: for (int i = 0, n = vector.length; i < n; ++i) {
208: stream.writeObject(vector[i]);
209: }
210: }
211: },
212: SHORT_VECTOR {
213: @Override
214: void write(ServerSerializationStreamWriter stream,
215: Object instance) {
216: short[] vector = (short[]) instance;
217: stream.writeInt(vector.length);
218: for (int i = 0, n = vector.length; i < n; ++i) {
219: stream.writeShort(vector[i]);
220: }
221: }
222: },
223: STRING_VECTOR {
224: @Override
225: void write(ServerSerializationStreamWriter stream,
226: Object instance) {
227: String[] vector = (String[]) instance;
228: stream.writeInt(vector.length);
229: for (int i = 0, n = vector.length; i < n; ++i) {
230: stream.writeString(vector[i]);
231: }
232: }
233: };
234:
235: abstract void write(ServerSerializationStreamWriter stream,
236: Object instance) throws SerializationException;
237: }
238:
239: private static final char NON_BREAKING_HYPHEN = '\u2011';
240:
241: /**
242: * Number of escaped JS Chars.
243: */
244: private static final int NUMBER_OF_JS_ESCAPED_CHARS = 128;
245:
246: /**
247: * A list of any characters that need escaping when printing a JavaScript
248: * string literal. Contains a 0 if the character does not need escaping,
249: * otherwise contains the character to escape with.
250: */
251: private static final char[] JS_CHARS_ESCAPED = new char[NUMBER_OF_JS_ESCAPED_CHARS];
252:
253: /**
254: * This defines the character used by JavaScript to mark the start of an
255: * escape sequence.
256: */
257: private static final char JS_ESCAPE_CHAR = '\\';
258:
259: /**
260: * This defines the character used to enclose JavaScript strings.
261: */
262: private static final char JS_QUOTE_CHAR = '\"';
263:
264: /**
265: * Index into this array using a nibble, 4 bits, to get the corresponding
266: * hexa-decimal character representation.
267: */
268: private static final char NIBBLE_TO_HEX_CHAR[] = { '0', '1', '2',
269: '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
270: 'F' };
271:
272: /**
273: * Map of {@link Class} objects to {@link ValueWriter}s.
274: */
275: private static final Map<Class<?>, ValueWriter> CLASS_TO_VALUE_WRITER = new IdentityHashMap<Class<?>, ValueWriter>();
276:
277: /**
278: * Map of {@link Class} vector objects to {@link VectorWriter}s.
279: */
280: private static final Map<Class<?>, VectorWriter> CLASS_TO_VECTOR_WRITER = new IdentityHashMap<Class<?>, VectorWriter>();
281:
282: static {
283: /*
284: * NOTE: The JS VM in IE6 & IE7 do not interpret \v correctly. They convert
285: * JavaScript Vertical Tab character '\v' into 'v'. As such, we do not use
286: * the short form of the unicode escape here.
287: */
288: JS_CHARS_ESCAPED['\u0000'] = '0';
289: JS_CHARS_ESCAPED['\b'] = 'b';
290: JS_CHARS_ESCAPED['\t'] = 't';
291: JS_CHARS_ESCAPED['\n'] = 'n';
292: JS_CHARS_ESCAPED['\f'] = 'f';
293: JS_CHARS_ESCAPED['\r'] = 'r';
294: JS_CHARS_ESCAPED[JS_ESCAPE_CHAR] = JS_ESCAPE_CHAR;
295: JS_CHARS_ESCAPED[JS_QUOTE_CHAR] = JS_QUOTE_CHAR;
296:
297: CLASS_TO_VECTOR_WRITER.put(boolean[].class,
298: VectorWriter.BOOLEAN_VECTOR);
299: CLASS_TO_VECTOR_WRITER.put(byte[].class,
300: VectorWriter.BYTE_VECTOR);
301: CLASS_TO_VECTOR_WRITER.put(char[].class,
302: VectorWriter.CHAR_VECTOR);
303: CLASS_TO_VECTOR_WRITER.put(double[].class,
304: VectorWriter.DOUBLE_VECTOR);
305: CLASS_TO_VECTOR_WRITER.put(float[].class,
306: VectorWriter.FLOAT_VECTOR);
307: CLASS_TO_VECTOR_WRITER
308: .put(int[].class, VectorWriter.INT_VECTOR);
309: CLASS_TO_VECTOR_WRITER.put(long[].class,
310: VectorWriter.LONG_VECTOR);
311: CLASS_TO_VECTOR_WRITER.put(Object[].class,
312: VectorWriter.OBJECT_VECTOR);
313: CLASS_TO_VECTOR_WRITER.put(short[].class,
314: VectorWriter.SHORT_VECTOR);
315: CLASS_TO_VECTOR_WRITER.put(String[].class,
316: VectorWriter.STRING_VECTOR);
317:
318: CLASS_TO_VALUE_WRITER.put(boolean.class, ValueWriter.BOOLEAN);
319: CLASS_TO_VALUE_WRITER.put(byte.class, ValueWriter.BYTE);
320: CLASS_TO_VALUE_WRITER.put(char.class, ValueWriter.CHAR);
321: CLASS_TO_VALUE_WRITER.put(double.class, ValueWriter.DOUBLE);
322: CLASS_TO_VALUE_WRITER.put(float.class, ValueWriter.FLOAT);
323: CLASS_TO_VALUE_WRITER.put(int.class, ValueWriter.INT);
324: CLASS_TO_VALUE_WRITER.put(long.class, ValueWriter.LONG);
325: CLASS_TO_VALUE_WRITER.put(Object.class, ValueWriter.OBJECT);
326: CLASS_TO_VALUE_WRITER.put(short.class, ValueWriter.SHORT);
327: CLASS_TO_VALUE_WRITER.put(String.class, ValueWriter.STRING);
328: }
329:
330: /**
331: * This method takes a string and outputs a JavaScript string literal. The
332: * data is surrounded with quotes, and any contained characters that need to
333: * be escaped are mapped onto their escape sequence.
334: *
335: * Assumptions: We are targeting a version of JavaScript that that is later
336: * than 1.3 that supports unicode strings.
337: */
338: private static String escapeString(String toEscape) {
339: // make output big enough to escape every character (plus the quotes)
340: char[] input = toEscape.toCharArray();
341: CharVector charVector = new CharVector(input.length * 2 + 2,
342: input.length);
343:
344: charVector.add(JS_QUOTE_CHAR);
345:
346: for (int i = 0, n = input.length; i < n; ++i) {
347: char c = input[i];
348: if (c < NUMBER_OF_JS_ESCAPED_CHARS
349: && JS_CHARS_ESCAPED[c] != 0) {
350: charVector.add(JS_ESCAPE_CHAR);
351: charVector.add(JS_CHARS_ESCAPED[c]);
352: } else if (needsUnicodeEscape(c)) {
353: charVector.add(JS_ESCAPE_CHAR);
354: unicodeEscape(c, charVector);
355: } else {
356: charVector.add(c);
357: }
358: }
359:
360: charVector.add(JS_QUOTE_CHAR);
361: return String.valueOf(charVector.asArray(), 0, charVector
362: .getSize());
363: }
364:
365: /**
366: * Returns the {@link Class} instance to use for serialization. Enumerations
367: * are serialized as their declaring class while all others are serialized
368: * using their true class instance.
369: */
370: private static Class<?> getClassForSerialization(Object instance) {
371: assert (instance != null);
372:
373: if (instance instanceof Enum) {
374: Enum<?> e = (Enum<?>) instance;
375: return e.getDeclaringClass();
376: } else {
377: return instance.getClass();
378: }
379: }
380:
381: /**
382: * Returns <code>true</code> if the character requires the \\uXXXX unicode
383: * character escape sequence. This is necessary if the raw character could be
384: * consumed and/or interpreted as a special character when the JSON encoded
385: * response is evaluated. For example, 0x2028 and 0x2029 are alternate line
386: * endings for JS per ECMA-232, which are respected by Firefox and Mozilla.
387: *
388: * @param ch character to check
389: * @return <code>true</code> if the character requires the \\uXXXX unicode
390: * character escape
391: *
392: * Notes:
393: * <ol>
394: * <li> The following cases are a more conservative set of cases which are are
395: * in the future proofing space as opposed to the required minimal set. We
396: * could remove these and still pass our tests.
397: * <ul>
398: * <li>UNASSIGNED - 6359</li>
399: * <li>NON_SPACING_MARK - 530</li>
400: * <li>ENCLOSING_MARK - 10</li>
401: * <li>COMBINING_SPACE_MARK - 131</li>
402: * <li>SPACE_SEPARATOR - 19</li>
403: * <li>CONTROL - 65</li>
404: * <li>PRIVATE_USE - 6400</li>
405: * <li>DASH_PUNCTUATION - 1</li>
406: * <li>Total Characters Escaped: 13515</li>
407: * </ul>
408: * </li>
409: * <li> The following cases are the minimal amount of escaping required to
410: * prevent test failure.
411: * <ul>
412: * <li>LINE_SEPARATOR - 1</li>
413: * <li>PARAGRAPH_SEPARATOR - 1</li>
414: * <li>FORMAT - 32</li>
415: * <li>SURROGATE - 2048</li>
416: * <li>Total Characters Escaped: 2082</li>
417: * </li>
418: * </ul>
419: * </li>
420: * </ol>
421: */
422: private static boolean needsUnicodeEscape(char ch) {
423: switch (Character.getType(ch)) {
424: // Conservative
425: case Character.COMBINING_SPACING_MARK:
426: case Character.ENCLOSING_MARK:
427: case Character.NON_SPACING_MARK:
428: case Character.UNASSIGNED:
429: case Character.PRIVATE_USE:
430: case Character.SPACE_SEPARATOR:
431: case Character.CONTROL:
432:
433: // Minimal
434: case Character.LINE_SEPARATOR:
435: case Character.FORMAT:
436: case Character.PARAGRAPH_SEPARATOR:
437: case Character.SURROGATE:
438: return true;
439:
440: default:
441: if (ch == NON_BREAKING_HYPHEN) {
442: // This can be expanded into a break followed by a hyphen
443: return true;
444: }
445: break;
446: }
447:
448: return false;
449: }
450:
451: /**
452: * Writes either the two or four character escape sequence for a character.
453: *
454: *
455: * @param ch character to unicode escape
456: * @param charVector char vector to receive the unicode escaped representation
457: */
458: private static void unicodeEscape(char ch, CharVector charVector) {
459: if (ch < 256) {
460: charVector.add('x');
461: charVector.add(NIBBLE_TO_HEX_CHAR[(ch >> 4) & 0x0F]);
462: charVector.add(NIBBLE_TO_HEX_CHAR[ch & 0x0F]);
463: } else {
464: charVector.add('u');
465: charVector.add(NIBBLE_TO_HEX_CHAR[(ch >> 12) & 0x0F]);
466: charVector.add(NIBBLE_TO_HEX_CHAR[(ch >> 8) & 0x0F]);
467: charVector.add(NIBBLE_TO_HEX_CHAR[(ch >> 4) & 0x0F]);
468: charVector.add(NIBBLE_TO_HEX_CHAR[ch & 0x0F]);
469: }
470: }
471:
472: private int objectCount;
473:
474: private IdentityHashMap<Object, Integer> objectMap = new IdentityHashMap<Object, Integer>();
475:
476: private HashMap<String, Integer> stringMap = new HashMap<String, Integer>();
477:
478: private ArrayList<String> stringTable = new ArrayList<String>();
479:
480: private ArrayList<String> tokenList = new ArrayList<String>();
481:
482: private int tokenListCharCount;
483:
484: private final SerializationPolicy serializationPolicy;
485:
486: public ServerSerializationStreamWriter(
487: SerializationPolicy serializationPolicy) {
488: this .serializationPolicy = serializationPolicy;
489: }
490:
491: public void prepareToWrite() {
492: objectCount = 0;
493: objectMap.clear();
494: tokenList.clear();
495: tokenListCharCount = 0;
496: stringMap.clear();
497: stringTable.clear();
498: }
499:
500: public void serializeValue(Object value, Class<?> type)
501: throws SerializationException {
502: ValueWriter valueWriter = CLASS_TO_VALUE_WRITER.get(type);
503: if (valueWriter != null) {
504: valueWriter.write(this , value);
505: } else {
506: // Arrays of primitive or reference types need to go through writeObject.
507: ValueWriter.OBJECT.write(this , value);
508: }
509: }
510:
511: /**
512: * Build an array of JavaScript string literals that can be decoded by the
513: * client via the eval function.
514: *
515: * NOTE: We build the array in reverse so the client can simply use the pop
516: * function to remove the next item from the list.
517: */
518: @Override
519: public String toString() {
520: // Build a JavaScript string (with escaping, of course).
521: // We take a guess at how big to make to buffer to avoid numerous resizes.
522: //
523: int capacityGuess = 2 * tokenListCharCount + 2
524: * tokenList.size();
525: StringBuffer buffer = new StringBuffer(capacityGuess);
526: buffer.append("[");
527: writePayload(buffer);
528: writeStringTable(buffer);
529: writeHeader(buffer);
530: buffer.append("]");
531: return buffer.toString();
532: }
533:
534: @Override
535: protected int addString(String string) {
536: if (string == null) {
537: return 0;
538: }
539: Integer o = stringMap.get(string);
540: if (o != null) {
541: return o;
542: }
543: stringTable.add(string);
544: // index is 1-based
545: int index = stringTable.size();
546: stringMap.put(string, index);
547: return index;
548: }
549:
550: @Override
551: protected void append(String token) {
552: tokenList.add(token);
553: if (token != null) {
554: tokenListCharCount += token.length();
555: }
556: }
557:
558: @Override
559: protected int getIndexForObject(Object instance) {
560: Integer o = objectMap.get(instance);
561: if (o != null) {
562: return o;
563: }
564: return -1;
565: }
566:
567: @Override
568: protected String getObjectTypeSignature(Object instance) {
569: assert (instance != null);
570:
571: Class<?> clazz = getClassForSerialization(instance);
572: if (shouldEnforceTypeVersioning()) {
573: return SerializabilityUtil
574: .encodeSerializedInstanceReference(clazz);
575: } else {
576: return SerializabilityUtil.getSerializedTypeName(clazz);
577: }
578: }
579:
580: @Override
581: protected void saveIndexForObject(Object instance) {
582: objectMap.put(instance, objectCount++);
583: }
584:
585: @Override
586: protected void serialize(Object instance, String typeSignature)
587: throws SerializationException {
588: assert (instance != null);
589:
590: Class<?> clazz = getClassForSerialization(instance);
591:
592: serializationPolicy.validateSerialize(clazz);
593:
594: serializeImpl(instance, clazz);
595: }
596:
597: /**
598: * Serialize an instance that is an array. Will default to serializing the
599: * instance as an Object vector if the instance is not a vector of primitives,
600: * Strings or Object.
601: *
602: * @param instanceClass
603: * @param instance
604: * @throws SerializationException
605: */
606: private void serializeArray(Class<?> instanceClass, Object instance)
607: throws SerializationException {
608: assert (instanceClass.isArray());
609:
610: VectorWriter instanceWriter = CLASS_TO_VECTOR_WRITER
611: .get(instanceClass);
612: if (instanceWriter != null) {
613: instanceWriter.write(this , instance);
614: } else {
615: VectorWriter.OBJECT_VECTOR.write(this , instance);
616: }
617: }
618:
619: private void serializeClass(Object instance, Class<?> instanceClass)
620: throws SerializationException {
621: assert (instance != null);
622:
623: Field[] serializableFields = SerializabilityUtil
624: .applyFieldSerializationPolicy(instanceClass);
625: for (Field declField : serializableFields) {
626: assert (declField != null);
627:
628: boolean isAccessible = declField.isAccessible();
629: boolean needsAccessOverride = !isAccessible
630: && !Modifier.isPublic(declField.getModifiers());
631: if (needsAccessOverride) {
632: // Override the access restrictions
633: declField.setAccessible(true);
634: }
635:
636: Object value;
637: try {
638: value = declField.get(instance);
639: serializeValue(value, declField.getType());
640:
641: } catch (IllegalArgumentException e) {
642: throw new SerializationException(e);
643:
644: } catch (IllegalAccessException e) {
645: throw new SerializationException(e);
646: }
647:
648: if (needsAccessOverride) {
649: // Restore the access restrictions
650: declField.setAccessible(isAccessible);
651: }
652: }
653:
654: Class<?> super Class = instanceClass.getSuperclass();
655: if (serializationPolicy.shouldSerializeFields(super Class)) {
656: serializeImpl(instance, super Class);
657: }
658: }
659:
660: private void serializeImpl(Object instance, Class<?> instanceClass)
661: throws SerializationException {
662: assert (instance != null);
663:
664: Class<?> customSerializer = SerializabilityUtil
665: .hasCustomFieldSerializer(instanceClass);
666: if (customSerializer != null) {
667: // Use custom field serializer
668: serializeWithCustomSerializer(customSerializer, instance,
669: instanceClass);
670: } else if (instanceClass.isArray()) {
671: serializeArray(instanceClass, instance);
672: } else if (instanceClass.isEnum()) {
673: writeInt(((Enum<?>) instance).ordinal());
674: } else {
675: // Regular class instance
676: serializeClass(instance, instanceClass);
677: }
678: }
679:
680: private void serializeWithCustomSerializer(
681: Class<?> customSerializer, Object instance,
682: Class<?> instanceClass) throws SerializationException {
683:
684: Method serialize;
685: try {
686: assert (!instanceClass.isArray());
687:
688: serialize = customSerializer.getMethod("serialize",
689: SerializationStreamWriter.class, instanceClass);
690:
691: serialize.invoke(null, this , instance);
692:
693: } catch (SecurityException e) {
694: throw new SerializationException(e);
695:
696: } catch (NoSuchMethodException e) {
697: throw new SerializationException(e);
698:
699: } catch (IllegalArgumentException e) {
700: throw new SerializationException(e);
701:
702: } catch (IllegalAccessException e) {
703: throw new SerializationException(e);
704:
705: } catch (InvocationTargetException e) {
706: throw new SerializationException(e);
707: }
708: }
709:
710: /**
711: * Notice that the field are written in reverse order that the client can just
712: * pop items out of the stream.
713: */
714: private void writeHeader(StringBuffer buffer) {
715: buffer.append(",");
716: buffer.append(getFlags());
717: buffer.append(",");
718: buffer.append(getVersion());
719: }
720:
721: private void writePayload(StringBuffer buffer) {
722: for (int i = tokenList.size() - 1; i >= 0; --i) {
723: String token = tokenList.get(i);
724: buffer.append(token);
725: if (i > 0) {
726: buffer.append(",");
727: }
728: }
729: }
730:
731: private void writeStringTable(StringBuffer buffer) {
732: if (tokenList.size() > 0) {
733: buffer.append(",");
734: }
735: buffer.append("[");
736: for (int i = 0, c = stringTable.size(); i < c; ++i) {
737: if (i > 0) {
738: buffer.append(",");
739: }
740: buffer.append(escapeString(stringTable.get(i)));
741: }
742: buffer.append("]");
743: }
744: }
|