001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.rolling;
015
016import static ch.qos.logback.core.CoreConstants.UNBOUND_HISTORY;
017import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
018
019import java.io.File;
020import java.util.Date;
021import java.util.concurrent.Future;
022import java.util.concurrent.TimeUnit;
023import java.util.concurrent.TimeoutException;
024
025import ch.qos.logback.core.CoreConstants;
026import ch.qos.logback.core.rolling.helper.ArchiveRemover;
027import ch.qos.logback.core.rolling.helper.CompressionMode;
028import ch.qos.logback.core.rolling.helper.Compressor;
029import ch.qos.logback.core.rolling.helper.FileFilterUtil;
030import ch.qos.logback.core.rolling.helper.FileNamePattern;
031import ch.qos.logback.core.rolling.helper.RenameUtil;
032import ch.qos.logback.core.util.FileSize;
033
034/**
035 * <code>TimeBasedRollingPolicy</code> is both easy to configure and quite
036 * powerful. It allows the roll over to be made based on time. It is possible to
037 * specify that the roll over occur once per day, per week or per month.
038 * 
039 * <p>For more information, please refer to the online manual at
040 * http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy
041 * 
042 * @author Ceki G&uuml;lc&uuml;
043 */
044public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> {
045    static final String FNP_NOT_SET = "The FileNamePattern option must be set before using TimeBasedRollingPolicy. ";
046    // WCS: without compression suffix
047    FileNamePattern fileNamePatternWithoutCompSuffix;
048
049    private Compressor compressor;
050    private RenameUtil renameUtil = new RenameUtil();
051    Future<?> compressionFuture;
052    Future<?> cleanUpFuture;
053
054    private int maxHistory = UNBOUND_HISTORY;
055    protected FileSize totalSizeCap = new FileSize(UNBOUNDED_TOTAL_SIZE_CAP);
056
057    private ArchiveRemover archiveRemover;
058
059    TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedFileNamingAndTriggeringPolicy;
060
061    boolean cleanHistoryOnStart = false;
062
063    public void start() {
064        // set the LR for our utility object
065        renameUtil.setContext(this.context);
066
067        // find out period from the filename pattern
068        if (fileNamePatternStr != null) {
069            fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
070            determineCompressionMode();
071        } else {
072            addWarn(FNP_NOT_SET);
073            addWarn(CoreConstants.SEE_FNP_NOT_SET);
074            throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET);
075        }
076
077        compressor = new Compressor(compressionMode);
078        compressor.setContext(context);
079
080        // wcs : without compression suffix
081        fileNamePatternWithoutCompSuffix = new FileNamePattern(Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context);
082
083        addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file");
084
085        if (compressionMode == CompressionMode.ZIP) {
086            String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr);
087            zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context);
088        }
089
090        if (timeBasedFileNamingAndTriggeringPolicy == null) {
091            timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
092        }
093        timeBasedFileNamingAndTriggeringPolicy.setContext(context);
094        timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
095        timeBasedFileNamingAndTriggeringPolicy.start();
096
097        if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
098            addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
099            return;
100        }
101
102        // the maxHistory property is given to TimeBasedRollingPolicy instead of to
103        // the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient
104        // for the user at the cost of inconsistency here.
105        if (maxHistory != UNBOUND_HISTORY) {
106            archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
107            archiveRemover.setMaxHistory(maxHistory);
108            archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
109            if (cleanHistoryOnStart) {
110                addInfo("Cleaning on start up");
111                Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
112                cleanUpFuture = archiveRemover.cleanAsynchronously(now);
113            }
114        } else if (!isUnboundedTotalSizeCap()) {
115            addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
116        }
117
118        super.start();
119    }
120
121    protected boolean isUnboundedTotalSizeCap() {
122        return totalSizeCap.getSize() == UNBOUNDED_TOTAL_SIZE_CAP;
123    }
124
125    @Override
126    public void stop() {
127        if (!isStarted())
128            return;
129        waitForAsynchronousJobToStop(compressionFuture, "compression");
130        waitForAsynchronousJobToStop(cleanUpFuture, "clean-up");
131        super.stop();
132    }
133
134    private void waitForAsynchronousJobToStop(Future<?> aFuture, String jobDescription) {
135        if (aFuture != null) {
136            try {
137                aFuture.get(CoreConstants.SECONDS_TO_WAIT_FOR_COMPRESSION_JOBS, TimeUnit.SECONDS);
138            } catch (TimeoutException e) {
139                addError("Timeout while waiting for " + jobDescription + " job to finish", e);
140            } catch (Exception e) {
141                addError("Unexpected exception while waiting for " + jobDescription + " job to finish", e);
142            }
143        }
144    }
145
146    private String transformFileNamePattern2ZipEntry(String fileNamePatternStr) {
147        String slashified = FileFilterUtil.slashify(fileNamePatternStr);
148        return FileFilterUtil.afterLastSlash(slashified);
149    }
150
151    public void setTimeBasedFileNamingAndTriggeringPolicy(TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedTriggering) {
152        this.timeBasedFileNamingAndTriggeringPolicy = timeBasedTriggering;
153    }
154
155    public TimeBasedFileNamingAndTriggeringPolicy<E> getTimeBasedFileNamingAndTriggeringPolicy() {
156        return timeBasedFileNamingAndTriggeringPolicy;
157    }
158
159    public void rollover() throws RolloverFailure {
160
161        // when rollover is called the elapsed period's file has
162        // been already closed. This is a working assumption of this method.
163
164        String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
165
166        String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
167
168        if (compressionMode == CompressionMode.NONE) {
169            if (getParentsRawFileProperty() != null) {
170                renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
171            } // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == null }
172        } else {
173            if (getParentsRawFileProperty() == null) {
174                compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
175            } else {
176                compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
177            }
178        }
179
180        if (archiveRemover != null) {
181            Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
182            this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
183        }
184    }
185
186    Future<?> renameRawAndAsyncCompress(String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
187        String parentsRawFile = getParentsRawFileProperty();
188        String tmpTarget = nameOfCompressedFile + System.nanoTime() + ".tmp";
189        renameUtil.rename(parentsRawFile, tmpTarget);
190        return compressor.asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName);
191    }
192
193    /**
194     * 
195     * The active log file is determined by the value of the parent's filename
196     * option. However, in case the file name is left blank, then, the active log
197     * file equals the file name for the current period as computed by the
198     * <b>FileNamePattern</b> option.
199     * 
200     * <p>The RollingPolicy must know whether it is responsible for changing the
201     * name of the active file or not. If the active file name is set by the user
202     * via the configuration file, then the RollingPolicy must let it like it is.
203     * If the user does not specify an active file name, then the RollingPolicy
204     * generates one.
205     * 
206     * <p> To be sure that the file name used by the parent class has been
207     * generated by the RollingPolicy and not specified by the user, we keep track
208     * of the last generated name object and compare its reference to the parent
209     * file name. If they match, then the RollingPolicy knows it's responsible for
210     * the change of the file name.
211     * 
212     */
213    public String getActiveFileName() {
214        String parentsRawFileProperty = getParentsRawFileProperty();
215        if (parentsRawFileProperty != null) {
216            return parentsRawFileProperty;
217        } else {
218            return timeBasedFileNamingAndTriggeringPolicy.getCurrentPeriodsFileNameWithoutCompressionSuffix();
219        }
220    }
221
222    public boolean isTriggeringEvent(File activeFile, final E event) {
223        return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
224    }
225
226    /**
227     * Get the number of archive files to keep.
228     * 
229     * @return number of archive files to keep
230     */
231    public int getMaxHistory() {
232        return maxHistory;
233    }
234
235    /**
236     * Set the maximum number of archive files to keep.
237     * 
238     * @param maxHistory
239     *                number of archive files to keep
240     */
241    public void setMaxHistory(int maxHistory) {
242        this.maxHistory = maxHistory;
243    }
244
245    public boolean isCleanHistoryOnStart() {
246        return cleanHistoryOnStart;
247    }
248
249    /**
250     * Should archive removal be attempted on application start up? Default is false.
251     * @since 1.0.1
252     * @param cleanHistoryOnStart
253     */
254    public void setCleanHistoryOnStart(boolean cleanHistoryOnStart) {
255        this.cleanHistoryOnStart = cleanHistoryOnStart;
256    }
257
258    @Override
259    public String toString() {
260        return "c.q.l.core.rolling.TimeBasedRollingPolicy@"+this.hashCode();
261    }
262
263    public void setTotalSizeCap(FileSize totalSizeCap) {
264        addInfo("setting totalSizeCap to "+totalSizeCap.toString());
265        this.totalSizeCap = totalSizeCap;
266    }
267}