001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.util;
030
031import java.io.File;
032import java.io.FileOutputStream;
033import java.io.IOException;
034
035import java.util.Date;
036import java.util.concurrent.Future;
037
038import ch.qos.logback.core.rolling.RolloverFailure;
039import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy;
040import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
041import ch.qos.logback.core.rolling.helper.ArchiveRemover;
042import ch.qos.logback.core.rolling.helper.AsynchronousCompressor;
043import ch.qos.logback.core.rolling.helper.CompressionMode;
044import ch.qos.logback.core.rolling.helper.Compressor;
045import ch.qos.logback.core.rolling.helper.FileFilterUtil;
046import ch.qos.logback.core.util.FileUtil;
047
048/**
049 * This Logback {@literal "rolling policy"} copies the contents of a log file
050 * (in this case, mcidasv.log) to the specified destination, and then
051 * {@literal "zeroes out"} the original log file. This approach allows McIDAS-V
052 * users to run a command like {@literal "tail -f mcidasv.log"} without any
053 * issue. Even on Windows.
054 */
055public class TailFriendlyRollingPolicy<E> extends TimeBasedRollingPolicy<E> {
056
057    private Compressor compressor;
058
059    Future<?> future;
060
061    @Override public void rollover() throws RolloverFailure {
062
063        // when rollover is called the elapsed period's file has
064        // been already closed. This is a working assumption of this method.
065
066        TimeBasedFileNamingAndTriggeringPolicy timeBasedFileNamingAndTriggeringPolicy = getTimeBasedFileNamingAndTriggeringPolicy();
067        String elapsedPeriodsFileName =
068            timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
069
070        String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
071
072        if (compressionMode == CompressionMode.NONE) {
073            String src = getParentsRawFileProperty();
074            if (src != null) {
075                if (isFileEmpty(src)) {
076                    addInfo("File '"+src+"' exists and is zero-length; avoiding copy");
077                } else {
078                    renameByCopying(src, elapsedPeriodsFileName);
079                }
080            } // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == null }
081        } else {
082            if (getParentsRawFileProperty() == null) {
083                future = asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
084            } else {
085                future = renamedRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
086            }
087        }
088
089        ArchiveRemover archiveRemover = getTimeBasedFileNamingAndTriggeringPolicy().getArchiveRemover();
090
091        if (archiveRemover != null) {
092            archiveRemover.clean(new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime()));
093        }
094    }
095
096    Future<?> asyncCompress(String nameOfFile2Compress,
097                            String nameOfCompressedFile, String innerEntryName)
098        throws RolloverFailure
099    {
100        AsynchronousCompressor ac = new AsynchronousCompressor(compressor);
101        return ac.compressAsynchronously(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
102    }
103
104    Future<?> renamedRawAndAsyncCompress(String nameOfCompressedFile,
105                                         String innerEntryName)
106        throws RolloverFailure
107    {
108        String parentsRawFile = getParentsRawFileProperty();
109        String tmpTarget = parentsRawFile + System.nanoTime() + ".tmp";
110        renameByCopying(parentsRawFile, tmpTarget);
111        return asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName);
112    }
113
114    /**
115     * Copies the contents of {@code src} into {@code target}, and then
116     * {@literal "zeroes out"} {@code src}.
117     *
118     * @param src Path to the file to be copied. Cannot be {@code null}.
119     * @param target Path to the destination file. Cannot be {@code null}.
120     * 
121     * @throws RolloverFailure if copying failed.
122     */
123    public void renameByCopying(String src, String target)
124        throws RolloverFailure
125    {
126        FileUtil fileUtil = new FileUtil(getContext());
127        fileUtil.copy(src, target);
128        try (FileOutputStream writer = new FileOutputStream(src)) {
129            addInfo("zeroing out " + src);
130        } catch (IOException e) {
131            addError("Could not reset " + src, e);
132        }
133    }
134
135    /**
136     * Determine if the file at the given path is zero length.
137     *
138     * @param filepath Path to the file to be tested. Cannot be {@code null}.
139     *
140     * @return {@code true} if {@code filepath} exists and is empty.
141     */
142    private static boolean isFileEmpty(String filepath) {
143        File f = new File(filepath);
144        return f.exists() && (f.length() == 0L);
145    }
146}