| /* |
| ******************************************************************************* |
| * Copyright (C) 2010-2013, International Business Machines Corporation and |
| * others. All Rights Reserved. |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.dev.test.bidi; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| |
| import com.ibm.icu.dev.test.TestFmwk; |
| import com.ibm.icu.dev.test.TestUtil; |
| import com.ibm.icu.lang.UCharacterDirection; |
| import com.ibm.icu.text.Bidi; |
| import com.ibm.icu.text.BidiClassifier; |
| |
| /** |
| * @author Markus W. Scherer |
| * BiDi conformance test, using the Unicode BidiTest.txt and BidiCharacterTest.txt files. |
| * Ported from ICU4C intltest/bidiconf.cpp . |
| */ |
| public class BiDiConformanceTest extends TestFmwk { |
| public static void main(String[] args) throws Exception { |
| new BiDiConformanceTest().run(args); |
| } |
| public BiDiConformanceTest() {} |
| |
| public void TestBidiTest() throws IOException { |
| BufferedReader bidiTestFile=TestUtil.getDataReader("unicode/BidiTest.txt"); |
| Bidi ubidi=new Bidi(); |
| ubidi.setCustomClassifier(new ConfTestBidiClassifier()); |
| lineNumber=0; |
| levelsCount=0; |
| orderingCount=0; |
| errorCount=0; |
| outerLoop: |
| while(errorCount<10 && (line=bidiTestFile.readLine())!=null) { |
| ++lineNumber; |
| lineIndex=0; |
| // Remove trailing comments and whitespace. |
| int commentStart=line.indexOf('#'); |
| if(commentStart>=0) { |
| line=line.substring(0, commentStart); |
| } |
| if(!skipWhitespace()) { |
| continue; // Skip empty and comment-only lines. |
| } |
| if(line.charAt(lineIndex)=='@') { |
| ++lineIndex; |
| if(line.startsWith("Levels:", lineIndex)) { |
| lineIndex+=7; |
| if(!parseLevels(line.substring(lineIndex))) { break; } |
| } else if(line.startsWith("Reorder:", lineIndex)) { |
| lineIndex+=8; |
| if(!parseOrdering(line.substring(lineIndex))) { break; } |
| } |
| // Skip unknown @Xyz: ... |
| } else { |
| parseInputStringFromBiDiClasses(); |
| if(!skipWhitespace() || line.charAt(lineIndex++)!=';') { |
| errln("missing ; separator on input line "+line); |
| return; |
| } |
| int bitset=Integer.parseInt(line.substring(lineIndex).trim(), 16); |
| // Loop over the bitset. |
| for(int i=0; i<=3; ++i) { |
| if((bitset&(1<<i))!=0) { |
| ubidi.setPara(inputString, paraLevels[i], null); |
| byte actualLevels[]=ubidi.getLevels(); |
| paraLevelName=paraLevelNames[i]; |
| if(!checkLevels(actualLevels)) { |
| continue outerLoop; |
| } |
| if(!checkOrdering(ubidi)) { |
| continue outerLoop; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /* |
| ******************************************************************************* |
| * |
| * created on: 2013jul01 |
| * created by: Matitiahu Allouche |
| |
| This function performs a conformance test for implementations of the |
| Unicode Bidirectional Algorithm, specified in UAX #9: Unicode |
| Bidirectional Algorithm, at http://www.unicode.org/unicode/reports/tr9/ |
| |
| Each test case is represented in a single line which is read from a file |
| named BidiCharacter.txt. Empty, blank and comment lines may also appear |
| in this file. |
| |
| The format of the test data is specified below. Note that each test |
| case constitutes a single line of text; reordering is applied within a |
| single line and independently of a rendering engine, and rules L3 and L4 |
| are out of scope. |
| |
| The number sign '#' is the comment character: everything is ignored from |
| the occurrence of '#' until the end of the line, |
| Empty lines and lines containing only spaces and/or comments are ignored. |
| |
| Lines which represent test cases consist of 4 or 5 fields separated by a |
| semicolon. Each field consists of tokens separated by whitespace (space |
| or Tab). Whitespace before and after semicolons is optional. |
| |
| Field 0: A sequence of hexadecimal code point values separated by space |
| |
| Field 1: A value representing the paragraph direction, as follows: |
| - 0 represents left-to-right |
| - 1 represents right-to-left |
| - 2 represents auto-LTR according to rules P2 and P3 of the algorithm |
| - 3 represents auto-RTL according to rules P2 and P3 of the algorithm |
| - a negative number whose absolute value is taken as paragraph level; |
| this may be useful to test cases where the embedding level approaches |
| or exceeds the maximum embedding level. |
| |
| Field 2: The resolved paragraph embedding level. If the input (field 0) |
| includes more than one paragraph, this field represents the |
| resolved level of the first paragraph. |
| |
| Field 3: An ordered list of resulting levels for each token in field 0 |
| (each token represents one source character). |
| The UBA does not assign levels to certain characters (e.g. LRO); |
| characters removed in rule X9 are indicated with an 'x'. |
| |
| Field 4: An ordered list of indices showing the resulting visual ordering |
| from left to right; characters with a resolved level of 'x' are |
| skipped. The number are zero-based. Each index corresponds to |
| a character in the reordered (visual) string. It represents the |
| index of the source character in the input (field 0). |
| This field is optional. When it is absent, the visual ordering |
| is not verified. |
| |
| Examples: |
| |
| # This is a comment line. |
| L L ON R ; 0 ; 0 ; 0 0 0 1 ; 0 1 2 3 |
| L L ON R;0;0;0 0 0 1;0 1 2 3 |
| |
| # Note: in the next line, 'B' represents a block separator, not the letter 'B'. |
| LRE A B C PDF;2;0;x 2 0 0 x;1 2 3 |
| # Note: in the next line, 'b' represents the letter 'b', not a block separator. |
| a b c 05d0 05d1 x ; 0 ; 0 ; 0 0 0 1 1 0 ; 0 1 2 4 3 5 |
| |
| a R R x ; 1 ; 1 ; 2 1 1 2 |
| L L R R R B R R L L L B ON ON ; 3 ; 0 ; 0 0 1 1 1 0 1 1 2 2 2 1 1 1 |
| |
| * |
| ******************************************************************************* |
| */ |
| public void TestBidiCharacterTest() throws IOException { |
| BufferedReader bidiTestFile=TestUtil.getDataReader("unicode/BidiCharacterTest.txt"); |
| Bidi ubidi=new Bidi(); |
| lineNumber=0; |
| levelsCount=0; |
| orderingCount=0; |
| errorCount=0; |
| outerLoop: |
| while(errorCount<20 && (line=bidiTestFile.readLine())!=null) { |
| ++lineNumber; |
| paraLevelName="N/A"; |
| inputString="N/A"; |
| lineIndex=0; |
| // Remove trailing comments and whitespace. |
| int commentStart=line.indexOf('#'); |
| if(commentStart>=0) { |
| line=line.substring(0, commentStart); |
| } |
| if(!skipWhitespace()) { |
| continue; // Skip empty and comment-only lines. |
| } |
| String[] parts=line.split(";"); |
| if(parts.length<4) { |
| errorCount++; |
| errln(" on line " + lineNumber + ": Missing ; separator on line: " + line); |
| continue; |
| } |
| // Parse the code point string in field 0. |
| try { |
| inputStringBuilder.delete(0, inputStringBuilder.length()); |
| for(String cp : parts[0].trim().split("[ \t]+")) { |
| inputStringBuilder.appendCodePoint(Integer.parseInt(cp, 16)); |
| } |
| inputString=inputStringBuilder.toString(); |
| } catch(Exception e) { |
| errln(" ------------ Invalid string in field 0 on line '"+line+"'"); |
| ++errorCount; |
| continue; |
| } |
| int paraDirection=intFromString(parts[1].trim()); |
| byte paraLevel; |
| if(paraDirection==0) { |
| paraLevel=0; |
| paraLevelName="LTR"; |
| } |
| else if(paraDirection==1) { |
| paraLevel=1; |
| paraLevelName="RTL"; |
| } |
| else if(paraDirection==2) { |
| paraLevel=Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; |
| paraLevelName="Auto/LTR"; |
| } |
| else if(paraDirection==3) { |
| paraLevel=Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; |
| paraLevelName="Auto/RTL"; |
| } |
| else if(paraDirection<0 && -paraDirection<=(Bidi.MAX_EXPLICIT_LEVEL+1)) { |
| paraLevel=(byte)(-paraDirection); |
| paraLevelName=Byte.toString(paraLevel); |
| } |
| else { |
| errorCount++; |
| errln(" on line " + lineNumber + ": Input paragraph direction incorrect at " + line); |
| continue; |
| } |
| int resolvedParaLevel=intFromString(parts[2].trim()); |
| if(resolvedParaLevel<0 || resolvedParaLevel>(Bidi.MAX_EXPLICIT_LEVEL+1)) { |
| errorCount++; |
| errln(" on line " + lineNumber + ": Resolved paragraph level incorrect at " + line); |
| continue; |
| } |
| if(!parseLevels(parts[3])) { |
| continue; |
| } |
| if(parts.length>4) { |
| if(!parseOrdering(parts[4])) |
| continue; |
| } |
| else |
| orderingCount=-1; |
| |
| ubidi.setPara(inputString, paraLevel, null); |
| byte actualParaLevel=ubidi.getParaLevel(); |
| if(actualParaLevel!=resolvedParaLevel) { |
| errln(" ------------ Wrong resolved paragraph level; expected " |
| +resolvedParaLevel+" actual " |
| +actualParaLevel); |
| printErrorLine(); |
| } |
| byte[] actualLevels=ubidi.getLevels(); |
| if(!checkLevels(actualLevels)) { |
| continue outerLoop; |
| } |
| if(!checkOrdering(ubidi)) { |
| continue outerLoop; |
| } |
| } |
| } |
| |
| private static final byte paraLevels[]={ |
| Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT, |
| 0, |
| 1, |
| Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT |
| }; |
| private static final String paraLevelNames[]={ "auto/LTR", "LTR", "RTL", "auto/RTL" }; |
| |
| private int intFromString(String str) { |
| try { |
| return Integer.parseInt(str); |
| } catch (Exception e) { |
| return -9999; |
| } |
| } |
| |
| private boolean parseLevels(String s) { |
| directionBits=0; |
| levelsCount=0; |
| String[] levelStrings=s.trim().split("[ \t]+"); |
| for(String levelString: levelStrings) { |
| if(levelString.length()==0) { continue; } |
| if(levelString.equals("x")) { |
| levels[levelsCount++]=-1; |
| } else { |
| try { |
| int value=Integer.parseInt(levelString); |
| if(0<=value && value<=(Bidi.MAX_EXPLICIT_LEVEL+1)) { |
| levels[levelsCount++]=(byte)value; |
| directionBits|=(1<<(value&1)); |
| continue; |
| } |
| } catch(Exception e) { |
| } |
| errln(" ------------ Levels parse error at '"+levelString+"'"); |
| printErrorLine(); |
| return false; |
| } |
| } |
| return true; |
| } |
| private boolean parseOrdering(String s) { |
| orderingCount=0; |
| String[] orderingStrings=s.trim().split("[ \t]+"); |
| for(String orderingString: orderingStrings) { |
| if(orderingString.length()==0) { continue; } |
| try { |
| int value=Integer.parseInt(orderingString); |
| if(value<1000) { |
| ordering[orderingCount++]=value; |
| continue; |
| } |
| } catch(Exception e) { |
| } |
| errln(" ------------ Reorder parse error at '"+orderingString+"'"); |
| printErrorLine(); |
| return false; |
| } |
| return true; |
| } |
| private static char charFromBiDiClass[]={ |
| 0x6c, // 'l' for L |
| 0x52, // 'R' for R |
| 0x33, // '3' for EN |
| 0x2d, // '-' for ES |
| 0x25, // '%' for ET |
| 0x39, // '9' for AN |
| 0x2c, // ',' for CS |
| 0x2f, // '/' for B |
| 0x5f, // '_' for S |
| 0x20, // ' ' for WS |
| 0x3d, // '=' for ON |
| 0x65, // 'e' for LRE |
| 0x6f, // 'o' for LRO |
| 0x41, // 'A' for AL |
| 0x45, // 'E' for RLE |
| 0x4f, // 'O' for RLO |
| 0x2a, // '*' for PDF |
| 0x60, // '`' for NSM |
| 0x7c, // '|' for BN |
| // new in Unicode 6.3/ICU 52 |
| 0x53, // 'S' for FSI |
| 0x69, // 'i' for LRI |
| 0x49, // 'I' for RLI |
| 0x2e // '.' for PDI |
| }; |
| private class ConfTestBidiClassifier extends BidiClassifier { |
| public ConfTestBidiClassifier() { |
| super(null); |
| } |
| @Override |
| public int classify(int c) { |
| for(int i=0; i<charFromBiDiClass.length; ++i) { |
| if(c==charFromBiDiClass[i]) { |
| return i; |
| } |
| } |
| // Character not in our hardcoded table. |
| // Should not occur during testing. |
| return Bidi.CLASS_DEFAULT; |
| } |
| } |
| private static final int biDiClassNameLengths[]={ |
| 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 2, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 0 |
| }; |
| private void parseInputStringFromBiDiClasses() { |
| inputStringBuilder.delete(0, 0x7fffffff); |
| /* |
| * Lengthy but fast BiDi class parser. |
| * A simple parser could terminate or extract the name string and use |
| * int32_t biDiClassInt=u_getPropertyValueEnum(UCHAR_BIDI_CLASS, bidiClassString); |
| * but that makes this test take significantly more time. |
| */ |
| char c0, c1, c2; |
| while(skipWhitespace() && (c0=line.charAt(lineIndex))!=';') { |
| int biDiClass=UCharacterDirection.CHAR_DIRECTION_COUNT; |
| // Compare each character once until we have a match on |
| // a complete, short BiDi class name. |
| if(c0=='L') { |
| if((lineIndex+2)<line.length() && line.charAt(lineIndex+1)=='R') { |
| c2=line.charAt(lineIndex+2); |
| if(c2=='E') { |
| biDiClass=UCharacterDirection.LEFT_TO_RIGHT_EMBEDDING; |
| } else if(c2=='I') { |
| biDiClass=UCharacterDirection.LEFT_TO_RIGHT_ISOLATE; |
| } else if(c2=='O') { |
| biDiClass=UCharacterDirection.LEFT_TO_RIGHT_OVERRIDE; |
| } |
| } else { |
| biDiClass=UCharacterDirection.LEFT_TO_RIGHT; |
| } |
| } else if(c0=='R') { |
| if((lineIndex+2)<line.length() && line.charAt(lineIndex+1)=='L') { |
| c2=line.charAt(lineIndex+2); |
| if(c2=='E') { |
| biDiClass=UCharacterDirection.RIGHT_TO_LEFT_EMBEDDING; |
| } else if(c2=='I') { |
| biDiClass=UCharacterDirection.RIGHT_TO_LEFT_ISOLATE; |
| } else if(c2=='O') { |
| biDiClass=UCharacterDirection.RIGHT_TO_LEFT_OVERRIDE; |
| } |
| } else { |
| biDiClass=UCharacterDirection.RIGHT_TO_LEFT; |
| } |
| } else if(c0=='E') { |
| if((lineIndex+1)>=line.length()) { |
| // too short |
| } else if((c1=line.charAt(lineIndex+1))=='N') { |
| biDiClass=UCharacterDirection.EUROPEAN_NUMBER; |
| } else if(c1=='S') { |
| biDiClass=UCharacterDirection.EUROPEAN_NUMBER_SEPARATOR; |
| } else if(c1=='T') { |
| biDiClass=UCharacterDirection.EUROPEAN_NUMBER_TERMINATOR; |
| } |
| } else if(c0=='A') { |
| if((lineIndex+1)>=line.length()) { |
| // too short |
| } else if((c1=line.charAt(lineIndex+1))=='L') { |
| biDiClass=UCharacterDirection.RIGHT_TO_LEFT_ARABIC; |
| } else if(c1=='N') { |
| biDiClass=UCharacterDirection.ARABIC_NUMBER; |
| } |
| } else if(c0=='C' && (lineIndex+1)<line.length() && line.charAt(lineIndex+1)=='S') { |
| biDiClass=UCharacterDirection.COMMON_NUMBER_SEPARATOR; |
| } else if(c0=='B') { |
| if((lineIndex+1)<line.length() && line.charAt(lineIndex+1)=='N') { |
| biDiClass=UCharacterDirection.BOUNDARY_NEUTRAL; |
| } else { |
| biDiClass=UCharacterDirection.BLOCK_SEPARATOR; |
| } |
| } else if(c0=='S') { |
| biDiClass=UCharacterDirection.SEGMENT_SEPARATOR; |
| } else if(c0=='W' && (lineIndex+1)<line.length() && line.charAt(lineIndex+1)=='S') { |
| biDiClass=UCharacterDirection.WHITE_SPACE_NEUTRAL; |
| } else if(c0=='O' && (lineIndex+1)<line.length() && line.charAt(lineIndex+1)=='N') { |
| biDiClass=UCharacterDirection.OTHER_NEUTRAL; |
| } else if(c0=='P' && (lineIndex+2)<line.length() && line.charAt(lineIndex+1)=='D') { |
| if(line.charAt(lineIndex+2)=='F') { |
| biDiClass=UCharacterDirection.POP_DIRECTIONAL_FORMAT; |
| } else if(line.charAt(lineIndex+2)=='I') { |
| biDiClass=UCharacterDirection.POP_DIRECTIONAL_ISOLATE; |
| } |
| } else if(c0=='N' && (lineIndex+2)<line.length() && |
| line.charAt(lineIndex+1)=='S' && line.charAt(lineIndex+2)=='M') { |
| biDiClass=UCharacterDirection.DIR_NON_SPACING_MARK; |
| } else if(c0=='F' && (lineIndex+2)<line.length() && |
| line.charAt(lineIndex+1)=='S' && line.charAt(lineIndex+2)=='I') { |
| biDiClass=UCharacterDirection.FIRST_STRONG_ISOLATE; |
| } |
| // Now we verify that the class name is terminated properly, |
| // and not just the start of a longer word. |
| int biDiClassNameLength=biDiClassNameLengths[biDiClass]; |
| char c; |
| if( biDiClass==UCharacterDirection.CHAR_DIRECTION_COUNT || |
| ((lineIndex+biDiClassNameLength)<line.length() && |
| !isInvWhitespace(c=line.charAt(lineIndex+biDiClassNameLength)) && |
| c!=';') |
| ) { |
| throw new IllegalArgumentException( |
| "BiDi class string not recognized at "+line.substring(lineIndex)+" in "+line); |
| } |
| inputStringBuilder.append(charFromBiDiClass[biDiClass]); |
| lineIndex+=biDiClassNameLength; |
| } |
| inputString=inputStringBuilder.toString(); |
| } |
| |
| private static char printLevel(byte level) { |
| if(level<0) { |
| return 'x'; |
| } else { |
| return (char)('0'+level); |
| } |
| } |
| |
| private static int getDirectionBits(byte actualLevels[]) { |
| int actualDirectionBits=0; |
| for(int i=0; i<actualLevels.length; ++i) { |
| actualDirectionBits|=(1<<(actualLevels[i]&1)); |
| } |
| return actualDirectionBits; |
| } |
| private boolean checkLevels(byte actualLevels[]) { |
| boolean isOk=true; |
| if(levelsCount!=actualLevels.length) { |
| errln(" ------------ Wrong number of level values; expected "+levelsCount+" actual "+actualLevels.length); |
| isOk=false; |
| } else { |
| for(int i=0; i<actualLevels.length; ++i) { |
| if(levels[i]!=actualLevels[i] && levels[i]>=0) { |
| if(directionBits!=3 && directionBits==getDirectionBits(actualLevels)) { |
| // ICU used a shortcut: |
| // Since the text is unidirectional, it did not store the resolved |
| // levels but just returns all levels as the paragraph level 0 or 1. |
| // The reordering result is the same, so this is fine. |
| break; |
| } else { |
| errln(" ------------ Wrong level value at index "+i+"; expected "+levels[i]+" actual "+actualLevels[i]); |
| isOk=false; |
| break; |
| } |
| } |
| } |
| } |
| if(!isOk) { |
| printErrorLine(); |
| StringBuilder els=new StringBuilder("Expected levels: "); |
| int i; |
| for(i=0; i<levelsCount; ++i) { |
| els.append(' ').append(printLevel(levels[i])); |
| } |
| StringBuilder als=new StringBuilder("Actual levels: "); |
| for(i=0; i<actualLevels.length; ++i) { |
| als.append(' ').append(printLevel(actualLevels[i])); |
| } |
| errln(els.toString()); |
| errln(als.toString()); |
| } |
| return isOk; |
| } |
| |
| // Note: ubidi_setReorderingOptions(ubidi, UBIDI_OPTION_REMOVE_CONTROLS); |
| // does not work for custom BiDi class assignments |
| // and anyway also removes LRM/RLM/ZWJ/ZWNJ which is not desirable here. |
| // Therefore we just skip the indexes for BiDi controls while comparing |
| // with the expected ordering that has them omitted. |
| private boolean checkOrdering(Bidi ubidi) { |
| if(orderingCount<0) |
| return true; |
| boolean isOk=true; |
| int resultLength=ubidi.getResultLength(); // visual length including BiDi controls |
| int i, visualIndex; |
| // Note: It should be faster to call ubidi_countRuns()/ubidi_getVisualRun() |
| // and loop over each run's indexes, but that seems unnecessary for this test code. |
| for(i=visualIndex=0; i<resultLength; ++i) { |
| int logicalIndex=ubidi.getLogicalIndex(i); |
| if(levels[logicalIndex]<0) { |
| continue; // BiDi control, omitted from expected ordering. |
| } |
| if(visualIndex<orderingCount && logicalIndex!=ordering[visualIndex]) { |
| errln(" ------------ Wrong ordering value at visual index "+visualIndex+"; expected "+ |
| ordering[visualIndex]+" actual "+logicalIndex); |
| isOk=false; |
| break; |
| } |
| ++visualIndex; |
| } |
| // visualIndex is now the visual length minus the BiDi controls, |
| // which should match the length of the BidiTest.txt ordering. |
| if(isOk && orderingCount!=visualIndex) { |
| errln(" ------------ Wrong number of ordering values; expected "+orderingCount+" actual "+visualIndex); |
| isOk=false; |
| } |
| if(!isOk) { |
| printErrorLine(); |
| StringBuilder eord=new StringBuilder("Expected ordering: "); |
| for(i=0; i<orderingCount; ++i) { |
| eord.append(' ').append((char)('0'+ordering[i])); |
| } |
| StringBuilder aord=new StringBuilder("Actual ordering: "); |
| for(i=0; i<resultLength; ++i) { |
| int logicalIndex=ubidi.getLogicalIndex(i); |
| if(levels[logicalIndex]<Bidi.LEVEL_DEFAULT_LTR) { |
| aord.append(' ').append((char)('0'+logicalIndex)); |
| } |
| } |
| errln(eord.toString()); |
| errln(aord.toString()); |
| } |
| return isOk; |
| } |
| |
| private void printErrorLine() { |
| ++errorCount; |
| errln(String.format("Input line %5d: %s", lineNumber, line)); |
| errln("Input string: "+inputString); |
| errln("Para level: "+paraLevelName); |
| } |
| |
| private static boolean isInvWhitespace(char c) { |
| return ((c)==' ' || (c)=='\t' || (c)=='\r' || (c)=='\n'); |
| } |
| /** |
| * Skip isInvWhitespace() characters. |
| * @return true if line.charAt[lineIndex] is a non-whitespace, false if lineIndex>=line.length() |
| */ |
| private boolean skipWhitespace() { |
| while(lineIndex<line.length()) { |
| if(!isInvWhitespace(line.charAt(lineIndex))) { |
| return true; |
| } |
| ++lineIndex; |
| } |
| return false; |
| } |
| |
| private String line; |
| private int lineIndex; |
| private byte levels[]=new byte[1000]; // UBiDiLevel |
| private int directionBits; |
| private int ordering[]=new int[1000]; |
| private int lineNumber; |
| private int levelsCount; |
| private int orderingCount; |
| private int errorCount; |
| private String inputString; |
| private String paraLevelName; |
| private StringBuilder inputStringBuilder=new StringBuilder(); |
| } |