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 = stripBlanksBeforeLineBreaks( text, keepOne );
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        /**
163         * Drops the blanks (spaces, tabs) that sit directly before a line break ({@code \n} or {@code \r\n}),
164         * leaving one blank when {@code keepOne}. The result is that of
165         * {@code text.replaceAll("[ \t]+(\r?\n)", keepOne ? " $1" : "$1")}, in one linear pass: the regex
166         * backtracks quadratically over a long run of blanks that no line break follows.
167         */
168        static String stripBlanksBeforeLineBreaks( String text, boolean keepOne )
169        {
170                StringBuilder out = null;
171                int n = text.length( );
172                int i = 0;
173                while ( i < n )
174                {
175                        char c = text.charAt( i );
176                        if ( c != ' ' && c != '\t' )
177                        {
178                                if ( out != null )
179                                {
180                                        out.append( c );
181                                }
182                                i++;
183                                continue;
184                        }
185                        int end = i;
186                        while ( end < n && ( text.charAt( end ) == ' ' || text.charAt( end ) == '\t' ) )
187                        {
188                                end++;
189                        }
190                        boolean beforeLineBreak = end < n && ( text.charAt( end ) == '\n'
191                                        || ( text.charAt( end ) == '\r' && end + 1 < n && text.charAt( end + 1 ) == '\n' ) );
192                        if ( beforeLineBreak )
193                        {
194                                if ( out == null )
195                                {
196                                        out = new StringBuilder( n ).append( text, 0, i );
197                                }
198                                if ( keepOne )
199                                {
200                                        out.append( ' ' );
201                                }
202                        }
203                        else if ( out != null )
204                        {
205                                out.append( text, i, end );
206                        }
207                        i = end;
208                }
209                return out == null ? text : out.toString( );
210        }
211
212        /** Whitespace / return tokens, including formatter-made ones that carry no type. */
213        private static boolean isWhitespaceToken( TSourceToken t )
214        {
215                if ( t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn )
216                {
217                        return true;
218                }
219                if ( t.tokentype != null )
220                {
221                        return false;
222                }
223                String s = t.toString( );
224                if ( s == null || s.isEmpty( ) )
225                {
226                        return false;
227                }
228                for ( int i = 0; i < s.length( ); i++ )
229                {
230                        char c = s.charAt( i );
231                        if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' )
232                        {
233                                return false;
234                        }
235                }
236                return true;
237        }
238
239        private void printText( String text )
240        {
241                try
242                {
243                        out.write( text.getBytes( ) );
244                }
245                catch ( IOException e )
246                {
247                        e.printStackTrace( );
248                }
249        }
250
251        private static boolean isComment( TSourceToken t )
252        {
253                return t.tokentype == ETokenType.ttsimplecomment
254                                || t.tokentype == ETokenType.ttbracketedcomment
255                                || t.tokentype == ETokenType.ttCPPComment;
256        }
257
258        /**
259         * A token made only of blanks (spaces / tabs), never a return; formatter-made
260         * blanks may carry no type, and an empty placeholder token counts as blank.
261         */
262        private static boolean isBlankToken( TSourceToken t )
263        {
264                if ( t.tokentype != null && t.tokentype != ETokenType.ttwhitespace )
265                {
266                        return false;
267                }
268                String s = t.toString( );
269                if ( s == null || s.isEmpty( ) )
270                {
271                        return true;
272                }
273                for ( int i = 0; i < s.length( ); i++ )
274                {
275                        char c = s.charAt( i );
276                        if ( c != ' ' && c != '\t' )
277                        {
278                                return false;
279                        }
280                }
281                return true;
282        }
283
284        /**
285         * A token ending in "$" (MySQL delimiter, {@code END$}, PostgreSQL {@code $$}):
286         * the blank after it is kept because "$" directly followed by a line break
287         * fuses with the preceding word when the output is lexed again.
288         */
289        private static boolean endsWithDollar( TSourceToken t )
290        {
291                String s = t.toString( );
292                return s != null && !s.isEmpty( ) && s.charAt( s.length( ) - 1 ) == '$';
293        }
294
295        private static boolean startsWithLineBreak( TSourceToken t )
296        {
297                if ( t.tokentype == ETokenType.ttreturn )
298                {
299                        return true;
300                }
301                String s = t.toString( );
302                return s != null && !s.isEmpty( )
303                                && ( s.charAt( 0 ) == '\n' || s.charAt( 0 ) == '\r' );
304        }
305
306        /*
307         * (non-Javadoc)
308         *
309         * @see
310         * gudusoft.gsqlparser.pp.print.IPrinter#print(gudusoft.gsqlparser.TSourceToken
311         * )
312         */
313        public void print( TSourceToken t )
314        {
315                try
316                {
317                        if ( outputConfig != null )
318                        {
319                                out.write( outputConfig.renderHighlightingElement( t )
320                                                .toString( )
321                                                .getBytes( ) );
322                        }
323                        else
324                        {
325                                out.write( t.toString( ).getBytes( ) );
326                        }
327                }
328                catch ( IOException e )
329                {
330                        // TODO Auto-generated catch block
331                        e.printStackTrace( );
332                }
333        }
334
335        public OutputStream getOut( )
336        {
337                return out;
338        }
339
340        public void setOut( OutputStream out )
341        {
342                this.out = out;
343        }
344
345        public void setOutputConfig( OutputConfig outputConfig )
346        {
347                this.outputConfig = outputConfig;
348        }
349
350}