1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.util.logging; 19 20import java.io.BufferedOutputStream; 21import java.io.File; 22import java.io.FileNotFoundException; 23import java.io.FileOutputStream; 24import java.io.IOException; 25import java.io.OutputStream; 26import java.nio.channels.FileChannel; 27import java.nio.channels.FileLock; 28import java.util.Hashtable; 29import libcore.io.IoUtils; 30 31/** 32 * A {@code FileHandler} writes logging records into a specified file or a 33 * rotating set of files. 34 * <p> 35 * When a set of files is used and a given amount of data has been written to 36 * one file, then this file is closed and another file is opened. The name of 37 * these files are generated by given name pattern, see below for details. 38 * When the files have all been filled the Handler returns to the first and goes 39 * through the set again. 40 * <p> 41 * By default, the I/O buffering mechanism is enabled, but when each log record 42 * is complete, it is flushed out. 43 * <p> 44 * {@code XMLFormatter} is the default formatter for {@code FileHandler}. 45 * <p> 46 * {@code FileHandler} reads the following {@code LogManager} properties for 47 * initialization; if a property is not defined or has an invalid value, a 48 * default value is used. 49 * <ul> 50 * <li>java.util.logging.FileHandler.append specifies whether this 51 * {@code FileHandler} should append onto existing files, defaults to 52 * {@code false}.</li> 53 * <li>java.util.logging.FileHandler.count specifies how many output files to 54 * rotate, defaults to 1.</li> 55 * <li>java.util.logging.FileHandler.filter specifies the {@code Filter} class 56 * name, defaults to no {@code Filter}.</li> 57 * <li>java.util.logging.FileHandler.formatter specifies the {@code Formatter} 58 * class, defaults to {@code java.util.logging.XMLFormatter}.</li> 59 * <li>java.util.logging.FileHandler.encoding specifies the character set 60 * encoding name, defaults to the default platform encoding.</li> 61 * <li>java.util.logging.FileHandler.level specifies the level for this 62 * {@code Handler}, defaults to {@code Level.ALL}.</li> 63 * <li>java.util.logging.FileHandler.limit specifies the maximum number of 64 * bytes to write to any one file, defaults to zero, which means no limit.</li> 65 * <li>java.util.logging.FileHandler.pattern specifies name pattern for the 66 * output files. See below for details. Defaults to "%h/java%u.log".</li> 67 * </ul> 68 * <p> 69 * Name pattern is a string that may include some special substrings, which will 70 * be replaced to generate output files: 71 * <ul> 72 * <li>"/" represents the local pathname separator</li> 73 * <li>"%g" represents the generation number to distinguish rotated logs</li> 74 * <li>"%h" represents the home directory of the current user, which is 75 * specified by "user.home" system property</li> 76 * <li>"%t" represents the system's temporary directory</li> 77 * <li>"%u" represents a unique number to resolve conflicts</li> 78 * <li>"%%" represents the percent sign character '%'</li> 79 * </ul> 80 * <p> 81 * Normally, the generation numbers are not larger than the given file count and 82 * follow the sequence 0, 1, 2.... If the file count is larger than one, but the 83 * generation field("%g") has not been specified in the pattern, then the 84 * generation number after a dot will be added to the end of the file name. 85 * <p> 86 * The "%u" unique field is used to avoid conflicts and is set to 0 at first. If 87 * one {@code FileHandler} tries to open the filename which is currently in use 88 * by another process, it will repeatedly increment the unique number field and 89 * try again. If the "%u" component has not been included in the file name 90 * pattern and some contention on a file does occur, then a unique numerical 91 * value will be added to the end of the filename in question immediately to the 92 * right of a dot. The generation of unique IDs for avoiding conflicts is only 93 * guaranteed to work reliably when using a local disk file system. 94 */ 95public class FileHandler extends StreamHandler { 96 97 private static final String LCK_EXT = ".lck"; 98 99 private static final int DEFAULT_COUNT = 1; 100 101 private static final int DEFAULT_LIMIT = 0; 102 103 private static final boolean DEFAULT_APPEND = false; 104 105 private static final String DEFAULT_PATTERN = "%h/java%u.log"; 106 107 // maintain all file locks hold by this process 108 private static final Hashtable<String, FileLock> allLocks = new Hashtable<String, FileLock>(); 109 110 // the count of files which the output cycle through 111 private int count; 112 113 // the size limitation in byte of log file 114 private int limit; 115 116 // whether the FileHandler should open a existing file for output in append 117 // mode 118 private boolean append; 119 120 // the pattern for output file name 121 private String pattern; 122 123 // maintain a LogManager instance for convenience 124 private LogManager manager; 125 126 // output stream, which can measure the output file length 127 private MeasureOutputStream output; 128 129 // used output file 130 private File[] files; 131 132 // output file lock 133 FileLock lock = null; 134 135 // current output file name 136 String fileName = null; 137 138 // current unique ID 139 int uniqueID = -1; 140 141 /** 142 * Construct a {@code FileHandler} using {@code LogManager} properties or 143 * their default value. 144 * 145 * @throws IOException 146 * if any I/O error occurs. 147 */ 148 public FileHandler() throws IOException { 149 init(null, null, null, null); 150 } 151 152 // init properties 153 private void init(String p, Boolean a, Integer l, Integer c) 154 throws IOException { 155 // check access 156 manager = LogManager.getLogManager(); 157 manager.checkAccess(); 158 initProperties(p, a, l, c); 159 initOutputFiles(); 160 } 161 162 private void initOutputFiles() throws FileNotFoundException, IOException { 163 while (true) { 164 // try to find a unique file which is not locked by other process 165 uniqueID++; 166 // FIXME: improve performance here 167 for (int generation = 0; generation < count; generation++) { 168 // cache all file names for rotation use 169 files[generation] = new File(parseFileName(generation)); 170 } 171 fileName = files[0].getAbsolutePath(); 172 synchronized (allLocks) { 173 /* 174 * if current process has held lock for this fileName continue 175 * to find next file 176 */ 177 if (allLocks.get(fileName) != null) { 178 continue; 179 } 180 if (files[0].exists() 181 && (!append || files[0].length() >= limit)) { 182 for (int i = count - 1; i > 0; i--) { 183 if (files[i].exists()) { 184 files[i].delete(); 185 } 186 files[i - 1].renameTo(files[i]); 187 } 188 } 189 FileOutputStream fileStream = new FileOutputStream(fileName 190 + LCK_EXT); 191 FileChannel channel = fileStream.getChannel(); 192 /* 193 * if lock is unsupported and IOException thrown, just let the 194 * IOException throws out and exit otherwise it will go into an 195 * undead cycle 196 */ 197 lock = channel.tryLock(); 198 if (lock == null) { 199 IoUtils.closeQuietly(fileStream); 200 continue; 201 } 202 allLocks.put(fileName, lock); 203 break; 204 } 205 } 206 output = new MeasureOutputStream(new BufferedOutputStream( 207 new FileOutputStream(fileName, append)), files[0].length()); 208 setOutputStream(output); 209 } 210 211 private void initProperties(String p, Boolean a, Integer l, Integer c) { 212 super.initProperties("ALL", null, "java.util.logging.XMLFormatter", 213 null); 214 String className = this.getClass().getName(); 215 pattern = (p == null) ? getStringProperty(className + ".pattern", 216 DEFAULT_PATTERN) : p; 217 if (pattern == null) { 218 throw new NullPointerException("pattern == null"); 219 } else if (pattern.isEmpty()) { 220 throw new NullPointerException("pattern.isEmpty()"); 221 } 222 append = (a == null) ? getBooleanProperty(className + ".append", 223 DEFAULT_APPEND) : a.booleanValue(); 224 count = (c == null) ? getIntProperty(className + ".count", 225 DEFAULT_COUNT) : c.intValue(); 226 limit = (l == null) ? getIntProperty(className + ".limit", 227 DEFAULT_LIMIT) : l.intValue(); 228 count = count < 1 ? DEFAULT_COUNT : count; 229 limit = limit < 0 ? DEFAULT_LIMIT : limit; 230 files = new File[count]; 231 } 232 233 void findNextGeneration() { 234 super.close(); 235 for (int i = count - 1; i > 0; i--) { 236 if (files[i].exists()) { 237 files[i].delete(); 238 } 239 files[i - 1].renameTo(files[i]); 240 } 241 try { 242 output = new MeasureOutputStream(new BufferedOutputStream( 243 new FileOutputStream(files[0]))); 244 } catch (FileNotFoundException e1) { 245 this.getErrorManager().error("Error opening log file", e1, ErrorManager.OPEN_FAILURE); 246 } 247 setOutputStream(output); 248 } 249 250 /** 251 * Transform the pattern to the valid file name, replacing any patterns, and 252 * applying generation and uniqueID if present. 253 * 254 * @param gen 255 * generation of this file 256 * @return transformed filename ready for use. 257 */ 258 private String parseFileName(int gen) { 259 int cur = 0; 260 int next = 0; 261 boolean hasUniqueID = false; 262 boolean hasGeneration = false; 263 264 // TODO privilege code? 265 266 String tempPath = System.getProperty("java.io.tmpdir"); 267 boolean tempPathHasSepEnd = (tempPath == null ? false : tempPath 268 .endsWith(File.separator)); 269 270 String homePath = System.getProperty("user.home"); 271 boolean homePathHasSepEnd = (homePath == null ? false : homePath 272 .endsWith(File.separator)); 273 274 StringBuilder sb = new StringBuilder(); 275 pattern = pattern.replace('/', File.separatorChar); 276 277 char[] value = pattern.toCharArray(); 278 while ((next = pattern.indexOf('%', cur)) >= 0) { 279 if (++next < pattern.length()) { 280 switch (value[next]) { 281 case 'g': 282 sb.append(value, cur, next - cur - 1).append(gen); 283 hasGeneration = true; 284 break; 285 case 'u': 286 sb.append(value, cur, next - cur - 1).append(uniqueID); 287 hasUniqueID = true; 288 break; 289 case 't': 290 /* 291 * we should probably try to do something cute here like 292 * lookahead for adjacent '/' 293 */ 294 sb.append(value, cur, next - cur - 1).append(tempPath); 295 if (!tempPathHasSepEnd) { 296 sb.append(File.separator); 297 } 298 break; 299 case 'h': 300 sb.append(value, cur, next - cur - 1).append(homePath); 301 if (!homePathHasSepEnd) { 302 sb.append(File.separator); 303 } 304 break; 305 case '%': 306 sb.append(value, cur, next - cur - 1).append('%'); 307 break; 308 default: 309 sb.append(value, cur, next - cur); 310 } 311 cur = ++next; 312 } else { 313 // fail silently 314 } 315 } 316 317 sb.append(value, cur, value.length - cur); 318 319 if (!hasGeneration && count > 1) { 320 sb.append(".").append(gen); 321 } 322 323 if (!hasUniqueID && uniqueID > 0) { 324 sb.append(".").append(uniqueID); 325 } 326 327 return sb.toString(); 328 } 329 330 // get boolean LogManager property, if invalid value got, using default 331 // value 332 private boolean getBooleanProperty(String key, boolean defaultValue) { 333 String property = manager.getProperty(key); 334 if (property == null) { 335 return defaultValue; 336 } 337 boolean result = defaultValue; 338 if ("true".equalsIgnoreCase(property)) { 339 result = true; 340 } else if ("false".equalsIgnoreCase(property)) { 341 result = false; 342 } 343 return result; 344 } 345 346 // get String LogManager property, if invalid value got, using default value 347 private String getStringProperty(String key, String defaultValue) { 348 String property = manager.getProperty(key); 349 return property == null ? defaultValue : property; 350 } 351 352 // get int LogManager property, if invalid value got, using default value 353 private int getIntProperty(String key, int defaultValue) { 354 String property = manager.getProperty(key); 355 int result = defaultValue; 356 if (property != null) { 357 try { 358 result = Integer.parseInt(property); 359 } catch (Exception e) { 360 // ignore 361 } 362 } 363 return result; 364 } 365 366 /** 367 * Constructs a new {@code FileHandler}. The given name pattern is used as 368 * output filename, the file limit is set to zero (no limit), the file count 369 * is set to one; the remaining configuration is done using 370 * {@code LogManager} properties or their default values. This handler 371 * writes to only one file with no size limit. 372 * 373 * @param pattern 374 * the name pattern for the output file. 375 * @throws IOException 376 * if any I/O error occurs. 377 * @throws IllegalArgumentException 378 * if the pattern is empty. 379 * @throws NullPointerException 380 * if the pattern is {@code null}. 381 */ 382 public FileHandler(String pattern) throws IOException { 383 if (pattern.isEmpty()) { 384 throw new IllegalArgumentException("Pattern cannot be empty"); 385 } 386 init(pattern, null, Integer.valueOf(DEFAULT_LIMIT), Integer.valueOf(DEFAULT_COUNT)); 387 } 388 389 /** 390 * Construct a new {@code FileHandler}. The given name pattern is used as 391 * output filename, the file limit is set to zero (no limit), the file count 392 * is initialized to one and the value of {@code append} becomes the new 393 * instance's append mode. The remaining configuration is done using 394 * {@code LogManager} properties. This handler writes to only one file 395 * with no size limit. 396 * 397 * @param pattern 398 * the name pattern for the output file. 399 * @param append 400 * the append mode. 401 * @throws IOException 402 * if any I/O error occurs. 403 * @throws IllegalArgumentException 404 * if {@code pattern} is empty. 405 * @throws NullPointerException 406 * if {@code pattern} is {@code null}. 407 */ 408 public FileHandler(String pattern, boolean append) throws IOException { 409 if (pattern.isEmpty()) { 410 throw new IllegalArgumentException("Pattern cannot be empty"); 411 } 412 init(pattern, Boolean.valueOf(append), Integer.valueOf(DEFAULT_LIMIT), 413 Integer.valueOf(DEFAULT_COUNT)); 414 } 415 416 /** 417 * Construct a new {@code FileHandler}. The given name pattern is used as 418 * output filename, the maximum file size is set to {@code limit} and the 419 * file count is initialized to {@code count}. The remaining configuration 420 * is done using {@code LogManager} properties. This handler is configured 421 * to write to a rotating set of count files, when the limit of bytes has 422 * been written to one output file, another file will be opened instead. 423 * 424 * @param pattern 425 * the name pattern for the output file. 426 * @param limit 427 * the data amount limit in bytes of one output file, can not be 428 * negative. 429 * @param count 430 * the maximum number of files to use, can not be less than one. 431 * @throws IOException 432 * if any I/O error occurs. 433 * @throws IllegalArgumentException 434 * if {@code pattern} is empty, {@code limit < 0} or 435 * {@code count < 1}. 436 * @throws NullPointerException 437 * if {@code pattern} is {@code null}. 438 */ 439 public FileHandler(String pattern, int limit, int count) throws IOException { 440 if (pattern.isEmpty()) { 441 throw new IllegalArgumentException("Pattern cannot be empty"); 442 } 443 if (limit < 0 || count < 1) { 444 throw new IllegalArgumentException("limit < 0 || count < 1"); 445 } 446 init(pattern, null, Integer.valueOf(limit), Integer.valueOf(count)); 447 } 448 449 /** 450 * Construct a new {@code FileHandler}. The given name pattern is used as 451 * output filename, the maximum file size is set to {@code limit}, the file 452 * count is initialized to {@code count} and the append mode is set to 453 * {@code append}. The remaining configuration is done using 454 * {@code LogManager} properties. This handler is configured to write to a 455 * rotating set of count files, when the limit of bytes has been written to 456 * one output file, another file will be opened instead. 457 * 458 * @param pattern 459 * the name pattern for the output file. 460 * @param limit 461 * the data amount limit in bytes of one output file, can not be 462 * negative. 463 * @param count 464 * the maximum number of files to use, can not be less than one. 465 * @param append 466 * the append mode. 467 * @throws IOException 468 * if any I/O error occurs. 469 * @throws IllegalArgumentException 470 * if {@code pattern} is empty, {@code limit < 0} or 471 * {@code count < 1}. 472 * @throws NullPointerException 473 * if {@code pattern} is {@code null}. 474 */ 475 public FileHandler(String pattern, int limit, int count, boolean append) throws IOException { 476 if (pattern.isEmpty()) { 477 throw new IllegalArgumentException("Pattern cannot be empty"); 478 } 479 if (limit < 0 || count < 1) { 480 throw new IllegalArgumentException("limit < 0 || count < 1"); 481 } 482 init(pattern, Boolean.valueOf(append), Integer.valueOf(limit), Integer.valueOf(count)); 483 } 484 485 /** 486 * Flushes and closes all opened files. 487 */ 488 @Override 489 public void close() { 490 // release locks 491 super.close(); 492 allLocks.remove(fileName); 493 try { 494 FileChannel channel = lock.channel(); 495 lock.release(); 496 channel.close(); 497 File file = new File(fileName + LCK_EXT); 498 file.delete(); 499 } catch (IOException e) { 500 // ignore 501 } 502 } 503 504 /** 505 * Publish a {@code LogRecord}. 506 * 507 * @param record 508 * the log record to publish. 509 */ 510 @Override 511 public synchronized void publish(LogRecord record) { 512 super.publish(record); 513 flush(); 514 if (limit > 0 && output.getLength() >= limit) { 515 findNextGeneration(); 516 } 517 } 518 519 /** 520 * This output stream uses the decorator pattern to add measurement features 521 * to OutputStream which can detect the total size(in bytes) of output, the 522 * initial size can be set. 523 */ 524 static class MeasureOutputStream extends OutputStream { 525 526 OutputStream wrapped; 527 528 long length; 529 530 public MeasureOutputStream(OutputStream stream, long currentLength) { 531 wrapped = stream; 532 length = currentLength; 533 } 534 535 public MeasureOutputStream(OutputStream stream) { 536 this(stream, 0); 537 } 538 539 @Override 540 public void write(int oneByte) throws IOException { 541 wrapped.write(oneByte); 542 length++; 543 } 544 545 @Override 546 public void write(byte[] b, int off, int len) throws IOException { 547 wrapped.write(b, off, len); 548 length += len; 549 } 550 551 @Override 552 public void close() throws IOException { 553 wrapped.close(); 554 } 555 556 @Override 557 public void flush() throws IOException { 558 wrapped.flush(); 559 } 560 561 public long getLength() { 562 return length; 563 } 564 565 public void setLength(long newLength) { 566 length = newLength; 567 } 568 } 569} 570