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