Source for java.util.logging.FileHandler

   1: /* FileHandler.java -- a class for publishing log messages to log files
   2:    Copyright (C) 2002, 2003, 2004, 2005  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package java.util.logging;
  40: 
  41: import gnu.java.lang.CPStringBuilder;
  42: 
  43: import java.io.File;
  44: import java.io.FileOutputStream;
  45: import java.io.FilterOutputStream;
  46: import java.io.IOException;
  47: import java.io.OutputStream;
  48: import java.util.LinkedList;
  49: import java.util.ListIterator;
  50: 
  51: /**
  52:  * A <code>FileHandler</code> publishes log records to a set of log
  53:  * files.  A maximum file size can be specified; as soon as a log file
  54:  * reaches the size limit, it is closed and the next file in the set
  55:  * is taken.
  56:  *
  57:  * <p><strong>Configuration:</strong> Values of the subsequent
  58:  * <code>LogManager</code> properties are taken into consideration
  59:  * when a <code>FileHandler</code> is initialized.  If a property is
  60:  * not defined, or if it has an invalid value, a default is taken
  61:  * without an exception being thrown.
  62:  *
  63:  * <ul>
  64:  *
  65:  * <li><code>java.util.FileHandler.level</code> - specifies
  66:  *     the initial severity level threshold. Default value:
  67:  *     <code>Level.ALL</code>.</li>
  68:  *
  69:  * <li><code>java.util.FileHandler.filter</code> - specifies
  70:  *     the name of a Filter class. Default value: No Filter.</li>
  71:  *
  72:  * <li><code>java.util.FileHandler.formatter</code> - specifies
  73:  *     the name of a Formatter class. Default value:
  74:  *     <code>java.util.logging.XMLFormatter</code>.</li>
  75:  *
  76:  * <li><code>java.util.FileHandler.encoding</code> - specifies
  77:  *     the name of the character encoding. Default value:
  78:  *     the default platform encoding.</li>
  79:  *
  80:  * <li><code>java.util.FileHandler.limit</code> - specifies the number
  81:  *     of bytes a log file is approximately allowed to reach before it
  82:  *     is closed and the handler switches to the next file in the
  83:  *     rotating set.  A value of zero means that files can grow
  84:  *     without limit.  Default value: 0 (unlimited growth).</li>
  85:  *
  86:  * <li><code>java.util.FileHandler.count</code> - specifies the number
  87:  *     of log files through which this handler cycles.  Default value:
  88:  *     1.</li>
  89:  *
  90:  * <li><code>java.util.FileHandler.pattern</code> - specifies a
  91:  *     pattern for the location and name of the produced log files.
  92:  *     See the section on <a href="#filePatterns">file name
  93:  *     patterns</a> for details.  Default value:
  94:  *     <code>"%h/java%u.log"</code>.</li>
  95:  *
  96:  * <li><code>java.util.FileHandler.append</code> - specifies
  97:  *     whether the handler will append log records to existing
  98:  *     files, or whether the handler will clear log files
  99:  *     upon switching to them. Default value: <code>false</code>,
 100:  *     indicating that files will be cleared.</li>
 101:  *
 102:  * </ul>
 103:  *
 104:  * <p><a name="filePatterns"><strong>File Name Patterns:</strong></a>
 105:  * The name and location and log files are specified with pattern
 106:  * strings. The handler will replace the following character sequences
 107:  * when opening log files:
 108:  *
 109:  * <p><ul>
 110:  * <li><code>/</code> - replaced by the platform-specific path name
 111:  *     separator.  This value is taken from the system property
 112:  *     <code>file.separator</code>.</li>
 113:  *
 114:  * <li><code>%t</code> - replaced by the platform-specific location of
 115:  *     the directory intended for temporary files.  This value is
 116:  *     taken from the system property <code>java.io.tmpdir</code>.</li>
 117:  *
 118:  * <li><code>%h</code> - replaced by the location of the home
 119:  *     directory of the current user.  This value is taken from the
 120:  *     system property <code>user.home</code>.</li>
 121:  *
 122:  * <li><code>%g</code> - replaced by a generation number for
 123:  *     distinguisthing the individual items in the rotating set
 124:  *     of log files.  The generation number cycles through the
 125:  *     sequence 0, 1, ..., <code>count</code> - 1.</li>
 126:  *
 127:  * <li><code>%u</code> - replaced by a unique number for
 128:  *     distinguisthing the output files of several concurrently
 129:  *     running processes.  The <code>FileHandler</code> starts
 130:  *     with 0 when it tries to open a log file.  If the file
 131:  *     cannot be opened because it is currently in use,
 132:  *     the unique number is incremented by one and opening
 133:  *     is tried again.  These steps are repeated until the
 134:  *     opening operation succeeds.
 135:  *
 136:  *     <p>FIXME: Is the following correct? Please review.  The unique
 137:  *     number is determined for each log file individually when it is
 138:  *     opened upon switching to the next file.  Therefore, it is not
 139:  *     correct to assume that all log files in a rotating set bear the
 140:  *     same unique number.
 141:  *
 142:  *     <p>FIXME: The Javadoc for the Sun reference implementation
 143:  *     says: "Note that the use of unique ids to avoid conflicts is
 144:  *     only guaranteed to work reliably when using a local disk file
 145:  *     system." Why? This needs to be mentioned as well, in case
 146:  *     the reviewers decide the statement is true.  Otherwise,
 147:  *     file a bug report with Sun.</li>
 148:  *
 149:  * <li><code>%%</code> - replaced by a single percent sign.</li>
 150:  * </ul>
 151:  *
 152:  * <p>If the pattern string does not contain <code>%g</code> and
 153:  * <code>count</code> is greater than one, the handler will append
 154:  * the string <code>.%g</code> to the specified pattern.
 155:  *
 156:  * <p>If the handler attempts to open a log file, this log file
 157:  * is being used at the time of the attempt, and the pattern string
 158:  * does not contain <code>%u</code>, the handler will append
 159:  * the string <code>.%u</code> to the specified pattern. This
 160:  * step is performed after any generation number has been
 161:  * appended.
 162:  *
 163:  * <p><em>Examples for the GNU platform:</em>
 164:  *
 165:  * <p><ul>
 166:  *
 167:  * <li><code>%h/java%u.log</code> will lead to a single log file
 168:  *     <code>/home/janet/java0.log</code>, assuming <code>count</code>
 169:  *     equals 1, the user's home directory is
 170:  *     <code>/home/janet</code>, and the attempt to open the file
 171:  *     succeeds.</li>
 172:  *
 173:  * <li><code>%h/java%u.log</code> will lead to three log files
 174:  *     <code>/home/janet/java0.log.0</code>,
 175:  *     <code>/home/janet/java0.log.1</code>, and
 176:  *     <code>/home/janet/java0.log.2</code>,
 177:  *     assuming <code>count</code> equals 3, the user's home
 178:  *     directory is <code>/home/janet</code>, and all attempts
 179:  *     to open files succeed.</li>
 180:  *
 181:  * <li><code>%h/java%u.log</code> will lead to three log files
 182:  *     <code>/home/janet/java0.log.0</code>,
 183:  *     <code>/home/janet/java1.log.1</code>, and
 184:  *     <code>/home/janet/java0.log.2</code>,
 185:  *     assuming <code>count</code> equals 3, the user's home
 186:  *     directory is <code>/home/janet</code>, and the attempt
 187:  *     to open <code>/home/janet/java0.log.1</code> fails.</li>
 188:  *
 189:  * </ul>
 190:  *
 191:  * @author Sascha Brawer (brawer@acm.org)
 192:  */
 193: public class FileHandler
 194:   extends StreamHandler
 195: {
 196:   /**
 197:    * A literal that prefixes all file-handler related properties in the
 198:    * logging.properties file.
 199:    */
 200:   private static final String PROPERTY_PREFIX = "java.util.logging.FileHandler";
 201:   /**
 202:    * The name of the property to set for specifying a file naming (incl. path)
 203:    * pattern to use with rotating log files.
 204:    */
 205:   private static final String PATTERN_KEY = PROPERTY_PREFIX + ".pattern";
 206:   /**
 207:    * The default pattern to use when the <code>PATTERN_KEY</code> property was
 208:    * not specified in the logging.properties file.
 209:    */
 210:   private static final String DEFAULT_PATTERN = "%h/java%u.log";
 211:   /**
 212:    * The name of the property to set for specifying an approximate maximum
 213:    * amount, in bytes, to write to any one log output file. A value of zero
 214:    * (which is the default) implies a no limit.
 215:    */
 216:   private static final String LIMIT_KEY = PROPERTY_PREFIX + ".limit";
 217:   private static final int DEFAULT_LIMIT = 0;
 218:   /**
 219:    * The name of the property to set for specifying how many output files to
 220:    * cycle through. The default value is 1.
 221:    */
 222:   private static final String COUNT_KEY = PROPERTY_PREFIX + ".count";
 223:   private static final int DEFAULT_COUNT = 1;
 224:   /**
 225:    * The name of the property to set for specifying whether this handler should
 226:    * append, or not, its output to existing files. The default value is
 227:    * <code>false</code> meaning NOT to append.
 228:    */
 229:   private static final String APPEND_KEY = PROPERTY_PREFIX + ".append";
 230:   private static final boolean DEFAULT_APPEND = false;
 231: 
 232:   /**
 233:    * The number of bytes a log file is approximately allowed to reach
 234:    * before it is closed and the handler switches to the next file in
 235:    * the rotating set.  A value of zero means that files can grow
 236:    * without limit.
 237:    */
 238:   private final int limit;
 239: 
 240: 
 241:  /**
 242:   * The number of log files through which this handler cycles.
 243:   */
 244:   private final int count;
 245: 
 246: 
 247:   /**
 248:    * The pattern for the location and name of the produced log files.
 249:    * See the section on <a href="#filePatterns">file name patterns</a>
 250:    * for details.
 251:    */
 252:   private final String pattern;
 253: 
 254: 
 255:   /**
 256:    * Indicates whether the handler will append log records to existing
 257:    * files (<code>true</code>), or whether the handler will clear log files
 258:    * upon switching to them (<code>false</code>).
 259:    */
 260:   private final boolean append;
 261: 
 262: 
 263:   /**
 264:    * The number of bytes that have currently been written to the stream.
 265:    * Package private for use in inner classes.
 266:    */
 267:   long written;
 268: 
 269: 
 270:   /**
 271:    * A linked list of files we are, or have written to. The entries
 272:    * are file path strings, kept in the order
 273:    */
 274:   private LinkedList logFiles;
 275: 
 276: 
 277:   /**
 278:    * Constructs a <code>FileHandler</code>, taking all property values
 279:    * from the current {@link LogManager LogManager} configuration.
 280:    *
 281:    * @throws java.io.IOException FIXME: The Sun Javadoc says: "if
 282:    *         there are IO problems opening the files."  This conflicts
 283:    *         with the general principle that configuration errors do
 284:    *         not prohibit construction. Needs review.
 285:    *
 286:    * @throws SecurityException if a security manager exists and
 287:    *         the caller is not granted the permission to control
 288:    *         the logging infrastructure.
 289:    */
 290:   public FileHandler()
 291:     throws IOException, SecurityException
 292:   {
 293:     this(LogManager.getLogManager().getProperty(PATTERN_KEY),
 294:          LogManager.getIntProperty(LIMIT_KEY, DEFAULT_LIMIT),
 295:          LogManager.getIntProperty(COUNT_KEY, DEFAULT_COUNT),
 296:          LogManager.getBooleanProperty(APPEND_KEY, DEFAULT_APPEND));
 297:   }
 298: 
 299: 
 300:   /* FIXME: Javadoc missing. */
 301:   public FileHandler(String pattern)
 302:     throws IOException, SecurityException
 303:   {
 304:     this(pattern, DEFAULT_LIMIT, DEFAULT_COUNT, DEFAULT_APPEND);
 305:   }
 306: 
 307: 
 308:   /* FIXME: Javadoc missing. */
 309:   public FileHandler(String pattern, boolean append)
 310:     throws IOException, SecurityException
 311:   {
 312:     this(pattern, DEFAULT_LIMIT, DEFAULT_COUNT, append);
 313:   }
 314: 
 315: 
 316:   /* FIXME: Javadoc missing. */
 317:   public FileHandler(String pattern, int limit, int count)
 318:     throws IOException, SecurityException
 319:   {
 320:     this(pattern, limit, count,
 321:          LogManager.getBooleanProperty(APPEND_KEY, DEFAULT_APPEND));
 322:   }
 323: 
 324: 
 325:   /**
 326:    * Constructs a <code>FileHandler</code> given the pattern for the
 327:    * location and name of the produced log files, the size limit, the
 328:    * number of log files thorough which the handler will rotate, and
 329:    * the <code>append</code> property.  All other property values are
 330:    * taken from the current {@link LogManager LogManager}
 331:    * configuration.
 332:    *
 333:    * @param pattern The pattern for the location and name of the
 334:    *        produced log files.  See the section on <a
 335:    *        href="#filePatterns">file name patterns</a> for details.
 336:    *        If <code>pattern</code> is <code>null</code>, the value is
 337:    *        taken from the {@link LogManager LogManager} configuration
 338:    *        property
 339:    *        <code>java.util.logging.FileHandler.pattern</code>.
 340:    *        However, this is a pecularity of the GNU implementation,
 341:    *        and Sun's API specification does not mention what behavior
 342:    *        is to be expected for <code>null</code>. Therefore,
 343:    *        applications should not rely on this feature.
 344:    *
 345:    * @param limit specifies the number of bytes a log file is
 346:    *        approximately allowed to reach before it is closed and the
 347:    *        handler switches to the next file in the rotating set.  A
 348:    *        value of zero means that files can grow without limit.
 349:    *
 350:    * @param count specifies the number of log files through which this
 351:    *        handler cycles.
 352:    *
 353:    * @param append specifies whether the handler will append log
 354:    *        records to existing files (<code>true</code>), or whether the
 355:    *        handler will clear log files upon switching to them
 356:    *        (<code>false</code>).
 357:    *
 358:    * @throws java.io.IOException FIXME: The Sun Javadoc says: "if
 359:    *         there are IO problems opening the files."  This conflicts
 360:    *         with the general principle that configuration errors do
 361:    *         not prohibit construction. Needs review.
 362:    *
 363:    * @throws SecurityException if a security manager exists and
 364:    *         the caller is not granted the permission to control
 365:    *         the logging infrastructure.
 366:    *         <p>FIXME: This seems in contrast to all other handler
 367:    *         constructors -- verify this by running tests against
 368:    *         the Sun reference implementation.
 369:    */
 370:   public FileHandler(String pattern,
 371:                      int limit,
 372:                      int count,
 373:                      boolean append)
 374:     throws IOException, SecurityException
 375:   {
 376:     super(/* output stream, created below */ null,
 377:           PROPERTY_PREFIX,
 378:           /* default level */ Level.ALL,
 379:           /* formatter */ null,
 380:           /* default formatter */ XMLFormatter.class);
 381: 
 382:     if ((limit <0) || (count < 1))
 383:       throw new IllegalArgumentException();
 384: 
 385:     this.pattern = pattern != null ? pattern : DEFAULT_PATTERN;
 386:     this.limit = limit;
 387:     this.count = count;
 388:     this.append = append;
 389:     this.written = 0;
 390:     this.logFiles = new LinkedList ();
 391: 
 392:     setOutputStream (createFileStream (this.pattern, limit, count, append,
 393:                                        /* generation */ 0));
 394:   }
 395: 
 396: 
 397:   /* FIXME: Javadoc missing. */
 398:   private OutputStream createFileStream(String pattern,
 399:                                         int limit,
 400:                                         int count,
 401:                                         boolean append,
 402:                                         int generation)
 403:   {
 404:     String  path;
 405:     int     unique = 0;
 406: 
 407:     /* Throws a SecurityException if the caller does not have
 408:      * LoggingPermission("control").
 409:      */
 410:     LogManager.getLogManager().checkAccess();
 411: 
 412:     /* Default value from the java.util.logging.FileHandler.pattern
 413:      * LogManager configuration property.
 414:      */
 415:     if (pattern == null)
 416:       pattern = LogManager.getLogManager().getProperty(PATTERN_KEY);
 417:     if (pattern == null)
 418:       pattern = DEFAULT_PATTERN;
 419: 
 420:     if (count > 1 && !has (pattern, 'g'))
 421:       pattern = pattern + ".%g";
 422: 
 423:     do
 424:     {
 425:       path = replaceFileNameEscapes(pattern, generation, unique, count);
 426: 
 427:       try
 428:       {
 429:         File file = new File(path);
 430:         if (!file.exists () || append)
 431:           {
 432:             FileOutputStream fout = new FileOutputStream (file, append);
 433:             // FIXME we need file locks for this to work properly, but they
 434:             // are not implemented yet in Classpath! Madness!
 435: //             FileChannel channel = fout.getChannel ();
 436: //             FileLock lock = channel.tryLock ();
 437: //             if (lock != null) // We've locked the file.
 438: //               {
 439:                 if (logFiles.isEmpty ())
 440:                   logFiles.addFirst (path);
 441:                 return new ostr (fout);
 442: //               }
 443:           }
 444:       }
 445:       catch (Exception ex)
 446:       {
 447:         reportError (null, ex, ErrorManager.OPEN_FAILURE);
 448:       }
 449: 
 450:       unique = unique + 1;
 451:       if (!has (pattern, 'u'))
 452:         pattern = pattern + ".%u";
 453:     }
 454:     while (true);
 455:   }
 456: 
 457: 
 458:   /**
 459:    * Replaces the substrings <code>"/"</code> by the value of the
 460:    * system property <code>"file.separator"</code>, <code>"%t"</code>
 461:    * by the value of the system property
 462:    * <code>"java.io.tmpdir"</code>, <code>"%h"</code> by the value of
 463:    * the system property <code>"user.home"</code>, <code>"%g"</code>
 464:    * by the value of <code>generation</code>, <code>"%u"</code> by the
 465:    * value of <code>uniqueNumber</code>, and <code>"%%"</code> by a
 466:    * single percent character.  If <code>pattern</code> does
 467:    * <em>not</em> contain the sequence <code>"%g"</code>,
 468:    * the value of <code>generation</code> will be appended to
 469:    * the result.
 470:    *
 471:    * @throws NullPointerException if one of the system properties
 472:    *         <code>"file.separator"</code>,
 473:    *         <code>"java.io.tmpdir"</code>, or
 474:    *         <code>"user.home"</code> has no value and the
 475:    *         corresponding escape sequence appears in
 476:    *         <code>pattern</code>.
 477:    */
 478:   private static String replaceFileNameEscapes(String pattern,
 479:                                                int generation,
 480:                                                int uniqueNumber,
 481:                                                int count)
 482:   {
 483:     CPStringBuilder buf = new CPStringBuilder(pattern);
 484:     String       replaceWith;
 485:     boolean      foundGeneration = false;
 486: 
 487:     int pos = 0;
 488:     do
 489:     {
 490:       // Uncomment the next line for finding bugs.
 491:       // System.out.println(buf.substring(0,pos) + '|' + buf.substring(pos));
 492: 
 493:       if (buf.charAt(pos) == '/')
 494:       {
 495:         /* The same value is also provided by java.io.File.separator. */
 496:         replaceWith = System.getProperty("file.separator");
 497:         buf.replace(pos, pos + 1, replaceWith);
 498:         pos = pos + replaceWith.length() - 1;
 499:         continue;
 500:       }
 501: 
 502:       if (buf.charAt(pos) == '%')
 503:       {
 504:         switch (buf.charAt(pos + 1))
 505:         {
 506:         case 't':
 507:           replaceWith = System.getProperty("java.io.tmpdir");
 508:           break;
 509: 
 510:         case 'h':
 511:           replaceWith = System.getProperty("user.home");
 512:           break;
 513: 
 514:         case 'g':
 515:           replaceWith = Integer.toString(generation);
 516:           foundGeneration = true;
 517:           break;
 518: 
 519:         case 'u':
 520:           replaceWith = Integer.toString(uniqueNumber);
 521:           break;
 522: 
 523:         case '%':
 524:           replaceWith = "%";
 525:           break;
 526: 
 527:         default:
 528:           replaceWith = "??";
 529:           break; // FIXME: Throw exception?
 530:         }
 531: 
 532:         buf.replace(pos, pos + 2, replaceWith);
 533:         pos = pos + replaceWith.length() - 1;
 534:         continue;
 535:       }
 536:     }
 537:     while (++pos < buf.length() - 1);
 538: 
 539:     if (!foundGeneration && (count > 1))
 540:     {
 541:       buf.append('.');
 542:       buf.append(generation);
 543:     }
 544: 
 545:     return buf.toString();
 546:   }
 547: 
 548: 
 549:   /* FIXME: Javadoc missing. */
 550:   public void publish(LogRecord record)
 551:   {
 552:     if (limit > 0 && written >= limit)
 553:       rotate ();
 554:     super.publish(record);
 555:     flush ();
 556:   }
 557: 
 558:   /**
 559:    * Rotates the current log files, possibly removing one if we
 560:    * exceed the file count.
 561:    */
 562:   private synchronized void rotate ()
 563:   {
 564:     if (logFiles.size () > 0)
 565:       {
 566:         File f1 = null;
 567:         ListIterator lit = null;
 568: 
 569:         // If we reach the file count, ditch the oldest file.
 570:         if (logFiles.size () == count)
 571:           {
 572:             f1 = new File ((String) logFiles.getLast ());
 573:             f1.delete ();
 574:             lit = logFiles.listIterator (logFiles.size () - 1);
 575:           }
 576:         // Otherwise, move the oldest to a new location.
 577:         else
 578:           {
 579:             String path = replaceFileNameEscapes (pattern, logFiles.size (),
 580:                                                   /* unique */ 0, count);
 581:             f1 = new File (path);
 582:             logFiles.addLast (path);
 583:             lit = logFiles.listIterator (logFiles.size () - 1);
 584:           }
 585: 
 586:         // Now rotate the files.
 587:         while (lit.hasPrevious ())
 588:           {
 589:             String s = (String) lit.previous ();
 590:             File f2 = new File (s);
 591:             f2.renameTo (f1);
 592:             f1 = f2;
 593:           }
 594:       }
 595: 
 596:     setOutputStream (createFileStream (pattern, limit, count, append,
 597:                                        /* generation */ 0));
 598: 
 599:     // Reset written count.
 600:     written = 0;
 601:   }
 602: 
 603:   /**
 604:    * Tell if <code>pattern</code> contains the pattern sequence
 605:    * with character <code>escape</code>. That is, if <code>escape</code>
 606:    * is 'g', this method returns true if the given pattern contains
 607:    * "%g", and not just the substring "%g" (for example, in the case of
 608:    * "%%g").
 609:    *
 610:    * @param pattern The pattern to test.
 611:    * @param escape The escape character to search for.
 612:    * @return True iff the pattern contains the escape sequence with the
 613:    *  given character.
 614:    */
 615:   private static boolean has (final String pattern, final char escape)
 616:   {
 617:     final int len = pattern.length ();
 618:     boolean sawPercent = false;
 619:     for (int i = 0; i < len; i++)
 620:       {
 621:         char c = pattern.charAt (i);
 622:         if (sawPercent)
 623:           {
 624:             if (c == escape)
 625:               return true;
 626:             if (c == '%') // Double percent
 627:               {
 628:                 sawPercent = false;
 629:                 continue;
 630:               }
 631:           }
 632:         sawPercent = (c == '%');
 633:       }
 634:     return false;
 635:   }
 636: 
 637:   /**
 638:    * An output stream that tracks the number of bytes written to it.
 639:    */
 640:   private final class ostr extends FilterOutputStream
 641:   {
 642:     private ostr (OutputStream out)
 643:     {
 644:       super (out);
 645:     }
 646: 
 647:     public void write (final int b) throws IOException
 648:     {
 649:       out.write (b);
 650:       FileHandler.this.written++; // FIXME: synchronize?
 651:     }
 652: 
 653:     public void write (final byte[] b) throws IOException
 654:     {
 655:       write (b, 0, b.length);
 656:     }
 657: 
 658:     public void write (final byte[] b, final int offset, final int length)
 659:       throws IOException
 660:     {
 661:       out.write (b, offset, length);
 662:       FileHandler.this.written += length; // FIXME: synchronize?
 663:     }
 664:   }
 665: }