001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.commons.io.log.log4j.appender;
020
021import java.io.IOException;
022import java.io.Writer;
023import java.nio.charset.Charset;
024
025import lucee.commons.io.res.Resource;
026import lucee.commons.io.retirement.RetireListener;
027
028import org.apache.log4j.Layout;
029import org.apache.log4j.helpers.CountingQuietWriter;
030import org.apache.log4j.helpers.LogLog;
031import org.apache.log4j.spi.LoggingEvent;
032
033public class RollingResourceAppender extends ResourceAppender {
034        
035        
036        public static final long DEFAULT_MAX_FILE_SIZE = 10*1024*1024;
037        public static final int DEFAULT_MAX_BACKUP_INDEX = 10;
038
039        protected final long maxFileSize;
040        protected int  maxBackupIndex;
041        
042        private long nextRollover = 0;
043
044
045  /**
046     Instantiate a FileAppender and open the file designated by
047    <code>filename</code>. The opened filename will become the output
048    destination for this appender.
049
050    <p>The file will be appended to.  */
051  public RollingResourceAppender(Layout layout, Resource res,Charset charset,RetireListener listener) throws IOException {
052    this(layout, res,charset, true,DEFAULT_MAX_FILE_SIZE,DEFAULT_MAX_BACKUP_INDEX,60,listener);
053  }
054
055
056  /**
057    Instantiate a RollingFileAppender and open the file designated by
058    <code>filename</code>. The opened filename will become the ouput
059    destination for this appender.
060
061    <p>If the <code>append</code> parameter is true, the file will be
062    appended to. Otherwise, the file desginated by
063    <code>filename</code> will be truncated before being opened.
064  */
065  public RollingResourceAppender(Layout layout, Resource res,Charset charset, boolean append,RetireListener listener) throws IOException {
066    this(layout, res,charset, append,DEFAULT_MAX_FILE_SIZE,DEFAULT_MAX_BACKUP_INDEX,60,listener);
067  }
068  
069
070  /**
071    Instantiate a RollingFileAppender and open the file designated by
072    <code>filename</code>. The opened filename will become the ouput
073    destination for this appender.
074
075    <p>If the <code>append</code> parameter is true, the file will be
076    appended to. Otherwise, the file desginated by
077    <code>filename</code> will be truncated before being opened.
078  */
079  public RollingResourceAppender(Layout layout, Resource res,Charset charset, boolean append, long maxFileSize,int maxBackupIndex, int timeout,RetireListener listener) throws IOException {
080    super(layout, res,charset, append,timeout,listener);
081    this.maxFileSize=maxFileSize;
082    this.maxBackupIndex=maxBackupIndex;
083  }
084  
085
086/**
087     Returns the value of the <b>MaxBackupIndex</b> option.
088   */
089  public int getMaxBackupIndex() {
090    return maxBackupIndex;
091  }
092
093 /**
094    Get the maximum size that the output file is allowed to reach
095    before being rolled over to backup files.
096
097    @since 1.1
098 */
099  public long getMaximumFileSize() {
100    return maxFileSize;
101  }
102
103  /**
104     Implements the usual roll over behaviour.
105
106     <p>If <code>MaxBackupIndex</code> is positive, then files
107     {<code>File.1</code>, ..., <code>File.MaxBackupIndex -1</code>}
108     are renamed to {<code>File.2</code>, ...,
109     <code>File.MaxBackupIndex</code>}. Moreover, <code>File</code> is
110     renamed <code>File.1</code> and closed. A new <code>File</code> is
111     created to receive further log output.
112
113     <p>If <code>MaxBackupIndex</code> is equal to zero, then the
114     <code>File</code> is truncated with no backup files created.
115
116   */
117        public void rollOver() {
118                Resource target;
119                Resource file;
120
121                if (qw != null) {
122                        long size = ((CountingQuietWriter) qw).getCount();
123                        LogLog.debug("rolling over count=" + size);
124                        //   if operation fails, do not roll again until
125                        //   maxFileSize more bytes are written
126                        nextRollover = size + maxFileSize;
127                }
128                LogLog.debug("maxBackupIndex="+maxBackupIndex);
129
130                boolean renameSucceeded = true;
131                Resource parent = res.getParentResource();
132                
133                // If maxBackups <= 0, then there is no file renaming to be done.
134                if(maxBackupIndex > 0) {
135                        // Delete the oldest file, to keep Windows happy.
136                        file = parent.getRealResource(res.getName()+"."+maxBackupIndex+".bak");
137
138                        if (file.exists()) renameSucceeded = file.delete();
139
140                        // Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2}
141                        for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) {
142                                file = parent.getRealResource(res.getName()+"."+i+".bak");
143                                if (file.exists()) {
144                                        target = parent.getRealResource(res.getName()+"."+(i + 1)+".bak");
145                                        LogLog.debug("Renaming file " + file + " to " + target);
146                                        renameSucceeded = file.renameTo(target);
147                                }
148                        }
149
150                        if(renameSucceeded) {
151                                // Rename fileName to fileName.1
152                                target = parent.getRealResource(res.getName()+".1.bak");
153
154                                this.closeFile(); // keep windows happy.
155
156                                file = res;
157                                LogLog.debug("Renaming file " + file + " to " + target);
158                                renameSucceeded = file.renameTo(target);
159
160                                //   if file rename failed, reopen file with append = true
161
162                                if (!renameSucceeded) {
163                                        try {
164                                                this.setFile(true);
165                                        }
166                                        catch(IOException e) {
167                                                LogLog.error("setFile("+res+", true) call failed.", e);
168                                        }
169                                }
170                        }
171                }
172
173                //   if all renames were successful, then
174
175                if (renameSucceeded) {
176                        try {
177                                // This will also close the file. This is OK since multiple
178                                // close operations are safe.
179                                this.setFile(false);
180                                nextRollover = 0;
181                        }
182                        catch(IOException e) {
183                                LogLog.error("setFile("+res+", false) call failed.", e);
184                        }
185                }
186        }
187
188  public
189  synchronized
190  void setFile(boolean append) throws IOException {
191        long len = res.length();// this is done here, because in the location used the file is already locked
192    super.setFile(append);
193    if(append) {
194      ((CountingQuietWriter) qw).setCount(len);
195    }
196  }
197
198
199  /**
200     Set the maximum number of backup files to keep around.
201
202     <p>The <b>MaxBackupIndex</b> option determines how many backup
203     files are kept before the oldest is erased. This option takes
204     a positive integer value. If set to zero, then there will be no
205     backup files and the log file will be truncated when it reaches
206     <code>MaxFileSize</code>.
207   */
208  public
209  void setMaxBackupIndex(int maxBackups) {
210    this.maxBackupIndex = maxBackups;
211  }
212
213
214  protected
215  void setQWForFiles(Writer writer) {
216     this.qw = new CountingQuietWriter(writer, errorHandler);
217  }
218
219  /**
220     This method differentiates RollingFileAppender from its super
221     class.
222
223     @since 0.9.0
224  */
225  protected void subAppend(LoggingEvent event) {
226    super.subAppend(event);
227    if(res != null && qw != null) {
228        long size = ((CountingQuietWriter) qw).getCount();
229        if (size >= maxFileSize && size >= nextRollover) {
230            rollOver();
231        }
232    }
233   }
234}