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}