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}