// Module:  Log4CPLUS
// File:    fileappender.cxx
// Created: 6/2001
// Author:  Tad E. Smith
//
//
// Copyright (C) Tad E. Smith  All rights reserved.
//
// This software is published under the terms of the Apache Software
// License version 1.1, a copy of which has been included with this
// distribution in the LICENSE.APL file.
//
// $Log: fileappender.cxx,v $
// Revision 1.23  2004/01/29 03:08:18  tcsmith
// Fixed Bug #842280 - "'Append' mode when using FileAppended from cfg file."
//
// Revision 1.22  2003/09/10 06:42:17  tcsmith
// Modified calculateNextRolloverTime() to remove the unnecessary break statements
// in the switch statement.
//
// Revision 1.21  2003/08/29 05:03:40  tcsmith
// Changed rolloverFiles() to take a log4cplus::tstring instead of a std::string.
//
// Revision 1.20  2003/08/28 05:09:38  tcsmith
// The DailyRollingFileAppender now performs a "rollover" on close().  If the "rollover
// file" exists, then it will be renamed according to the pattern that is used by the
// RollingFileAppender class.
//
// Revision 1.19  2003/08/05 06:24:12  tcsmith
// Now initialize the "immediateFlush" field in all ctors.
//
// Revision 1.18  2003/07/30 05:19:02  tcsmith
// Added the "immediateFlush" field to the FileAppender class.
//
// Revision 1.17  2003/06/29 02:05:01  tcsmith
// Now using the new #defined type for open_mode.
//
// Revision 1.16  2003/06/28 17:56:19  tcsmith
// Changed the FileAppender ctors to take an 'int' instead of 'open_mode'.
//
// Revision 1.15  2003/06/26 21:34:02  baldheadedguy
// Added LOG4CPLUS_TEXT to strings in DailyRollingFileAppender::getFilename and
// DailyRollingFileAppender constructor.  In both cases, the comiple was failing
// under UNICODE.
//
// Revision 1.14  2003/06/23 23:14:12  tcsmith
// Corrected the DailyRollingFileAppender's weekly and montly calculations.
//
// Revision 1.13  2003/06/23 20:56:43  tcsmith
// Modified to support the changes in the spi::InternalLoggingEvent class.
//
// Revision 1.12  2003/06/13 20:56:24  tcsmith
// Made changes to make the DEC cXX 6.1 compiler happy.
//
// Revision 1.11  2003/06/12 23:55:49  tcsmith
// Initial implementation of the DailyRollingFileAppender class.
//
// Revision 1.10  2003/06/06 17:04:31  tcsmith
// Changed the ctor to take a 'const' Properties object.
//
// Revision 1.9  2003/06/03 20:19:42  tcsmith
// Modified the close() method to set "closed = true;".
//
// Revision 1.8  2003/04/19 23:04:31  tcsmith
// Fixed UNICODE support.
//
// Revision 1.7  2003/04/18 21:56:39  tcsmith
// Converted from std::string to log4cplus::tstring.
//
// Revision 1.6  2003/04/03 00:49:07  tcsmith
// Standardized the formatting.
//

#include <log4cplus/fileappender.h>
#include <log4cplus/layout.h>
#include <log4cplus/streams.h>
#include <log4cplus/helpers/loglog.h>
#include <log4cplus/helpers/stringhelper.h>
#include <log4cplus/helpers/timehelper.h>
#include <log4cplus/spi/loggingevent.h>
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>

using namespace std;
using namespace log4cplus;
using namespace log4cplus::helpers;

#define MINIMUM_ROLLING_LOG_SIZE 200*1024L


///////////////////////////////////////////////////////////////////////////////
// File LOCAL definitions
///////////////////////////////////////////////////////////////////////////////

namespace {
    
    void rolloverFiles(const log4cplus::tstring& filename, unsigned int maxBackupIndex)
    {
        SharedObjectPtr<LogLog> loglog = LogLog::getLogLog();

        // Delete the oldest file
        log4cplus::tostringstream buffer;
        buffer << filename << LOG4CPLUS_TEXT('.') << maxBackupIndex;
        remove(LOG4CPLUS_TSTRING_TO_STRING(buffer.str()).c_str());

        // Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2}
        for(int i=maxBackupIndex - 1; i >= 1; i--) {
            log4cplus::tostringstream source;
            log4cplus::tostringstream target;

            source << filename << LOG4CPLUS_TEXT('.') << i;
            target << filename << LOG4CPLUS_TEXT('.') << (i+1);
            if(rename(LOG4CPLUS_TSTRING_TO_STRING(source.str()).c_str(), 
                      LOG4CPLUS_TSTRING_TO_STRING(target.str()).c_str()) == 0) 
            {
                loglog->debug(  LOG4CPLUS_TEXT("Renamed file ") 
                              + source.str() 
                              + LOG4CPLUS_TEXT(" to ")
                              + target.str());
            }
        }
    } // end rolloverFiles()
    
}



///////////////////////////////////////////////////////////////////////////////
// log4cplus::FileAppender ctors and dtor
///////////////////////////////////////////////////////////////////////////////

log4cplus::FileAppender::FileAppender(const log4cplus::tstring& filename, 
                                      LOG4CPLUS_OPEN_MODE_TYPE mode,
                                      bool immediateFlush)
: immediateFlush(immediateFlush)
{
    init(filename, mode);
}


log4cplus::FileAppender::FileAppender(const Properties& properties, 
                                      LOG4CPLUS_OPEN_MODE_TYPE mode)
: Appender(properties),
  immediateFlush(true)
{
    bool append = (mode == ios::app);
    tstring filename = properties.getProperty( LOG4CPLUS_TEXT("File") );
    if(filename.length() == 0) {
        getErrorHandler()->error( LOG4CPLUS_TEXT("Invalid filename") );
        return;
    }
    if(properties.exists( LOG4CPLUS_TEXT("ImmediateFlush") )) {
        tstring tmp = properties.getProperty( LOG4CPLUS_TEXT("ImmediateFlush") );
        immediateFlush = (toLower(tmp) == LOG4CPLUS_TEXT("true"));
    }
    if(properties.exists( LOG4CPLUS_TEXT("Append") )) {
        tstring tmp = properties.getProperty( LOG4CPLUS_TEXT("Append") );
        append = (toLower(tmp) == LOG4CPLUS_TEXT("true"));
    }

    init(filename, (append ? ios::app : ios::trunc));
}



void
log4cplus::FileAppender::init(const log4cplus::tstring& filename, 
                              LOG4CPLUS_OPEN_MODE_TYPE mode)
{
    this->filename = filename;
    out.open(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), mode);

    if(!out.good()) {
        getErrorHandler()->error(  LOG4CPLUS_TEXT("Unable to open file: ") 
                                 + filename);
        return;
    }
    getLogLog().debug(LOG4CPLUS_TEXT("Just opened file: ") + filename);
}



log4cplus::FileAppender::~FileAppender()
{
    destructorImpl();
}



///////////////////////////////////////////////////////////////////////////////
// log4cplus::FileAppender public methods
///////////////////////////////////////////////////////////////////////////////

void 
log4cplus::FileAppender::close()
{
    LOG4CPLUS_BEGIN_SYNCHRONIZE_ON_MUTEX( access_mutex )
        out.close();
        closed = true;
    LOG4CPLUS_END_SYNCHRONIZE_ON_MUTEX
}



///////////////////////////////////////////////////////////////////////////////
// log4cplus::FileAppender protected methods
///////////////////////////////////////////////////////////////////////////////

// This method does not need to be locked since it is called by
// doAppend() which performs the locking
void
log4cplus::FileAppender::append(const spi::InternalLoggingEvent& event)
{
    if(!out.good()) {
        getErrorHandler()->error(  LOG4CPLUS_TEXT("file is not open: ") 
                                 + filename);
        return;
    }

    layout->formatAndAppend(out, event);
    if(immediateFlush) {
        out.flush();
    }
}




///////////////////////////////////////////////////////////////////////////////
// log4cplus::RollingFileAppender ctors and dtor
///////////////////////////////////////////////////////////////////////////////

log4cplus::RollingFileAppender::RollingFileAppender(const log4cplus::tstring& filename,
                                                    long maxFileSize,
                                                    int maxBackupIndex,
                                                    bool immediateFlush)
: FileAppender(filename, ios::app, immediateFlush)
{
    init(maxFileSize, maxBackupIndex);
}



log4cplus::RollingFileAppender::RollingFileAppender(const Properties& properties)
: FileAppender(properties, ios::app)
{
    int maxFileSize = 10*1024*1024;
    int maxBackupIndex = 1;
    if(properties.exists( LOG4CPLUS_TEXT("MaxFileSize") )) {
        tstring tmp = properties.getProperty( LOG4CPLUS_TEXT("MaxFileSize") );
        tmp = toUpper(tmp);
        maxFileSize = atoi(LOG4CPLUS_TSTRING_TO_STRING(tmp).c_str());
        if(tmp.find( LOG4CPLUS_TEXT("MB") ) == (tmp.length() - 2)) {
            maxFileSize *= (1024 * 1024); // convert to megabytes
        }
        if(tmp.find( LOG4CPLUS_TEXT("KB") ) == (tmp.length() - 2)) {
            maxFileSize *= 1024; // convert to kilobytes
        }
    }

    if(properties.exists( LOG4CPLUS_TEXT("MaxBackupIndex") )) {
        tstring tmp = properties.getProperty(LOG4CPLUS_TEXT("MaxBackupIndex"));
        maxBackupIndex = atoi(LOG4CPLUS_TSTRING_TO_STRING(tmp).c_str());
    }

    init(maxFileSize, maxBackupIndex);
}



void
log4cplus::RollingFileAppender::init(long maxFileSize, int maxBackupIndex)
{
    this->maxFileSize = max(maxFileSize, MINIMUM_ROLLING_LOG_SIZE);
    this->maxBackupIndex = max(maxBackupIndex, 1);
}



log4cplus::RollingFileAppender::~RollingFileAppender()
{
    destructorImpl();
}




///////////////////////////////////////////////////////////////////////////////
// log4cplus::RollingFileAppender protected methods
///////////////////////////////////////////////////////////////////////////////

// This method does not need to be locked since it is called by
// doAppend() which performs the locking
void
log4cplus::RollingFileAppender::append(const spi::InternalLoggingEvent& event)
{
    if(!out.good()) {
        getErrorHandler()->error(  LOG4CPLUS_TEXT("file is not open: ") 
                                 + filename);
        return;
    }

    layout->formatAndAppend(out, event);
    if(immediateFlush) {
        out.flush();
    }
        
    if(out.tellp() > maxFileSize) {
        rollover();
    }
}



void 
log4cplus::RollingFileAppender::rollover()
{
    // If maxBackups <= 0, then there is no file renaming to be done.
    if(maxBackupIndex > 0) {
        rolloverFiles(filename, maxBackupIndex);

        // Close the current file
        out.close();
        out.clear(); // reset flags since the C++ standard specified that all the
                     // flags should remain unchanged on a close

        // Rename fileName to fileName.1
        log4cplus::tstring target = filename + LOG4CPLUS_TEXT(".1");
        getLogLog().debug(  LOG4CPLUS_TEXT("Renaming file ") 
                          + filename 
                          + LOG4CPLUS_TEXT(" to ")
                          + target);
        rename(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), 
               LOG4CPLUS_TSTRING_TO_STRING(target).c_str());

        // Open a new file
        out.open(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), 
                 ios::out | ios::trunc);
    }
    else {
        getLogLog().debug(  filename 
                          + LOG4CPLUS_TEXT(" has no backups specified"));
        // Close the current file
        out.close();
        out.clear(); // reset flags since the C++ standard specified that all the
                     // flags should remain unchanged on a close

        // Open it up again in truncation mode
        out.open(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), 
                 ios::out | ios::trunc);
    }
}



///////////////////////////////////////////////////////////////////////////////
// log4cplus::DailyRollingFileAppender ctors and dtor
///////////////////////////////////////////////////////////////////////////////

DailyRollingFileAppender::DailyRollingFileAppender(const log4cplus::tstring& filename,
                                                   DailyRollingFileSchedule schedule,
                                                   bool immediateFlush,
                                                   int maxBackupIndex)
: FileAppender(filename, ios::app, immediateFlush),
  maxBackupIndex(maxBackupIndex)
{
    init(schedule);
}



DailyRollingFileAppender::DailyRollingFileAppender(const Properties& properties)
: FileAppender(properties, ios::app),
  maxBackupIndex(10)
{
    DailyRollingFileSchedule theSchedule = DAILY;
    tstring scheduleStr = properties.getProperty(LOG4CPLUS_TEXT("Schedule"));\
    scheduleStr = toUpper(scheduleStr);

    if(scheduleStr == LOG4CPLUS_TEXT("MONTHLY")) {
        theSchedule = MONTHLY;
    }
    else if(scheduleStr == LOG4CPLUS_TEXT("WEEKLY")) {
        theSchedule = WEEKLY;
    }
    else if(scheduleStr == LOG4CPLUS_TEXT("DAILY")) {
        theSchedule = DAILY;
    }
    else if(scheduleStr == LOG4CPLUS_TEXT("TWICE_DAILY")) {
        theSchedule = TWICE_DAILY;
    }
    else if(scheduleStr == LOG4CPLUS_TEXT("HOURLY")) {
        theSchedule = HOURLY;
    }
    else if(scheduleStr == LOG4CPLUS_TEXT("MINUTELY")) {
        theSchedule = MINUTELY;
    }
    else {
        getLogLog().warn(  LOG4CPLUS_TEXT("DailyRollingFileAppender::ctor()- \"Schedule\" not valid: ")
                         + properties.getProperty(LOG4CPLUS_TEXT("Schedule")));
    }
    
    if(properties.exists( LOG4CPLUS_TEXT("MaxBackupIndex") )) {
        tstring tmp = properties.getProperty(LOG4CPLUS_TEXT("MaxBackupIndex"));
        maxBackupIndex = atoi(LOG4CPLUS_TSTRING_TO_STRING(tmp).c_str());
    }

    init(theSchedule);
}



void
DailyRollingFileAppender::init(DailyRollingFileSchedule schedule)
{
    this->schedule = schedule;

    Time now = Time::gettimeofday();
    now.usec(0);
    struct tm time;
    now.localtime(&time);

    time.tm_sec = 0;
    switch(schedule) {
    case MONTHLY:
        time.tm_mday = 1;
        time.tm_hour = 0;
        time.tm_min = 0;
        break;

    case WEEKLY:
        time.tm_mday -= (time.tm_wday % 7);
        time.tm_hour = 0;
        time.tm_min = 0;
        break;

    case DAILY:
        time.tm_hour = 0;
        time.tm_min = 0;
        break;

    case TWICE_DAILY:
        if(time.tm_hour >= 12) {
            time.tm_hour = 12;
        }
        else {
            time.tm_hour = 0;
        }
        time.tm_min = 0;
        break;

    case HOURLY:
        time.tm_min = 0;
        break;

    case MINUTELY:
        break;
    };
    now.setTime(&time);

    scheduledFilename = getFilename(now);
    nextRolloverTime = calculateNextRolloverTime(now);
}



DailyRollingFileAppender::~DailyRollingFileAppender()
{
    destructorImpl();
}




///////////////////////////////////////////////////////////////////////////////
// log4cplus::DailyRollingFileAppender public methods
///////////////////////////////////////////////////////////////////////////////

void
DailyRollingFileAppender::close()
{
    rollover();
    FileAppender::close();
}



///////////////////////////////////////////////////////////////////////////////
// log4cplus::DailyRollingFileAppender protected methods
///////////////////////////////////////////////////////////////////////////////

// This method does not need to be locked since it is called by
// doAppend() which performs the locking
void
DailyRollingFileAppender::append(const spi::InternalLoggingEvent& event)
{
    if(!out.good()) {
        getErrorHandler()->error(  LOG4CPLUS_TEXT("file is not open: ") 
                                 + filename);
        return;
    }

    if(event.getTimestamp() >= nextRolloverTime) {
        rollover();
    }

    layout->formatAndAppend(out, event);
    if(immediateFlush) {
        out.flush();
    }
}



void
DailyRollingFileAppender::rollover()
{
    // Close the current file
    out.close();
    out.clear(); // reset flags since the C++ standard specified that all the
                 // flags should remain unchanged on a close

    // If we've already rolled over this time period, we'll make sure that we
    // don't overwrite any of those previous files.
    rolloverFiles(scheduledFilename, maxBackupIndex);
    log4cplus::tostringstream backupTarget;
    backupTarget << scheduledFilename << LOG4CPLUS_TEXT('.') << 1;
    if( rename(LOG4CPLUS_TSTRING_TO_STRING(scheduledFilename).c_str(), 
               LOG4CPLUS_TSTRING_TO_STRING(backupTarget.str()).c_str()) == 0 )
    {
        getLogLog().debug(  LOG4CPLUS_TEXT("Renamed file ") 
                          + scheduledFilename 
                          + LOG4CPLUS_TEXT(" to ")
                          + backupTarget.str());
    }
    
    // Rename filename to scheduledFilename
    getLogLog().debug(  LOG4CPLUS_TEXT("Renaming file ") + filename 
                      + LOG4CPLUS_TEXT(" to ") + scheduledFilename);
    rename(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), 
           LOG4CPLUS_TSTRING_TO_STRING(scheduledFilename).c_str());

    // Open a new file
    out.open(LOG4CPLUS_TSTRING_TO_STRING(filename).c_str(), 
             ios::out | ios::trunc);

    // Calculate the next rollover time
    if(Time::gettimeofday() >= nextRolloverTime) {
        scheduledFilename = getFilename(nextRolloverTime);
        nextRolloverTime = calculateNextRolloverTime(nextRolloverTime);
    }
}



log4cplus::helpers::Time
DailyRollingFileAppender::calculateNextRolloverTime(const Time& t) const
{
    switch(schedule) {
    case MONTHLY: 
        {
            struct tm nextMonthTime;
            t.localtime(&nextMonthTime);
            nextMonthTime.tm_mon += 1;
            nextMonthTime.tm_isdst = 0;

            Time ret;
            if(ret.setTime(&nextMonthTime) == -1) {
                getLogLog().error(LOG4CPLUS_TEXT("DailyRollingFileAppender::calculateNextRolloverTime()- setTime() returned error"));
                ret = (t + Time(2678400));
            }

            return ret;
        }

    case WEEKLY:
        return (t + Time(604800)); // 7 * 24 * 60 * 60 seconds

    case DAILY:
        return (t + Time(86400)); //      24 * 60 * 60 seconds

    case TWICE_DAILY:
        return (t + Time(43200)); //      12 * 60 * 60 seconds

    case HOURLY:
        return (t + Time(3600));  //           60 * 60 seconds

    case MINUTELY:
        return (t + Time(60));    //                60 seconds
    };

    getLogLog().error(LOG4CPLUS_TEXT("DailyRollingFileAppender::calculateNextRolloverTime()- invalid schedule value"));
    return (t + Time(86400));
}



log4cplus::tstring
DailyRollingFileAppender::getFilename(const log4cplus::helpers::Time& t) const
{
    tstring pattern;
    switch(schedule) {
    case MONTHLY:
        pattern = LOG4CPLUS_TEXT("%Y-%m");
        break;

    case WEEKLY:
        pattern = LOG4CPLUS_TEXT("%Y-%W");
        break;

    case DAILY:
        pattern = LOG4CPLUS_TEXT("%Y-%m-%d");
        break;

    case TWICE_DAILY:
        pattern = LOG4CPLUS_TEXT("%Y-%m-%d-%p");
        break;

    case HOURLY:
        pattern = LOG4CPLUS_TEXT("%Y-%m-%d-%H");
        break;

    case MINUTELY:
        pattern = LOG4CPLUS_TEXT("%Y-%m-%d-%H-%M");
        break;
    };

    return filename + LOG4CPLUS_TEXT(".") + t.getFormattedTime(pattern, false);
}



syntax highlighted by Code2HTML, v. 0.9.1