Index: src/main/java/org/apache/log4j/DailyRollingFileAppender.java =================================================================== --- src/main/java/org/apache/log4j/DailyRollingFileAppender.java (revision 544346) +++ src/main/java/org/apache/log4j/DailyRollingFileAppender.java (working copy) @@ -28,6 +28,8 @@ import java.util.TimeZone; import java.util.Locale; +import org.apache.log4j.helpers.BackupDirectory; +import org.apache.log4j.helpers.FileMove; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.spi.LoggingEvent; @@ -179,6 +181,7 @@ // The gmtTimeZone is used only in computeCheckPeriod() method. static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT"); + protected String backupDirectory = null; /** The default constructor does nothing. */ @@ -212,6 +215,19 @@ return datePattern; } + public void setBackupDirectory(String directory) { + String val = directory.trim(); + backupDirectory = val; + } + + public String getBackupDirectory() { + return backupDirectory; + } + + protected String getScheduledFilename() { + return scheduledFilename; + } + public void activateOptions() { super.activateOptions(); if(datePattern != null && fileName != null) { @@ -312,17 +328,20 @@ // close current file, and rename it to datedFilename this.closeFile(); - File target = new File(scheduledFilename); + String backupFileName = BackupDirectory.createBackupFileName(backupDirectory, scheduledFilename); + LogLog.debug("backupFileName=" + backupFileName); + + File target = new File(backupFileName); if (target.exists()) { target.delete(); } File file = new File(fileName); - boolean result = file.renameTo(target); + boolean result = FileMove.move(file, target); if(result) { - LogLog.debug(fileName +" -> "+ scheduledFilename); + LogLog.debug(fileName +" -> "+ target); } else { - LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"]."); + LogLog.error("Failed to rename ["+fileName+"] to ["+target+"]."); } try { Index: src/main/java/org/apache/log4j/helpers/BackupDirectory.java =================================================================== --- src/main/java/org/apache/log4j/helpers/BackupDirectory.java (revision 0) +++ src/main/java/org/apache/log4j/helpers/BackupDirectory.java (revision 0) @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.log4j.helpers; + +import java.io.File; + +/** + Utility class for generating a backup directory name + */ +public class BackupDirectory { + + /** Create a backup file name + * Uses the log file name and the backup directory to generate the path for the backup file + * If the backup directory does not exist it will attempt to create the directory + * + * @param backupDirectory The backup directory path + * @param fileName The log file directory path + * @return The backup log file path + * */ + public final static String createBackupFileName(String backupDirectory, String fileName) { + String fileNameOnly = fileName; + File backupDir = null; + + if(backupDirectory != null) { + fileNameOnly = new File(fileName).getName(); + backupDir = new File(backupDirectory); + if(!backupDir.exists()) { + LogLog.debug("Backup directory does not exist attempting to create backupDirectory=" + backupDir); + if(!backupDir.mkdirs()) { + LogLog.warn("Could not create backupDirectory=" + backupDir + " using default directory instead"); + fileNameOnly = fileName; + backupDir = null; + } + } else if(!backupDir.isDirectory()) { + LogLog.warn("Backup directory is not a directory. backupDirectory=" + backupDir + " using default directory instead"); + fileNameOnly = fileName; + backupDir = null; + } + } + + return new File(backupDir, fileNameOnly).getPath(); + } +} Index: src/main/java/org/apache/log4j/helpers/FileMove.java =================================================================== --- src/main/java/org/apache/log4j/helpers/FileMove.java (revision 0) +++ src/main/java/org/apache/log4j/helpers/FileMove.java (revision 0) @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.log4j.helpers; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + Utility class for moving Files + */ +public class FileMove { + + /** Copy a File + * The renameTo method does not allow action across NFS mounted filesystems + * this method is the workaround + * see sun bug id:4073756 for more info + * + * @param fromFile The existing File + * @param toFile The new File + * @return true if and only if the renaming succeeded; + * false otherwise + * */ + public final static boolean copy(File fromFile, File toFile) { + + try { + FileInputStream in = new FileInputStream(fromFile); + FileOutputStream out = new FileOutputStream(toFile); + BufferedInputStream inBuffer = new BufferedInputStream(in); + BufferedOutputStream outBuffer = new + BufferedOutputStream(out); + + int theByte = 0; + + while ((theByte = inBuffer.read()) > -1) { + outBuffer.write(theByte); + } + + outBuffer.close(); + inBuffer.close(); + out.close(); + in.close(); + + // cleanup if files are not the same length + if (fromFile.length() != toFile.length()) { + toFile.delete(); + + return false; + } + + return true; + + } catch (IOException e) { + + return false; + } + } + + /** Move a File + * The renameTo method does not allow action across NFS mounted + * filesystems. this method is the workaround + * see sun bug id:4073756 for more info + * + * @param fromFile The existing File + * @param toFile The new File + * @return true if and only if the renaming succeeded; + * false otherwise + * */ + public final static boolean move(File fromFile, File toFile) { + + if(fromFile.renameTo(toFile)) { + return true; + } + + // delete if copy was successful, otherwise move will fail + if (copy(fromFile, toFile)) { + return fromFile.delete(); + } + + return false; + } +} Index: src/main/java/org/apache/log4j/RollingFileAppender.java =================================================================== --- src/main/java/org/apache/log4j/RollingFileAppender.java (revision 544346) +++ src/main/java/org/apache/log4j/RollingFileAppender.java (working copy) @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.Writer; import java.io.File; +import org.apache.log4j.helpers.BackupDirectory; +import org.apache.log4j.helpers.FileMove; import org.apache.log4j.helpers.OptionConverter; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.helpers.CountingQuietWriter; @@ -48,6 +50,8 @@ protected int maxBackupIndex = 1; private long nextRollover = 0; + + protected String backupDirectory = null; /** The default constructor simply calls its {@link @@ -102,6 +106,18 @@ return maxFileSize; } + public + void setBackupDirectory(String directory) { + String val = directory.trim(); + backupDirectory = val; + } + + public + String getBackupDirectory() { + return backupDirectory; + } + + /** Implements the usual roll over behaviour. @@ -130,33 +146,38 @@ } LogLog.debug("maxBackupIndex="+maxBackupIndex); + String backupFileName = BackupDirectory.createBackupFileName(backupDirectory, fileName); + LogLog.debug("backupFileName=" + backupFileName); + boolean renameSucceeded = true; // If maxBackups <= 0, then there is no file renaming to be done. if(maxBackupIndex > 0) { // Delete the oldest file, to keep Windows happy. - file = new File(fileName + '.' + maxBackupIndex); + file = new File(backupFileName + '.' + maxBackupIndex); if (file.exists()) renameSucceeded = file.delete(); // Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2} for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) { - file = new File(fileName + "." + i); - if (file.exists()) { - target = new File(fileName + '.' + (i + 1)); - LogLog.debug("Renaming file " + file + " to " + target); - renameSucceeded = file.renameTo(target); - } + file = new File(backupFileName + "." + i); + if (file.exists()) { + target = new File(backupFileName + '.' + (i + 1)); + + LogLog.debug("Renaming file " + file + " to " + target); + renameSucceeded = FileMove.move(file, target); + } } if(renameSucceeded) { // Rename fileName to fileName.1 - target = new File(fileName + "." + 1); + target = new File(backupFileName + "." + 1); this.closeFile(); // keep windows happy. file = new File(fileName); LogLog.debug("Renaming file " + file + " to " + target); - renameSucceeded = file.renameTo(target); + renameSucceeded = FileMove.move(file, target); + // // if file rename failed, reopen file with append = true // @@ -186,7 +207,7 @@ } } } - + public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize) Index: tests/build.xml =================================================================== --- tests/build.xml (revision 544346) +++ tests/build.xml (working copy) @@ -161,7 +161,7 @@ HierarchyThreshold, DefaultInit, SocketServer, XMLLayout, AsyncAppender, OptionConverter, BoundedFIFO, - CyclicBuffer, OR, + CyclicBuffer, BackupDirectory, OR, LevelMatchFilter, PatternParser, PatternLayout, RFA, ERFA, DRFA, NTEventLogAppender, Syslog, Socket"/> @@ -387,6 +387,15 @@ + + + + + + + + Index: tests/input/RFA2.properties =================================================================== --- tests/input/RFA2.properties (revision 0) +++ tests/input/RFA2.properties (revision 0) @@ -0,0 +1,13 @@ +log4j.rootLogger=DEBUG, testAppender +log4j.appender.testAppender=org.apache.log4j.RollingFileAppender +log4j.appender.testAppender.file=output/RFA-testBackup.log +log4j.appender.testAppender.BackupDirectory=output/archive +log4j.appender.testAppender.Append=false +log4j.appender.testAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.testAppender.layout.ConversionPattern=%m\n +log4j.appender.testAppender.maxFileSize=100 + +# Prevent internal log4j DEBUG messages from polluting the output. +log4j.logger.org.apache.log4j.PropertyConfigurator=INFO +log4j.logger.org.apache.log4j.config.PropertySetter=INFO +log4j.logger.org.apache.log4j.FileAppender=INFO Index: tests/run-tests.bat =================================================================== --- tests/run-tests.bat (revision 544346) +++ tests/run-tests.bat (working copy) @@ -75,6 +75,7 @@ java junit.textui.TestRunner org.apache.log4j.helpers.OptionConverterTestCase java junit.textui.TestRunner org.apache.log4j.helpers.BoundedFIFOTestCase java junit.textui.TestRunner org.apache.log4j.helpers.CyclicBufferTestCase +java junit.textui.TestRunner org.apache.log4j.helpers.BackupDirectoryTestCase java junit.textui.TestRunner org.apache.log4j.or.ORTestCase java junit.textui.TestRunner org.apache.log4j.varia.LevelMatchFilterTestCase java junit.textui.TestRunner org.apache.log4j.helpers.PatternParserTestCase Index: tests/src/java/org/apache/log4j/DRFATestCase.java =================================================================== --- tests/src/java/org/apache/log4j/DRFATestCase.java (revision 544346) +++ tests/src/java/org/apache/log4j/DRFATestCase.java (working copy) @@ -421,5 +421,48 @@ assertTrue(firstFile.length() > 0); } + + /** + * Tests rollOver with a minute periodicity. + * + * @throws IOException + * @throws InterruptedException + */ + public void testMinuteRolloverWithBackupDirectory() throws IOException, InterruptedException { + Layout layout = new SimpleLayout(); + String filename = "output/drfa_minuteRolloverBackup.log"; + String pattern = "'.'yyyy-MM-dd-HH-mm"; + DailyRollingFileAppender appender = + new DailyRollingFileAppender(layout, + filename, + pattern); + appender.setBackupDirectory("output/drfa_archive"); + Logger root = Logger.getRootLogger(); + root.addAppender(appender); + + root.info("Hello, World"); + String expectedFilename = appender.getScheduledFilename(); + String fileNameOnly = new File(expectedFilename).getName(); + File backupFile = new File("output/drfa_archive/" + fileNameOnly); + + // In theory this assertion could fail if this test is run twice in the same minute but it seems + // unlikely since we wait until the end of the current minute in the same test + assertFalse(backupFile.exists()); + + Calendar cal = Calendar.getInstance(); + long now = cal.getTime().getTime(); + cal.set(Calendar.SECOND, 3); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.MINUTE, 1); + + long until = cal.getTime().getTime(); + Thread.sleep(until - now); + + root.info("Hello, World"); + assertTrue("backup file " + backupFile + " does not exist", backupFile.exists()); + assertTrue(backupFile.length() > 0); + + } + } Index: tests/src/java/org/apache/log4j/helpers/BackupDirectoryTestCase.java =================================================================== --- tests/src/java/org/apache/log4j/helpers/BackupDirectoryTestCase.java (revision 0) +++ tests/src/java/org/apache/log4j/helpers/BackupDirectoryTestCase.java (revision 0) @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.log4j.helpers; + +import junit.framework.TestCase; + +import java.io.File; + + +/** + * Tests for BackupDirectory. + * + **/ +public class BackupDirectoryTestCase extends TestCase { + /** + * Create new instance of BackupDirectoryTest. + * @param testName test name + */ + public BackupDirectoryTestCase(final String testName) { + super(testName); + } + + public void testBackupDirectoryIsSubDirectoryOfFileDirectory() throws Exception { + String backupDirectory = "output" + File.separator + "archive"; + String fileName = "output" + File.separator + "testBackupDirectory.log"; + String backupFileName = BackupDirectory.createBackupFileName(backupDirectory, fileName); + assertTrue(new File(backupDirectory).exists()); + assertFalse(fileName.equals(backupFileName)); + assertFalse(new File(backupFileName).exists()); + assertFalse(new File(fileName).exists()); + } + + public void testBackupDirectoryIsSameAsFileDirectory() throws Exception { + String backupDirectory = "output"; + String fileName = backupDirectory + File.separator + "testBackupDirectory.log"; + String backupFileName = BackupDirectory.createBackupFileName(backupDirectory, fileName); + assertTrue(new File(backupDirectory).exists()); + assertEquals(fileName, backupFileName); + assertFalse(new File(backupFileName).exists()); + assertFalse(new File(fileName).exists()); + } + + public void testInvalidBackupDirectory() throws Exception { + String invalidDirectory = "+<=>*"; + String fileName = "output" + File.separator + "testBackupDirectory.log"; + String backupFileName = BackupDirectory.createBackupFileName(invalidDirectory, fileName); + assertFalse(new File(invalidDirectory).exists()); + assertEquals(fileName, backupFileName); + + assertFalse(new File(backupFileName).exists()); + assertFalse(new File(fileName).exists()); + } + + public void testBackupDirectoryIsAFileThatExists() throws Exception { + String backupDirectory = "output" + File.separator + "testBackupDirectory.log"; + String fileName = backupDirectory; + assertTrue(new File(fileName).createNewFile()); + + String backupFileName = BackupDirectory.createBackupFileName(backupDirectory, fileName); + assertTrue(new File(backupDirectory).exists()); + assertEquals(fileName, backupFileName); + + assertTrue(new File(backupFileName).exists()); + } +} Index: tests/src/java/org/apache/log4j/RFATestCase.java =================================================================== --- tests/src/java/org/apache/log4j/RFATestCase.java (revision 544346) +++ tests/src/java/org/apache/log4j/RFATestCase.java (working copy) @@ -233,5 +233,63 @@ } } + /** + * Test basic rolling functionality with backup directory using property file configuration. + */ + public void testBackupDirectoryFromConfigFile() throws Exception { + Logger logger = Logger.getLogger(RFATestCase.class); + PropertyConfigurator.configure("input/RFA2.properties"); + // Write exactly 10 bytes with each log + for (int i = 0; i < 25; i++) { + if (i < 10) { + logger.debug("Hello---" + i); + } else if (i < 100) { + logger.debug("Hello--" + i); + } + } + + assertTrue(new File("output/RFA-testBackup.log").exists()); + assertFalse(new File("output/RFA-testBackup.log.1").exists()); + assertTrue(new File("output/archive/RFA-testBackup.log.1").exists()); + } + + /** + * Test basic rolling functionality with a backup directory using API configuration. + */ + public void testBackupDirectoryThroughAPI() throws Exception { + Logger logger = Logger.getLogger(RFATestCase.class); + Logger root = Logger.getRootLogger(); + PatternLayout layout = new PatternLayout("%m\n"); + org.apache.log4j.RollingFileAppender rfa = + new org.apache.log4j.RollingFileAppender(); + rfa.setName("ROLLING"); + rfa.setLayout(layout); + rfa.setAppend(false); + rfa.setMaxBackupIndex(3); + rfa.setMaximumFileSize(100); + rfa.setFile("output/RFA-testBackup2.log"); + rfa.setBackupDirectory("output/archive2"); + rfa.activateOptions(); + root.addAppender(rfa); + + // Write exactly 10 bytes with each log + for (int i = 0; i < 55; i++) { + if (i < 10) { + logger.debug("Hello---" + i); + } else if (i < 100) { + logger.debug("Hello--" + i); + } + } + + assertTrue(new File("output/RFA-testBackup2.log").exists()); + assertFalse(new File("output/RFA-testBackup2.log.1").exists()); + assertTrue(new File("output/archive2/RFA-testBackup2.log.1").exists()); + assertFalse(new File("output/RFA-testBackup2.log.2").exists()); + assertTrue(new File("output/archive2/RFA-testBackup2.log.2").exists()); + assertFalse(new File("output/RFA-testBackup2.log.3").exists()); + assertTrue(new File("output/archive2/RFA-testBackup2.log.3").exists()); + assertFalse(new File("output/RFA-testBackup2.log.4").exists()); + assertFalse(new File("output/archive2/RFA-testBackup2.log.4").exists()); + } }