package 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. import java.util.*; import votorola.g.locale.*; import votorola.g.lang.*; /** A composite resource bundle, string formatter and character buffer. It builds a * plain-text, line-wrapped, message. */ public final @ThreadRestricted class ReplyBuilder extends BundleFormatter implements Appendable { /** Constructs a ReplyBuilder. * * @param bundle the resource bundle to use, per {@linkplain #bundle() bundle}() */ public ReplyBuilder( ResourceBundle bundle ) { super( bundle ); } /** Constructs a command/response (CR) ReplyBuilder, for building a reply * to a command. It uses bundle base name 'votorola.a.locale.CR'. * * @param l locale, per {@linkplain #locale() locale}() * * @see * locale/CR.properties */ public ReplyBuilder( Locale l ) { super( ResourceBundle.getBundle( "votorola.a.locale.CR", l )); } // ------------------------------------------------------------------------------------ /** Appends the string representation of the specified integer to the buffer. */ public ReplyBuilder append( int i ) { stringBuilder().append( i ); return ReplyBuilder.this; } /** Appends the string representation of the specified long integer to the buffer. */ public ReplyBuilder append( long l ) { stringBuilder().append( l ); return ReplyBuilder.this; } /** Appends a newline. */ public ReplyBuilder appendln() { stringBuilder().append( '\n' ); return ReplyBuilder.this; } /** Appends a double newline. */ public ReplyBuilder appendlnn() { stringBuilder().append( '\n' ).append( '\n' ); return ReplyBuilder.this; } /** Appends n copies of character c to the buffer. */ public ReplyBuilder appendRepeat( final char c, final int n ) { final StringBuilder sB = stringBuilder(); for( int i = 0; i < n; ++i ) sB.append( c ); return ReplyBuilder.this; } /** Appends n copies of character c, plus a newline, to the buffer. */ public ReplyBuilder appendRepeatln( final char c, final int n ) { appendRepeat( c, n ); return appendln(); } /** Appends n copies of character c, plus a double newline, to the buffer. */ public ReplyBuilder appendRepeatlnn( final char c, final int n ) { appendRepeat( c, n ); return appendlnn(); } /** Chops any trailing double newline down to a single newline. * Useful prior to calling {@linkplain #toString toString}(). */ public ReplyBuilder chomplnn() { final StringBuilder sB = stringBuilder(); int cLast = sB.length() - 1; if( cLast > 0 && sB.charAt(cLast) == '\n' && sB.charAt(cLast-1) == '\n' ) { sB.deleteCharAt( cLast ); } return ReplyBuilder.this; } /** Decreases the left wrap-margin, undoing a previous indent. * * @see #indent(int) */ public ReplyBuilder exdent( int amount ) { sync(); leftMargin -= amount; return ReplyBuilder.this; } /** Appends some spaces, and increases the left wrap-margin accordingly. * * @see #exdent(int) */ public ReplyBuilder indent( int amount ) { sync(); leftMargin += amount; return ReplyBuilder.this; } /** Returns true if wrapping is enabled; false otherwise. When enabled, lines longer * than {@linkplain #WRAPPED_WIDTH WRAPPED_WIDTH} will automatically wrap back to the * left margin. By default, it is enabled. * * @see #setWrapping */ public final boolean isWrapping() { return isWrapping; }; private boolean isWrapping = true; /** Sets whether wrapping is enabled. * * @see #isWrapping */ public final ReplyBuilder setWrapping( boolean toWrap ) { sync(); isWrapping = toWrap; return ReplyBuilder.this; } public @Override ReplyBuilder lappend( final String key, final Object... args ) { return (ReplyBuilder)super.lappend( key, args ); } /** Appends a localized string, plus a newline, to the buffer. * * @param key {@linkplain #bundle() bundle} key of the string * @param args arguments for insertion in the string, * per {@linkplain Formatter#format(String,Object[]) format}(String,Object[]) */ public ReplyBuilder lappendln( final String key, final Object... args ) { lappend( key, args ); return appendln(); } /** Appends a localized string, plus a double newline, to the buffer. * * @param key {@linkplain #bundle() bundle} key of the string * @param args arguments for insertion in the string, * per {@linkplain Formatter#format(String,Object[]) format}(String,Object[]) */ public ReplyBuilder lappendlnn( final String key, final Object... args ) { lappend( key, args ); return appendlnn(); } /** Returns the current length (character count) of the buffer. */ public int length() { sync(); return stringBuilder().length(); } /** Set indentation and wrapping to defaults. * * @see #indent(int) * @see #isWrapping */ public final ReplyBuilder resetFormattingToDefaults() // a formatting stack (push/pop) would be cleaner, so nested subroutines could avoid clobbering caller's settings { sync(); leftMargin = 0; isWrapping = true; return ReplyBuilder.this; } /** Maximum width of wrapped text, in 'columns'. * * @see #isWrapping */ public static final int WRAPPED_WIDTH = 78; // RFC 2821 recommends "SHOULD be no more than 78 characters, excluding the CRLF" // - A p p e n d a b l e -------------------------------------------------------------- /** Appends the specified character to the buffer. */ public ReplyBuilder append( char c ) { stringBuilder().append( c ); return ReplyBuilder.this; } /** Appends the specified character, plus a newline, * to the buffer. */ public ReplyBuilder appendln( char c ) { append( c ); return appendln(); } /** Appends the specified character sequence to the buffer. */ public ReplyBuilder append( CharSequence csq ) { stringBuilder().append( csq ); return ReplyBuilder.this; } /** Appends the specified character sequence, plus a newline, * to the buffer. */ public ReplyBuilder appendln( CharSequence csq ) { append( csq ); return appendln(); } /** Appends the specified character sequence, plus a double newline, * to the buffer. */ public ReplyBuilder appendlnn( CharSequence csq ) { append( csq ); return appendlnn(); } /** Appends a subsequence of the specified character sequence * to the buffer. */ public ReplyBuilder append( CharSequence csq, int start, int end ) { stringBuilder().append( csq, start, end ); return ReplyBuilder.this; } // - O b j e c t ---------------------------------------------------------------------- /** Returns the reply as constructed in the buffer, to this point. * * @see #chomplnn() */ public @Override String toString() { sync(); return stringBuilder().toString(); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private int leftMargin; private void sync() { final StringBuilder sB = stringBuilder(); // Wrap. Before indenting, so lines beginning with whitespace can be left unwrapped. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( isWrapping ) { final int width = WRAPPED_WIDTH - leftMargin; int cLineStart = 0; int cWrap = 0; boolean wasLastBlack = false; // initial values do not matter boolean isExemptLine = true; // till first newline discovered for( int c = syncIndex + 1; c < (sB.length()-1); ++c ) { final char ch; if( c < 0 ) ch = '\n'; // effectively else ch = sB.charAt( c ); if( ch == '\n' ) { cLineStart = c + 1; cWrap = cLineStart; wasLastBlack = false; isExemptLine = cLineStart < sB.length() && Character.isWhitespace( sB.charAt( cLineStart )); // do not wrap lines that begin with whitespace } else { if( isExemptLine ) continue; if( Character.isWhitespace( ch )) { if( wasLastBlack ) cWrap = c; wasLastBlack = false; } else wasLastBlack = true; if( cWrap == cLineStart || c - cLineStart < width ) continue; for( ;; ) { sB.deleteCharAt( cWrap ); // eat whitespace at wrap point if( cWrap >= sB.length() || !Character.isWhitespace( sB.charAt( cWrap ))) break; } sB.insert( cWrap, '\n' ); c = cWrap - 1; // hit the inserted newline on next pass } } } // Indent. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( leftMargin > 0 ) { boolean isBlackLine = false; // so far for( int c = sB.length() - 2; c > syncIndex; --c ) { final char ch; if( c < 0 ) ch = '\n'; // effectively else ch = sB.charAt( c ); if( ch == '\n' ) { if( isBlackLine ) { final int cIndent = c + 1; for( int m = 0; m < leftMargin; ++m ) sB.insert( cIndent, ' ' ); } isBlackLine = false; // so far } else if( !Character.isWhitespace( ch )) isBlackLine = true; } } // - - - syncIndex = sB.length() - 2; } /** Index of final character covered by last sync(); the penultimate * character at sync time. It is not the ultimate character because * that character (a newline usually) should not aquire * the old indentation/wrapping state that preceded the sync; * but rather the new state that will immediately follow it. */ private int syncIndex = -2; }