001package votorola.a.response; // Copyright 2007-2008, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.util.*;
004import votorola.g.locale.*;
005import votorola.g.lang.*;
006
007
008/** A composite resource bundle, string formatter and character buffer.  It builds a
009  * plain-text, line-wrapped, message.
010  */
011public final @ThreadRestricted class ReplyBuilder extends BundleFormatter implements Appendable
012{
013
014
015    /** Constructs a ReplyBuilder.
016      *
017      *     @param bundle the resource bundle to use, per {@linkplain #bundle() bundle}()
018      */
019    public ReplyBuilder( ResourceBundle bundle ) { super( bundle ); }
020
021
022
023    /** Constructs a command/response (CR) ReplyBuilder, for building a reply
024      * to a command.  It uses bundle base name 'votorola.a.locale.CR'.
025      *
026      *     @param l locale, per {@linkplain #locale() locale}()
027      *
028      *     @see <a href='../../../../a/locale/CR.properties'>
029      *                                 locale/CR.properties</a>
030      */
031    public ReplyBuilder( Locale l )
032    {
033        super( ResourceBundle.getBundle( "votorola.a.locale.CR", l ));
034    }
035
036
037
038   // ------------------------------------------------------------------------------------
039
040
041    /** Appends the string representation of the specified integer to the buffer.
042      */
043    public ReplyBuilder append( int i )
044    {
045        stringBuilder().append( i );
046        return ReplyBuilder.this;
047    }
048
049
050
051    /** Appends the string representation of the specified long integer to the buffer.
052      */
053    public ReplyBuilder append( long l )
054    {
055        stringBuilder().append( l );
056        return ReplyBuilder.this;
057    }
058
059
060
061    /** Appends a newline.
062      */
063    public ReplyBuilder appendln()
064    {
065        stringBuilder().append( '\n' );
066        return ReplyBuilder.this;
067    }
068
069
070        /** Appends a double newline.
071          */
072        public ReplyBuilder appendlnn()
073        {
074            stringBuilder().append( '\n' ).append( '\n' );
075            return ReplyBuilder.this;
076        }
077
078
079
080    /** Appends n copies of character c to the buffer.
081      */
082    public ReplyBuilder appendRepeat( final char c, final int n )
083    {
084        final StringBuilder sB = stringBuilder();
085        for( int i = 0; i < n; ++i ) sB.append( c );
086        return ReplyBuilder.this;
087    }
088
089
090        /** Appends n copies of character c, plus a newline, to the buffer.
091          */
092        public ReplyBuilder appendRepeatln( final char c, final int n )
093        {
094            appendRepeat( c, n );
095            return appendln();
096        }
097
098
099        /** Appends n copies of character c, plus a double newline, to the buffer.
100          */
101        public ReplyBuilder appendRepeatlnn( final char c, final int n )
102        {
103            appendRepeat( c, n );
104            return appendlnn();
105        }
106
107
108
109    /** Chops any trailing double newline down to a single newline.
110      * Useful prior to calling {@linkplain #toString toString}().
111      */
112    public ReplyBuilder chomplnn()
113    {
114        final StringBuilder sB = stringBuilder();
115        int cLast = sB.length() - 1;
116        if( cLast > 0 && sB.charAt(cLast) == '\n' && sB.charAt(cLast-1) == '\n' )
117        {
118            sB.deleteCharAt( cLast );
119        }
120        return ReplyBuilder.this;
121    }
122
123
124
125    /** Decreases the left wrap-margin, undoing a previous indent.
126      *
127      *     @see #indent(int)
128      */
129    public ReplyBuilder exdent( int amount )
130    {
131        sync();
132        leftMargin -= amount;
133        return ReplyBuilder.this;
134    }
135
136
137
138    /** Appends some spaces, and increases the left wrap-margin accordingly.
139      *
140      *     @see #exdent(int)
141      */
142    public ReplyBuilder indent( int amount )
143    {
144        sync();
145        leftMargin += amount;
146        return ReplyBuilder.this;
147    }
148
149
150
151    /** Returns true if wrapping is enabled; false otherwise.  When enabled, lines longer
152      * than {@linkplain #WRAPPED_WIDTH WRAPPED_WIDTH} will automatically wrap back to the
153      * left margin.  By default, it is enabled.
154      *
155      *     @see #setWrapping
156      */
157    public final boolean isWrapping() { return isWrapping; };
158
159
160        private boolean isWrapping = true;
161
162
163        /** Sets whether wrapping is enabled.
164          *
165          *     @see #isWrapping
166          */
167        public final ReplyBuilder setWrapping( boolean toWrap )
168        {
169            sync();
170            isWrapping = toWrap;
171            return ReplyBuilder.this;
172        }
173
174
175
176    public @Override ReplyBuilder lappend( final String key, final Object... args )
177    {
178        return (ReplyBuilder)super.lappend( key, args );
179    }
180
181
182        /** Appends a localized string, plus a newline, to the buffer.
183          *
184          *     @param key {@linkplain #bundle() bundle} key of the string
185          *     @param args arguments for insertion in the string,
186          *         per {@linkplain Formatter#format(String,Object[]) format}(String,Object[])
187          */
188        public ReplyBuilder lappendln( final String key, final Object... args )
189        {
190            lappend( key, args );
191            return appendln();
192        }
193
194
195        /** Appends a localized string, plus a double newline, to the buffer.
196          *
197          *     @param key {@linkplain #bundle() bundle} key of the string
198          *     @param args arguments for insertion in the string,
199          *         per {@linkplain Formatter#format(String,Object[]) format}(String,Object[])
200          */
201        public ReplyBuilder lappendlnn( final String key, final Object... args )
202        {
203            lappend( key, args );
204            return appendlnn();
205        }
206
207
208
209    /** Returns the current length (character count) of the buffer.
210      */
211    public int length()
212    {
213        sync();
214        return stringBuilder().length();
215    }
216
217
218
219    /** Set indentation and wrapping to defaults.
220      *
221      *     @see #indent(int)
222      *     @see #isWrapping
223      */
224    public final ReplyBuilder resetFormattingToDefaults() // a formatting stack (push/pop) would be cleaner, so nested subroutines could avoid clobbering caller's settings
225    {
226        sync();
227        leftMargin = 0;
228        isWrapping = true;
229        return ReplyBuilder.this;
230    }
231
232
233
234    /** Maximum width of wrapped text, in 'columns'.
235      *
236      *     @see #isWrapping
237      */
238    public static final int WRAPPED_WIDTH = 78; // RFC 2821 recommends "SHOULD be no more than 78 characters, excluding the CRLF"
239
240
241
242   // - A p p e n d a b l e --------------------------------------------------------------
243
244
245    /** Appends the specified character to the buffer.
246      */
247    public ReplyBuilder append( char c )
248    {
249        stringBuilder().append( c );
250        return ReplyBuilder.this;
251    }
252
253
254        /** Appends the specified character, plus a newline,
255          * to the buffer.
256          */
257        public ReplyBuilder appendln( char c )
258        {
259            append( c );
260            return appendln();
261        }
262
263
264
265    /** Appends the specified character sequence to the buffer.
266      */
267    public ReplyBuilder append( CharSequence csq )
268    {
269        stringBuilder().append( csq );
270        return ReplyBuilder.this;
271    }
272
273
274        /** Appends the specified character sequence, plus a newline,
275          * to the buffer.
276          */
277        public ReplyBuilder appendln( CharSequence csq )
278        {
279            append( csq );
280            return appendln();
281        }
282
283
284        /** Appends the specified character sequence, plus a double newline,
285          * to the buffer.
286          */
287        public ReplyBuilder appendlnn( CharSequence csq )
288        {
289            append( csq );
290            return appendlnn();
291        }
292
293
294
295    /** Appends a subsequence of the specified character sequence
296      * to the buffer.
297      */
298    public ReplyBuilder append( CharSequence csq, int start, int end )
299    {
300        stringBuilder().append( csq, start, end );
301        return ReplyBuilder.this;
302    }
303
304
305
306   // - O b j e c t ----------------------------------------------------------------------
307
308
309    /** Returns the reply as constructed in the buffer, to this point.
310      *
311      *     @see #chomplnn()
312      */
313    public @Override String toString()
314    {
315        sync();
316        return stringBuilder().toString();
317    }
318
319
320
321//// P r i v a t e ///////////////////////////////////////////////////////////////////////
322
323
324    private int leftMargin;
325
326
327
328    private void sync()
329    {
330        final StringBuilder sB = stringBuilder();
331
332      // Wrap. Before indenting, so lines beginning with whitespace can be left unwrapped.
333      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
334        if( isWrapping )
335        {
336            final int width = WRAPPED_WIDTH - leftMargin;
337
338            int cLineStart = 0; int cWrap = 0; boolean wasLastBlack = false; // initial values do not matter
339            boolean isExemptLine = true; // till first newline discovered
340            for( int c = syncIndex + 1; c < (sB.length()-1); ++c )
341            {
342                final char ch;
343                if( c < 0 ) ch = '\n'; // effectively
344                else ch = sB.charAt( c );
345
346                if( ch == '\n' )
347                {
348                    cLineStart = c + 1;
349                    cWrap = cLineStart;
350                    wasLastBlack = false;
351                    isExemptLine = cLineStart < sB.length()
352                      && Character.isWhitespace( sB.charAt( cLineStart )); // do not wrap lines that begin with whitespace
353                }
354                else
355                {
356                    if( isExemptLine ) continue;
357
358                    if( Character.isWhitespace( ch ))
359                    {
360                        if( wasLastBlack ) cWrap = c;
361                        wasLastBlack = false;
362                    }
363                    else wasLastBlack = true;
364
365                    if( cWrap == cLineStart || c - cLineStart < width ) continue;
366
367                    for( ;; )
368                    {
369                        sB.deleteCharAt( cWrap ); // eat whitespace at wrap point
370                        if( cWrap >= sB.length()
371                         || !Character.isWhitespace( sB.charAt( cWrap ))) break;
372                    }
373                    sB.insert( cWrap, '\n' );
374                    c = cWrap - 1; // hit the inserted newline on next pass
375                }
376            }
377        }
378
379      // Indent.
380      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
381        if( leftMargin > 0 )
382        {
383            boolean isBlackLine = false; // so far
384            for( int c = sB.length() - 2; c > syncIndex; --c )
385            {
386                final char ch;
387                if( c < 0 ) ch = '\n'; // effectively
388                else ch = sB.charAt( c );
389
390                if( ch == '\n' )
391                {
392                    if( isBlackLine )
393                    {
394                        final int cIndent = c + 1;
395                        for( int m = 0; m < leftMargin; ++m ) sB.insert( cIndent, ' ' );
396                    }
397                    isBlackLine = false; // so far
398                }
399                else if( !Character.isWhitespace( ch )) isBlackLine = true;
400            }
401        }
402
403      // - - -
404        syncIndex = sB.length() - 2;
405    }
406
407
408
409    /** Index of final character covered by last sync(); the penultimate
410      * character at sync time. It is not the ultimate character because
411      * that character (a newline usually) should not aquire
412      * the old indentation/wrapping state that preceded the sync;
413      * but rather the new state that will immediately follow it.
414      */
415    private int syncIndex = -2;
416
417
418
419}