package org.rzo.yajsw.log; /* * @(#)FileHandler.java 1.37 10/03/23 * * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ import java.io.*; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.security.*; import java.util.LinkedList; import java.util.logging.ErrorManager; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.StreamHandler; /** * Simple file logging Handler. *

* The FileHandler can either write to a specified file, * or it can write to a rotating set of files. *

* For a rotating set of files, as each file reaches a given size * limit, it is closed, rotated out, and a new file opened. * Successively older files are named by adding "0", "1", "2", * etc into the base filename. *

* By default buffering is enabled in the IO libraries but each log * record is flushed out when it is complete. *

* By default the XMLFormatter class is used for formatting. *

* Configuration: * By default each FileHandler is initialized using the following * LogManager configuration properties. If properties are not defined * (or have invalid values) then the specified default values are used. *

*

*

* A pattern consists of a string that includes the following special * components that will be replaced at runtime: *

* If no "%g" field has been specified and the file count is greater * than one, then the generation number will be added to the end of * the generated filename, after a dot. *

* Thus for example a pattern of "%t/java%g.log" with a count of 2 * would typically cause log files to be written on Solaris to * /var/tmp/java0.log and /var/tmp/java1.log whereas on Windows 95 they * would be typically written to C:\TEMP\java0.log and C:\TEMP\java1.log *

* Generation numbers follow the sequence 0, 1, 2, etc. *

* Normally the "%u" unique field is set to 0. However, if the FileHandler * tries to open the filename and finds the file is currently in use by * another process it will increment the unique number field and try * again. This will be repeated until FileHandler finds a file name that * is not currently in use. If there is a conflict and no "%u" field has * been specified, it will be added at the end of the filename after a dot. * (This will be after any automatically added generation number.) *

* Thus if three processes were all trying to log to fred%u.%g.txt then * they might end up using fred0.0.txt, fred1.0.txt, fred2.0.txt as * the first file in their rotating sequences. *

* Note that the use of unique ids to avoid conflicts is only guaranteed * to work reliably when using a local disk file system. * * @version 1.37, 03/23/10 * @since 1.4 */ public class MyFileHandler extends StreamHandler { private MeteredStream meter; private boolean append; private int limit; // zero => no limit. private int count; private String pattern; private String lockFileName; private FileOutputStream lockStream; private File files[]; private static final int MAX_LOCKS = 100; private static java.util.HashMap locks = new java.util.HashMap(); private FileChangeListner _listener = null; interface FileChangeListner { void fileChange(File file, boolean added); } // A metered stream is a subclass of OutputStream that // (a) forwards all its output to a target stream // (b) keeps track of how many bytes have been written private class MeteredStream extends OutputStream { OutputStream out; int written; MeteredStream(OutputStream out, int written) { this.out = out; this.written = written; } public void write(int b) throws IOException { out.write(b); written++; } public void write(byte buff[]) throws IOException { out.write(buff); written += buff.length; } public void write(byte buff[], int off, int len) throws IOException { out.write(buff,off,len); written += len; } public void flush() throws IOException { out.flush(); } public void close() throws IOException { out.close(); } } private void open(File fname, boolean append) throws IOException { int len = 0; if (append) { len = (int)fname.length(); } if (!fname.getParentFile().exists()) fname.getParentFile().mkdirs(); FileOutputStream fout = new FileOutputStream(fname.toString(), append); BufferedOutputStream bout = new BufferedOutputStream(fout); meter = new MeteredStream(bout, len); setOutputStream(meter); } // Private method to configure a FileHandler from LogManager // properties and/or default values as specified in the class // javadoc. private void configure() { /* LogManager manager = LogManager.getLogManager(); String cname = getClass().getName(); pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log"); limit = manager.getIntProperty(cname + ".limit", 0); if (limit < 0) { limit = 0; } count = manager.getIntProperty(cname + ".count", 1); if (count <= 0) { count = 1; } append = manager.getBooleanProperty(cname + ".append", false); setLevel(manager.getLevelProperty(cname + ".level", Level.ALL)); setFilter(manager.getFilterProperty(cname + ".filter", null)); setFormatter(manager.getFormatterProperty(cname + ".formatter", new XMLFormatter())); try { setEncoding(manager.getStringProperty(cname +".encoding", null)); } catch (Exception ex) { try { setEncoding(null); } catch (Exception ex2) { // doing a setEncoding with null should always work. // assert false; } } */ } /** * Construct a default FileHandler. This will be configured * entirely from LogManager properties (or their default values). *

* @exception IOException if there are IO problems opening the files. * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control")). * @exception NullPointerException if pattern property is an empty String. */ public MyFileHandler() throws IOException, SecurityException { //checkAccess(); configure(); openFiles(); } /** * Initialize a FileHandler to write to the given filename. *

* The FileHandler is configured based on LogManager * properties (or their default values) except that the given pattern * argument is used as the filename pattern, the file limit is * set to no limit, and the file count is set to one. *

* There is no limit on the amount of data that may be written, * so use this with care. * * @param pattern the name of the output file * @exception IOException if there are IO problems opening the files. * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control"). * @exception IllegalArgumentException if pattern is an empty string */ public MyFileHandler(String pattern) throws IOException, SecurityException { if (pattern.length() < 1 ) { throw new IllegalArgumentException(); } //checkAccess(); configure(); this.pattern = pattern; this.limit = 0; this.count = 1; openFiles(); } /** * Initialize a FileHandler to write to the given filename, * with optional append. *

* The FileHandler is configured based on LogManager * properties (or their default values) except that the given pattern * argument is used as the filename pattern, the file limit is * set to no limit, the file count is set to one, and the append * mode is set to the given append argument. *

* There is no limit on the amount of data that may be written, * so use this with care. * * @param pattern the name of the output file * @param append specifies append mode * @exception IOException if there are IO problems opening the files. * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control"). * @exception IllegalArgumentException if pattern is an empty string */ public MyFileHandler(String pattern, boolean append) throws IOException, SecurityException { if (pattern.length() < 1 ) { throw new IllegalArgumentException(); } //checkAccess(); configure(); this.pattern = pattern; this.limit = 0; this.count = 1; this.append = append; openFiles(); } /** * Initialize a FileHandler to write to a set of files. When * (approximately) the given limit has been written to one file, * another file will be opened. The output will cycle through a set * of count files. *

* The FileHandler is configured based on LogManager * properties (or their default values) except that the given pattern * argument is used as the filename pattern, the file limit is * set to the limit argument, and the file count is set to the * given count argument. *

* The count must be at least 1. * * @param pattern the pattern for naming the output file * @param limit the maximum number of bytes to write to any one file * @param count the number of files to use * @exception IOException if there are IO problems opening the files. * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control"). * @exception IllegalArgumentException if limit < 0, or count < 1. * @exception IllegalArgumentException if pattern is an empty string */ public MyFileHandler(String pattern, int limit, int count) throws IOException, SecurityException { if (limit < 0 || count < 1 || pattern.length() < 1) { throw new IllegalArgumentException(); } //checkAccess(); configure(); this.pattern = pattern; this.limit = limit; this.count = count; openFiles(); } /** * Initialize a FileHandler to write to a set of files * with optional append. When (approximately) the given limit has * been written to one file, another file will be opened. The * output will cycle through a set of count files. *

* The FileHandler is configured based on LogManager * properties (or their default values) except that the given pattern * argument is used as the filename pattern, the file limit is * set to the limit argument, and the file count is set to the * given count argument, and the append mode is set to the given * append argument. *

* The count must be at least 1. * * @param pattern the pattern for naming the output file * @param limit the maximum number of bytes to write to any one file * @param count the number of files to use * @param append specifies append mode * @exception IOException if there are IO problems opening the files. * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control"). * @exception IllegalArgumentException if limit < 0, or count < 1. * @exception IllegalArgumentException if pattern is an empty string * */ public MyFileHandler(String pattern, int limit, int count, boolean append) throws IOException, SecurityException { if (limit < 0 || count < 1 || pattern.length() < 1) { throw new IllegalArgumentException(); } //checkAccess(); configure(); this.pattern = pattern; this.limit = limit; this.count = count; this.append = append; openFiles(); } public MyFileHandler(String pattern, int limit, int count, boolean append, PatternFormatter fileFormatter, Level logLevel, String encoding) throws SecurityException, IOException { this(pattern, limit, count, append); if (encoding != null) setEncoding(encoding); setFormatter(fileFormatter); setLevel(logLevel); } // Private method to open the set of output files, based on the // configured instance variables. private void openFiles() throws IOException { LogManager manager = LogManager.getLogManager(); manager.checkAccess(); if (count < 1) { throw new IllegalArgumentException("file count = " + count); } if (limit < 0) { limit = 0; } // We register our own ErrorManager during initialization // so we can record exceptions. InitializationErrorManager em = new InitializationErrorManager(); setErrorManager(em); // Create a lock file. This grants us exclusive access // to our set of output files, as long as we are alive. int unique = -1; for (;;) { unique++; if (unique > MAX_LOCKS) { throw new IOException("Couldn't get lock for " + pattern); } // Generate a lock file name from the "unique" int. lockFileName = generate(pattern, 0, unique, count).toString() + ".lck"; // Now try to lock that filename. // Because some systems (e.g. Solaris) can only do file locks // between processes (and not within a process), we first check // if we ourself already have the file locked. synchronized(locks) { if (locks.get(lockFileName) != null) { // We already own this lock, for a different FileHandler // object. Try again. continue; } FileChannel fc; try { lockStream = new FileOutputStream(lockFileName); fc = lockStream.getChannel(); } catch (IOException ix) { // We got an IOException while trying to open the file. // Try the next file. continue; } try { FileLock fl = fc.tryLock(); if (fl == null) { // We failed to get the lock. Try next file. continue; } // We got the lock OK. } catch (IOException ix) { // We got an IOException while trying to get the lock. // This normally indicates that locking is not supported // on the target directory. We have to proceed without // getting a lock. Drop through. } // We got the lock. Remember it. locks.put(lockFileName, lockFileName); break; } } files = new File[count]; for (int i = 0; i < count; i++) { files[i] = generate(pattern, i, unique, count); } // Create the initial log file. if (append) { open(files[0], true); } else { rotate(); } // Did we detect any exceptions during initialization? Exception ex = em.lastException; if (ex != null) { if (ex instanceof IOException) { throw (IOException) ex; } else if (ex instanceof SecurityException) { throw (SecurityException) ex; } else { throw new IOException("Exception: " + ex); } } // Install the normal default ErrorManager. setErrorManager(new ErrorManager()); } // Generate a filename from a pattern. static File generate(String pattern, int generation, int unique, int count) throws IOException { File file = null; String word = ""; int ix = 0; boolean sawg = false; boolean sawu = false; while (ix < pattern.length()) { char ch = pattern.charAt(ix); ix++; char ch2 = 0; if (ix < pattern.length()) { ch2 = Character.toLowerCase(pattern.charAt(ix)); } if (ch == '/') { if (file == null) { file = new File(word); } else { file = new File(file, word); } word = ""; continue; } else if (ch == '%') { if (ch2 == 't') { String tmpDir = System.getProperty("java.io.tmpdir"); if (tmpDir == null) { tmpDir = System.getProperty("user.home"); } file = new File(tmpDir); ix++; word = ""; continue; } else if (ch2 == 'h') { file = new File(System.getProperty("user.home")); if (isSetUID()) { // Ok, we are in a set UID program. For safety's sake // we disallow attempts to open files relative to %h. throw new IOException("can't use %h in set UID program"); } ix++; word = ""; continue; } else if (ch2 == 'g') { word = word + generation; sawg = true; ix++; continue; } else if (ch2 == 'u') { word = word + unique; sawu = true; ix++; continue; } else if (ch2 == '%') { word = word + "%"; ix++; continue; } } word = word + ch; } if (count > 1 && !sawg && generation != 0) { word = word + "." + generation; } if (unique > 0 && !sawu && unique != 0) { word = word + "." + unique; } if (word.length() > 0) { if (file == null) { file = new File(word); } else { file = new File(file, word); } } return file; } // Rotate the set of output files private synchronized void rotate() { Level oldLevel = getLevel(); setLevel(Level.OFF); super.close(); for (int i = count-2; i >= 0; i--) { File f1 = files[i]; File f2 = files[i+1]; if (f1.exists()) { if (f2.exists()) { f2.delete(); } else if (_listener != null) _listener.fileChange(f2, true); f1.renameTo(f2); } } try { open(files[0], false); } catch (IOException ix) { // We don't want to throw an exception here, but we // report the exception to any registered ErrorManager. reportError(null, ix, ErrorManager.OPEN_FAILURE); } setLevel(oldLevel); } /** * Format and publish a LogRecord. * * @param record description of the log event. A null record is * silently ignored and is not published */ public synchronized void publish(LogRecord record) { if (!isLoggable(record)) { return; } super.publish(record); flush(); if (limit > 0 && meter.written >= limit) { // We performed access checks in the "init" method to make sure // we are only initialized from trusted code. So we assume // it is OK to write the target files, even if we are // currently being called from untrusted code. // So it is safe to raise privilege here. AccessController.doPrivileged(new PrivilegedAction() { public Object run() { rotate(); return null; } }); } } public File currentFile() { return files[0]; } public LinkedList getCurrentFiles() { LinkedList result = new LinkedList(); for (File f : files) { if (f.exists()) result.addLast(f); else break; } return result; } public void setNewFileListner(FileChangeListner listener) { _listener = listener; } /** * Close all the files. * * @exception SecurityException if a security manager exists and if * the caller does not have LoggingPermission("control"). */ public synchronized void close() throws SecurityException { super.close(); // Unlock any lock file. if (lockFileName == null) { return; } try { // Closing the lock file's FileOutputStream will close // the underlying channel and free any locks. lockStream.close(); } catch (Exception ex) { // Problems closing the stream. Punt. } synchronized(locks) { locks.remove(lockFileName); } new File(lockFileName).delete(); lockFileName = null; lockStream = null; } private static class InitializationErrorManager extends ErrorManager { Exception lastException; public void error(String msg, Exception ex, int code) { lastException = ex; } } // Private native method to check if we are in a set UID program. private static native boolean isSetUID(); }