001package gudusoft.gsqlparser.pp.print; 002 003import gudusoft.gsqlparser.ETokenStatus; 004import gudusoft.gsqlparser.ETokenType; 005import gudusoft.gsqlparser.TSourceToken; 006import gudusoft.gsqlparser.TSourceTokenList; 007import gudusoft.gsqlparser.pp.output.OutputConfig; 008import gudusoft.gsqlparser.pp.utils.SourceTokenNameConstant; 009 010import java.io.IOException; 011import java.io.OutputStream; 012import java.util.ArrayList; 013import java.util.List; 014 015/** 016 * the text print 017 * 018 * @author zhoujun 019 * 020 */ 021public class TextPrinter implements IPrinter 022{ 023 024 private OutputStream out = System.out; 025 026 private OutputConfig outputConfig = null; 027 028 /* 029 * (non-Javadoc) 030 * 031 * @see gudusoft.gsqlparser.pp.print.IPrinter#print(gudusoft.gsqlparser. 032 * TSourceTokenList) 033 */ 034 public void print( TSourceTokenList tl ) 035 { 036 List<TSourceToken> flat = new ArrayList<TSourceToken>( tl.size( ) ); 037 flatten( tl, flat ); 038 boolean inNoFormat = false; 039 for ( int i = 0; i < flat.size( ); i++ ) 040 { 041 TSourceToken t = flat.get( i ); 042 if ( isComment( t ) ) 043 { 044 String c = t.toString( ).toLowerCase( java.util.Locale.ROOT ); 045 if ( c.contains( SourceTokenNameConstant.BEGIN_NO_FORMAT ) ) 046 { 047 inNoFormat = true; 048 } 049 else if ( c.contains( SourceTokenNameConstant.END_NO_FORMAT ) ) 050 { 051 inNoFormat = false; 052 } 053 } 054 // Trailing blanks before a line break are never part of a token 055 // (string literals and comments are single tokens), so dropping 056 // them cannot change any token's text. Without this, a whitespace 057 // token left behind a "--" comment became part of that comment 058 // when the output was lexed again, and many lines ended in blanks 059 // (PrettyScore TRAILING_WHITESPACE / COMMENTS_PRESERVED). A 060 // no-format zone is reproduced verbatim, blanks included. 061 if ( !inNoFormat && isWhitespaceToken( t ) ) 062 { 063 // the first non-blank token after this one (blank tokens may come in runs) 064 int j = i + 1; 065 while ( j < flat.size( ) && isBlankToken( flat.get( j ) ) ) 066 { 067 j++; 068 } 069 TSourceToken next = j < flat.size( ) ? flat.get( j ) : null; 070 TSourceToken prev = i > 0 ? flat.get( i - 1 ) : null; 071 // keep one blank after a MySQL "$" delimiter: "END$" directly 072 // followed by a line break lexes as one identifier 073 boolean keepOne = prev != null && endsWithDollar( prev ); 074 String text = t.toString( ); 075 // blanks inside a merged whitespace token (" \n ") that sit 076 // before a line break 077 String stripped = text.replaceAll( "[ \t]+(\r?\n)", keepOne ? " $1" : "$1" ); 078 if ( isBlankToken( t ) && ( next == null || startsWithLineBreak( next ) ) ) 079 { 080 stripped = keepOne ? " " : ""; 081 } 082 if ( !stripped.equals( text ) ) 083 { 084 if ( !stripped.isEmpty( ) ) 085 { 086 printText( stripped ); 087 } 088 continue; 089 } 090 } 091 print( t ); 092 } 093 } 094 095 /** 096 * Expand tokensBefore / replaceToken / tokensAfter in print order, 097 * skipping deleted tokens. 098 */ 099 private static void flatten( TSourceTokenList tl, List<TSourceToken> out ) 100 { 101 TSourceToken activeSplit = null; 102 for ( int i = 0; i < tl.size( ); i++ ) 103 { 104 TSourceToken t = tl.get( i ); 105 if ( t.tokenstatus == ETokenStatus.tsdeleted ) 106 { 107 continue; 108 } 109 // A run of tokens the vendor tokenizer split from one source token 110 // (BigQuery `p.d.t`) is printed as that source token, once: the 111 // user's spelling is not the formatter's to change. 112 if ( t.splitFrom != null ) 113 { 114 boolean first = activeSplit != t.splitFrom; 115 boolean last = true; 116 for ( int j = i + 1; j < tl.size( ); j++ ) 117 { 118 TSourceToken n = tl.get( j ); 119 if ( n.tokenstatus == ETokenStatus.tsdeleted ) 120 { 121 continue; 122 } 123 last = n.splitFrom != t.splitFrom; 124 break; 125 } 126 if ( first ) 127 { 128 activeSplit = t.splitFrom; 129 if ( t.getTokensBefore( ) != null && t.getTokensBefore( ).size( ) > 0 ) 130 { 131 flatten( t.getTokensBefore( ), out ); 132 } 133 out.add( t.splitFrom ); 134 } 135 if ( last && t.getTokensAfter( ) != null && t.getTokensAfter( ).size( ) > 0 ) 136 { 137 flatten( t.getTokensAfter( ), out ); 138 } 139 continue; 140 } 141 activeSplit = null; 142 if ( t.getTokensBefore( ) != null 143 && t.getTokensBefore( ).size( ) > 0 ) 144 { 145 flatten( t.getTokensBefore( ), out ); 146 } 147 if ( t.getReplaceToken( ) != null ) 148 { 149 out.add( t.getReplaceToken( ) ); 150 } 151 else 152 { 153 out.add( t ); 154 } 155 if ( t.getTokensAfter( ) != null && t.getTokensAfter( ).size( ) > 0 ) 156 { 157 flatten( t.getTokensAfter( ), out ); 158 } 159 } 160 } 161 162 /** Whitespace / return tokens, including formatter-made ones that carry no type. */ 163 private static boolean isWhitespaceToken( TSourceToken t ) 164 { 165 if ( t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn ) 166 { 167 return true; 168 } 169 if ( t.tokentype != null ) 170 { 171 return false; 172 } 173 String s = t.toString( ); 174 if ( s == null || s.isEmpty( ) ) 175 { 176 return false; 177 } 178 for ( int i = 0; i < s.length( ); i++ ) 179 { 180 char c = s.charAt( i ); 181 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' ) 182 { 183 return false; 184 } 185 } 186 return true; 187 } 188 189 private void printText( String text ) 190 { 191 try 192 { 193 out.write( text.getBytes( ) ); 194 } 195 catch ( IOException e ) 196 { 197 e.printStackTrace( ); 198 } 199 } 200 201 private static boolean isComment( TSourceToken t ) 202 { 203 return t.tokentype == ETokenType.ttsimplecomment 204 || t.tokentype == ETokenType.ttbracketedcomment 205 || t.tokentype == ETokenType.ttCPPComment; 206 } 207 208 /** 209 * A token made only of blanks (spaces / tabs), never a return; formatter-made 210 * blanks may carry no type, and an empty placeholder token counts as blank. 211 */ 212 private static boolean isBlankToken( TSourceToken t ) 213 { 214 if ( t.tokentype != null && t.tokentype != ETokenType.ttwhitespace ) 215 { 216 return false; 217 } 218 String s = t.toString( ); 219 if ( s == null || s.isEmpty( ) ) 220 { 221 return true; 222 } 223 for ( int i = 0; i < s.length( ); i++ ) 224 { 225 char c = s.charAt( i ); 226 if ( c != ' ' && c != '\t' ) 227 { 228 return false; 229 } 230 } 231 return true; 232 } 233 234 /** 235 * A token ending in "$" (MySQL delimiter, {@code END$}, PostgreSQL {@code $$}): 236 * the blank after it is kept because "$" directly followed by a line break 237 * fuses with the preceding word when the output is lexed again. 238 */ 239 private static boolean endsWithDollar( TSourceToken t ) 240 { 241 String s = t.toString( ); 242 return s != null && !s.isEmpty( ) && s.charAt( s.length( ) - 1 ) == '$'; 243 } 244 245 private static boolean startsWithLineBreak( TSourceToken t ) 246 { 247 if ( t.tokentype == ETokenType.ttreturn ) 248 { 249 return true; 250 } 251 String s = t.toString( ); 252 return s != null && !s.isEmpty( ) 253 && ( s.charAt( 0 ) == '\n' || s.charAt( 0 ) == '\r' ); 254 } 255 256 /* 257 * (non-Javadoc) 258 * 259 * @see 260 * gudusoft.gsqlparser.pp.print.IPrinter#print(gudusoft.gsqlparser.TSourceToken 261 * ) 262 */ 263 public void print( TSourceToken t ) 264 { 265 try 266 { 267 if ( outputConfig != null ) 268 { 269 out.write( outputConfig.renderHighlightingElement( t ) 270 .toString( ) 271 .getBytes( ) ); 272 } 273 else 274 { 275 out.write( t.toString( ).getBytes( ) ); 276 } 277 } 278 catch ( IOException e ) 279 { 280 // TODO Auto-generated catch block 281 e.printStackTrace( ); 282 } 283 } 284 285 public OutputStream getOut( ) 286 { 287 return out; 288 } 289 290 public void setOut( OutputStream out ) 291 { 292 this.out = out; 293 } 294 295 public void setOutputConfig( OutputConfig outputConfig ) 296 { 297 this.outputConfig = outputConfig; 298 } 299 300}