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;
015
016import static ch.qos.logback.core.CoreConstants.CODES_URL;
017import static ch.qos.logback.core.CoreConstants.MORE_INFO_PREFIX;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.channels.FileChannel;
022import java.nio.channels.FileLock;
023import java.util.Map;
024import java.util.Map.Entry;
025
026import ch.qos.logback.core.recovery.ResilientFileOutputStream;
027import ch.qos.logback.core.util.ContextUtil;
028import ch.qos.logback.core.util.FileSize;
029import ch.qos.logback.core.util.FileUtil;
030
031/**
032 * FileAppender appends log events to a file.
033 * 
034 * For more information about this appender, please refer to the online manual
035 * at http://logback.qos.ch/manual/appenders.html#FileAppender
036 * 
037 * @author Ceki Gülcü
038 */
039public class FileAppender<E> extends OutputStreamAppender<E> {
040
041    public static final long DEFAULT_BUFFER_SIZE = 8192;
042
043    static protected String COLLISION_WITH_EARLIER_APPENDER_URL = CODES_URL + "#earlier_fa_collision";
044
045    /**
046     * Append to or truncate the file? The default value for this variable is
047     * <code>true</code>, meaning that by default a <code>FileAppender</code> will
048     * append to an existing file and not truncate it.
049     */
050    protected boolean append = true;
051
052    /**
053     * The name of the active log file.
054     */
055    protected String fileName = null;
056
057    private boolean prudent = false;
058
059    private FileSize bufferSize = new FileSize(DEFAULT_BUFFER_SIZE);
060
061    /**
062     * The <b>File</b> property takes a string value which should be the name of the
063     * file to append to.
064     */
065    public void setFile(String file) {
066        if (file == null) {
067            fileName = file;
068        } else {
069            // Trim spaces from both ends. The users probably does not want
070            // trailing spaces in file names.
071            fileName = file.trim();
072        }
073    }
074
075    /**
076     * Returns the value of the <b>Append</b> property.
077     */
078    public boolean isAppend() {
079        return append;
080    }
081
082    /**
083     * This method is used by derived classes to obtain the raw file property.
084     * Regular users should not be calling this method.
085     * 
086     * @return the value of the file property
087     */
088    final public String rawFileProperty() {
089        return fileName;
090    }
091
092    /**
093     * Returns the value of the <b>File</b> property.
094     * 
095     * <p>
096     * This method may be overridden by derived classes.
097     * 
098     */
099    public String getFile() {
100        return fileName;
101    }
102
103    /**
104     * If the value of <b>File</b> is not <code>null</code>, then {@link #openFile}
105     * is called with the values of <b>File</b> and <b>Append</b> properties.
106     */
107    public void start() {
108        int errors = 0;
109        if (getFile() != null) {
110            addInfo("File property is set to [" + fileName + "]");
111
112            if (prudent) {
113                if (!isAppend()) {
114                    setAppend(true);
115                    addWarn("Setting \"Append\" property to true on account of \"Prudent\" mode");
116                }
117            }
118
119            if (checkForFileCollisionInPreviousFileAppenders()) {
120                addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
121                addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
122                errors++;
123            } else {
124                // file should be opened only if collision free
125                try {
126                    openFile(getFile());
127                } catch (java.io.IOException e) {
128                    errors++;
129                    addError("openFile(" + fileName + "," + append + ") call failed.", e);
130                }
131            }
132        } else {
133            errors++;
134            addError("\"File\" property not set for appender named [" + name + "].");
135        }
136        if (errors == 0) {
137            super.start();
138        }
139    }
140
141    @Override
142    public void stop() {
143        if(!isStarted())
144            return;
145
146        super.stop();
147
148        Map<String, String> map = ContextUtil.getFilenameCollisionMap(context);
149        if (map == null || getName() == null)
150            return;
151
152        map.remove(getName());
153    }
154
155    protected boolean checkForFileCollisionInPreviousFileAppenders() {
156        boolean collisionsDetected = false;
157        if (fileName == null) {
158            return false;
159        }
160        @SuppressWarnings("unchecked")
161        Map<String, String> previousFilesMap = (Map<String, String>) context
162                .getObject(CoreConstants.FA_FILENAME_COLLISION_MAP);
163        if (previousFilesMap == null) {
164            return collisionsDetected;
165        }
166        for (Entry<String, String> entry : previousFilesMap.entrySet()) {
167            if (fileName.equals(entry.getValue())) {
168                addErrorForCollision("File", entry.getValue(), entry.getKey());
169                collisionsDetected = true;
170            }
171        }
172        if (name != null) {
173            previousFilesMap.put(getName(), fileName);
174        }
175        return collisionsDetected;
176    }
177
178    protected void addErrorForCollision(String optionName, String optionValue, String appenderName) {
179        addError("'" + optionName + "' option has the same value \"" + optionValue + "\" as that given for appender ["
180                + appenderName + "] defined earlier.");
181    }
182
183    /**
184     * <p>
185     * Sets and <i>opens</i> the file where the log output will go. The specified
186     * file must be writable.
187     * 
188     * <p>
189     * If there was already an opened file, then the previous file is closed first.
190     * 
191     * <p>
192     * <b>Do not use this method directly. To configure a FileAppender or one of its
193     * subclasses, set its properties one by one and then call start().</b>
194     * 
195     * @param file_name The path to the log file.
196     */
197    public void openFile(String file_name) throws IOException {
198        streamWriteLock.lock();
199        try {
200            File file = new File(file_name);
201            boolean result = FileUtil.createMissingParentDirectories(file);
202            if (!result) {
203                addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
204            }
205
206            ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
207            resilientFos.setContext(context);
208            setOutputStream(resilientFos);
209        } finally {
210            streamWriteLock.unlock();
211        }
212    }
213
214    /**
215     * @see #setPrudent(boolean)
216     * 
217     * @return true if in prudent mode
218     */
219    public boolean isPrudent() {
220        return prudent;
221    }
222
223    /**
224     * When prudent is set to true, file appenders from multiple JVMs can safely
225     * write to the same file.
226     * 
227     * @param prudent
228     */
229    public void setPrudent(boolean prudent) {
230        this.prudent = prudent;
231    }
232
233    public void setAppend(boolean append) {
234        this.append = append;
235    }
236
237    public void setBufferSize(FileSize bufferSize) {
238        addInfo("Setting bufferSize to [" + bufferSize.toString() + "]");
239        this.bufferSize = bufferSize;
240    }
241
242    @Override
243    protected void writeOut(E event) throws IOException {
244        if (prudent) {
245            safeWriteOut(event);
246        } else {
247            super.writeOut(event);
248        }
249    }
250
251    private void safeWriteOut(E event) {
252        byte[] byteArray = this.encoder.encode(event);
253        if (byteArray == null || byteArray.length == 0)
254            return;
255
256        streamWriteLock.lock();
257        try {
258           safeWriteBytes(byteArray);
259        } finally {
260            streamWriteLock.unlock();
261        }
262    }
263
264    private void safeWriteBytes(byte[] byteArray) {
265        ResilientFileOutputStream resilientFOS = (ResilientFileOutputStream) getOutputStream();
266        FileChannel fileChannel = resilientFOS.getChannel();
267        if (fileChannel == null) {
268            return;
269        }
270
271        // Clear any current interrupt (see LOGBACK-875)
272        boolean interrupted = Thread.interrupted();
273
274        FileLock fileLock = null;
275        try {
276            fileLock = fileChannel.lock();
277            long position = fileChannel.position();
278            long size = fileChannel.size();
279            if (size != position) {
280                fileChannel.position(size);
281            }
282            writeByteArrayToOutputStreamWithPossibleFlush(byteArray);
283        } catch (IOException e) {
284            // Mainly to catch FileLockInterruptionExceptions (see LOGBACK-875)
285            resilientFOS.postIOFailure(e);
286        } finally {
287            releaseFileLock(fileLock);
288
289            // Re-interrupt if we started in an interrupted state (see LOGBACK-875)
290            if (interrupted) {
291                Thread.currentThread().interrupt();
292            }
293        }
294    }
295
296    private void releaseFileLock(FileLock fileLock) {
297        if (fileLock != null && fileLock.isValid()) {
298            try {
299                fileLock.release();
300            } catch (IOException e) {
301                addError("failed to release lock", e);
302            }
303        }
304    }
305}