org.patientview.monitoring.ImportMonitor.java Source code

Java tutorial

Introduction

Here is the source code for org.patientview.monitoring.ImportMonitor.java

Source

/*
 * PatientView
 *
 * Copyright (c) Worth Solutions Limited 2004-2013
 *
 * This file is part of PatientView.
 *
 * PatientView is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * PatientView is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with PatientView in a file
 * titled COPYING. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package PatientView
 * @link http://www.patientview.org
 * @author PatientView <info@patientview.org>
 * @copyright Copyright (c) 2004-2013, Worth Solutions Limited
 * @license http://www.gnu.org/licenses/gpl-3.0.html The GNU General Public License V3.0
 */

package org.patientview.monitoring;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.util.StopWatch;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * Logs the number of files that XML Import needs to process
 * <p/>
 * Monitors those files to see if the XML Import is stalled or not working properly
 *
 * @author Deniz Ozger
 */
public final class ImportMonitor {

    // Timings and limitations
    private static final int FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES = 1;
    /**
     * Should always be equal to monitoringFrequencyInMinutes /
     * FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES. Thus; it is calculated in runtime.
     */
    private static int numberOfLinesToRead = -1;

    // Import data file format
    private static final String RECORD_DATA_DELIMITER = ",";
    private static final String RECORD_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    private static final int DATE_POSITION_IN_RECORD = 0;
    private static final String COUNT_LOG_FILENAME_FORMAT = "yyyy-MM-dd";
    private static final String COMMENT_PREFIX = "#";

    // Class constants
    private static final String PROJECT_PROPERTIES_FILE = "patientview.properties";
    private static final Logger LOGGER = LoggerFactory.getLogger(ImportMonitor.class);

    private static final int LINE_FEED = 0xA;
    private static final int CARRIAGE_RETURN = 0xD;

    private static final int SECONDS_IN_MINUTE = 60;
    private static final int MILLISECONDS = 1000;

    private static final int HASH_SEED_17 = 17;
    private static final int HASH_SEED_31 = 31;

    private ImportMonitor() {

    }

    public static void main(String[] args) {

        int importFileCheckCount = 0;

        while (true) {
            LOGGER.info("******** Import Logger & Monitor wakes up ********");

            int monitoringFrequencyInMinutes = Integer.parseInt(getProperty("importerMonitor.frequency.minutes"));
            numberOfLinesToRead = monitoringFrequencyInMinutes / FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES;

            LOGGER.info("Import file counts will be logged every {} minutes, whereas a "
                    + "health check will be done every {} minutes. Each monitoring will check the last {} lines "
                    + "of the log",
                    new Object[] { FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES, monitoringFrequencyInMinutes,
                            numberOfLinesToRead });

            StopWatch sw = new StopWatch();
            sw.start();

            importFileCheckCount = importFileCheckCount + FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES;

            /**
             * Get the folders that will be monitored
             */
            List<FolderToMonitor> foldersToMonitor = getFoldersToMonitor();

            /**
             * Count the number of files in these folders
             */
            setTheNumberOfCurrentFilesToFolderObjects(foldersToMonitor);

            /**
             * Log counts to a file
             */
            logNumberOfFiles(foldersToMonitor);

            /**
             * If it is time, check the overall monitor stability as well
             */
            if (importFileCheckCount == numberOfLinesToRead) {
                monitorImportProcess(foldersToMonitor);

                importFileCheckCount = 0;
            } else {
                LOGGER.info("Next monitoring will happen in {} minutes",
                        numberOfLinesToRead - importFileCheckCount);
            }

            sw.stop();
            LOGGER.info("ImportMonitor ends, it took {} (mm:ss)",
                    new SimpleDateFormat("mm:ss").format(sw.getTotalTimeMillis()));

            /**
             * Sleep for (frequency - execution time) seconds
             */
            long maxTimeToSleep = FREQUENCY_OF_LOGGING_IMPORT_FILE_COUNTS_IN_MINUTES * SECONDS_IN_MINUTE
                    * MILLISECONDS;
            long executionTime = sw.getTotalTimeMillis();
            long timeToSleep = maxTimeToSleep - executionTime;

            // if execution time is more than max time to sleep, then sleep for the max time
            if (timeToSleep < 0) {
                timeToSleep = maxTimeToSleep;
            }

            LOGGER.info("ImportMonitor will now sleep for {} (mm:ss)",
                    new SimpleDateFormat("mm:ss").format(timeToSleep));

            try {
                Thread.sleep(timeToSleep);
            } catch (InterruptedException e) {
                LOGGER.error("Import Monitor could not sleep: ", e); // possible insomnia
                System.exit(0);
            }
        }
    }

    /**
     * Counts the number of files in folders and sets those values to folder objects
     */
    private static void setTheNumberOfCurrentFilesToFolderObjects(List<FolderToMonitor> foldersToMonitor) {
        for (FolderToMonitor folderToMonitor : foldersToMonitor) {
            folderToMonitor
                    .setCurrentNumberOfFiles(getNumberOfFilesInDirectory(folderToMonitor.getPathIncludingName()));
        }
    }

    private static void monitorImportProcess(List<FolderToMonitor> foldersToMonitor) {
        /**
         * Read some lines from the file
         */

        List<String> lines = getLastNLinesOfFile(numberOfLinesToRead);

        /**
         * Make sure all lines have the same number of folders, and that matches folders to monitor now
         */
        if (doNumberOfFoldersInLogFileAndPropertiesFileMatch(lines, foldersToMonitor)) {
            /**
             * Convert them to meaningful objects
             */
            List<CountRecord> countRecords = getCountRecordsFromLines(lines, foldersToMonitor);

            /**
             * Check the records to see if files are static or they exceed the limit
             */
            if (areThereEnoughDataToMonitor(countRecords)) {

                List<FolderToMonitor> foldersThatHaveStaticFiles = getFoldersWhoseNumberOfFilesAreStatic(
                        countRecords);
                List<FolderToMonitor> foldersWhoseNumberOfFilesExceedTheirLimits = getFoldersWhoseNumberOfFilesExceedTheirLimits(
                        countRecords);

                if (foldersThatHaveStaticFiles.size() > 0
                        || foldersWhoseNumberOfFilesExceedTheirLimits.size() > 0) {
                    /**
                     * Send an email if there are problems with the importer
                     */
                    sendAWarningEmail(foldersThatHaveStaticFiles, foldersWhoseNumberOfFilesExceedTheirLimits,
                            countRecords);
                } else {
                    LOGGER.info("Importer appears to be working fine.");
                }
            }
        } else {
            LOGGER.error("Skipping monitoring folders as number of folders in log file and number of folders "
                    + "defined in properties file do not match.");
        }
    }

    private static boolean doNumberOfFoldersInLogFileAndPropertiesFileMatch(List<String> logLines,
            List<FolderToMonitor> foldersToMonitor) {
        boolean folderSizeInLinesMatch;
        int lastFolderSizeInLine = -1;

        /**
         * Check if lines in log file have the same number of folders
         */
        if (logLines.size() > 0) {
            lastFolderSizeInLine = getNumberOfFoldersInLogLine(logLines.get(0));
        }

        for (String line : logLines) {
            if (StringUtils.isBlank(line)) {
                LOGGER.info("Empty record is encountered, this might be the first line");
            } else {
                int currentNumberOfFoldersInThisLogLine = getNumberOfFoldersInLogLine(line);

                folderSizeInLinesMatch = lastFolderSizeInLine == currentNumberOfFoldersInThisLogLine;

                if (!folderSizeInLinesMatch) {
                    LOGGER.warn("Folders in properties file and log file do not match. If some folders were "
                            + "added/removed recently, then this may be the reason. Folder count of previous line "
                            + " is {}, whereas another line's count is {} ({})",
                            new Object[] { currentNumberOfFoldersInThisLogLine, lastFolderSizeInLine, line });

                    return false;
                } else {
                    lastFolderSizeInLine = getNumberOfFoldersInLogLine(line);
                }
            }
        }

        /**
         * Compare the folder count in log lines with the count of folders that we will monitor now
         */
        return foldersToMonitor.size() == lastFolderSizeInLine;
    }

    /**
     * Returns the number of folders defined in a line of log.
     * <p/>
     * If log file looks like 1985-07-03,1,2,3 then it returns 3
     */
    private static int getNumberOfFoldersInLogLine(String line) {
        String[] recordDataSections = line.split(RECORD_DATA_DELIMITER);

        if (recordDataSections != null) {
            // first part is date, and the last part is comment sections, so subtract 2 from the total number
            return recordDataSections.length - 2;
        } else {
            return 0;
        }
    }

    /**
     * Lists the folders to be monitored, which are defined in properties files
     */
    private static List<FolderToMonitor> getFoldersToMonitor() {
        List<FolderToMonitor> foldersToMonitor = new ArrayList<FolderToMonitor>();

        List<String> propertyNames = getPropertyNamesStartingWith("importerMonitor.directory.path");

        FolderToMonitor folderToMonitor;
        for (String propertyName : propertyNames) {
            folderToMonitor = new FolderToMonitor();

            String[] propertyNameParts = propertyName.split("\\.");

            if (propertyNameParts != null && propertyNameParts.length > 0) {
                try {
                    folderToMonitor.setId(Integer.parseInt(propertyNameParts[propertyNameParts.length - 1]));
                    folderToMonitor.setPathIncludingName(getProperty(propertyName));
                    folderToMonitor.setMaxNumberOfFiles(Integer.parseInt(
                            getProperty("importerMonitor.directory.maxNumberOfFiles." + folderToMonitor.getId())));
                } catch (NumberFormatException e) {
                    LOGGER.error("Could not retrieve property value {}, possible faulty property name definition",
                            propertyName, e);
                }

                if (!foldersToMonitor.contains(folderToMonitor)) {
                    foldersToMonitor.add(folderToMonitor);
                }
            }
        }

        Collections.sort(foldersToMonitor, FolderToMonitor.Order.ById.descending());

        return foldersToMonitor;
    }

    /**
     * Appends the current time and file counts to importer data file
     */
    private static void logNumberOfFiles(List<FolderToMonitor> foldersToMonitor) {
        File countFile = getTodaysCountFile();

        try {
            FileOutputStream fileOutStream = new FileOutputStream(countFile, true); // append data

            String countRecord = getCountDataToWriteToFile(foldersToMonitor);

            fileOutStream.write(countRecord.getBytes());
            LOGGER.info("Appended this line: \"{}\" to count file {}", countRecord, countFile.getAbsolutePath());

            fileOutStream.flush();
            fileOutStream.close();
        } catch (IOException e) {
            LOGGER.error("Could not persist number of files in folders", e);
        }
    }

    /**
     * Returns the log file that will be used today - there is a rotation in log files
     */
    private static File getTodaysCountFile() {
        SimpleDateFormat dateTimeFormat = new SimpleDateFormat(COUNT_LOG_FILENAME_FORMAT);

        String fileName = getProperty("importer.data.file.name") + dateTimeFormat.format(new Date());

        return new File(getProperty("importer.data.file.directory.path") + "/" + fileName);
    }

    /**
     * Returns the record that needs to be appended to importer data file
     */
    private static String getCountDataToWriteToFile(List<FolderToMonitor> foldersToMonitor) {
        SimpleDateFormat dateTimeFormat = new SimpleDateFormat(RECORD_DATE_FORMAT);

        String countData = "\n" + dateTimeFormat.format(new Date());

        /**
         * Append counts
         */
        for (FolderToMonitor folderToMonitor : foldersToMonitor) {
            countData = countData + RECORD_DATA_DELIMITER + folderToMonitor.getCurrentNumberOfFiles();
        }

        /**
         * Append folder names for reference
         */
        countData = countData + "," + COMMENT_PREFIX + "Folders:";
        for (FolderToMonitor folderToMonitor : foldersToMonitor) {
            countData = countData + folderToMonitor.getName() + "-";
        }

        return countData;
    }

    /**
     * Returns number of files in a directory, excluding hidden files of unix environment and directories
     */
    private static int getNumberOfFilesInDirectory(String directoryPath) {
        File[] files = new File(directoryPath).listFiles();

        int count = 0;
        if (files != null) {
            for (File file : files) {
                if (file.isFile() && !file.getName().startsWith(".")) { // no no no, we don't want any hidden files
                    count++;
                }
            }
        }

        LOGGER.info("Number of files in directory {} is {}", directoryPath, count);

        return count;
    }

    /**
     * Checks if there is enough data (file counts) in log file for monitoring
     */
    private static boolean areThereEnoughDataToMonitor(List<CountRecord> countRecords) {
        if (countRecords.size() < numberOfLinesToRead) {
            LOGGER.info("There are not enough data (only {} lines) to monitor. There should be at least {} lines "
                    + "for Import monitor to process.", countRecords.size(), numberOfLinesToRead);

            return false;
        }

        return true;
    }

    /**
     * Checks if the number of files in given directories are static over a certain period of time
     */
    private static List<FolderToMonitor> getFoldersWhoseNumberOfFilesAreStatic(List<CountRecord> countRecords) {
        List<FolderToMonitor> foldersWhoseFilesAreStatic = new ArrayList<FolderToMonitor>();

        /**
         * First, treat all folders with files as static
         */
        for (CountRecord countRecord : countRecords) {
            for (FolderToMonitor folderToMonitor : countRecord.getFoldersToMonitor()) {
                if (folderToMonitor.getCurrentNumberOfFiles() > 0
                        && !foldersWhoseFilesAreStatic.contains(folderToMonitor)) {
                    foldersWhoseFilesAreStatic.add(folderToMonitor);
                }
            }
        }

        /**
         * What we have in log files is a matrix (x,y) where x denotes folders and y denotes number of files in
         *      that folder. It is advised that you see the log file before continuing.. The data is stored in a
         *      primitive file instead of a relational database, hence the long explanations..
         *
         * We will first compare (x, y), (x, y+1), (x, y+2) to see if files in a folder was static over time.
         * Then we will check the rest of the folders, starting by comparing (x+1, y), (x+1, y+1), (x+1, y+2) ...
         */

        // first line of the log
        CountRecord firstLogRecord = countRecords.get(0);

        // total number of folders that are logged in his line
        int numberOfFoldersToCheck = firstLogRecord.getFoldersToMonitor().size();

        /**
         * We will make iterations totaling to the number of folders that needs to be monitored
         */
        for (int i = 0; i < numberOfFoldersToCheck; i++) {

            /**
             * i-th folder of the first log record. We will compare this value with other log records (other
             *      recordings recently)
             */
            int firstRecordsFolderFileCount = firstLogRecord.getFoldersToMonitor().get(i).getCurrentNumberOfFiles();

            /**
             * Go backwards in time (logs) to see if the file count was always the same or not
             */
            for (CountRecord countRecord : countRecords) {

                // i-th folder of the current log record
                FolderToMonitor thisRecordsFolder = countRecord.getFoldersToMonitor().get(i);

                // number of files of i-th folder of the current log record
                int thisRecordsFolderFileCount = thisRecordsFolder.getCurrentNumberOfFiles();

                /**
                 * If this log record's i-th folder's file count is different than the first log record's
                 *      i-th folder's file count, then it means importer is working on this folder
                 */
                if (firstRecordsFolderFileCount != thisRecordsFolderFileCount) {
                    foldersWhoseFilesAreStatic.remove(thisRecordsFolder);
                }
            }
        }

        /**
         * Log the findings
         */
        for (FolderToMonitor monitoredFolder : foldersWhoseFilesAreStatic) {
            LOGGER.info("Files are static in directory {}", monitoredFolder.getPathIncludingName());
        }

        return foldersWhoseFilesAreStatic;
    }

    /**
     * Checks if the number of pending files for importer exceeds a given limit
     */
    private static List<FolderToMonitor> getFoldersWhoseNumberOfFilesExceedTheirLimits(
            List<CountRecord> countRecords) {
        List<FolderToMonitor> foldersWhoseFilesExceedTheirLimits = new ArrayList<FolderToMonitor>();

        if (countRecords.size() > 0) {
            List<CountRecord> countRecordsToTest = new ArrayList<CountRecord>(countRecords);

            Collections.sort(countRecordsToTest, CountRecord.Order.ByRecordTime.descending());
            CountRecord firstCountRecord = countRecords.get(0);

            int numberOfFoldersToCheck = firstCountRecord.getFoldersToMonitor().size();

            for (int i = 0; i < numberOfFoldersToCheck; i++) {

                for (CountRecord countRecord : countRecords) {

                    FolderToMonitor thisRecordsFolder = countRecord.getFoldersToMonitor().get(i);

                    if (thisRecordsFolder.getCurrentNumberOfFiles() > thisRecordsFolder.getMaxNumberOfFiles()
                            && !foldersWhoseFilesExceedTheirLimits.contains(thisRecordsFolder)) {
                        LOGGER.info("Folder {}'s files ({}) exceed its limit ({})",
                                new Object[] { thisRecordsFolder.getId(),
                                        thisRecordsFolder.getCurrentNumberOfFiles(),
                                        thisRecordsFolder.getMaxNumberOfFiles() });

                        foldersWhoseFilesExceedTheirLimits.add(thisRecordsFolder);
                    }
                }
            }
        }

        return foldersWhoseFilesExceedTheirLimits;
    }

    private static void sendAWarningEmail(List<FolderToMonitor> foldersThatHaveStaticFiles,
            List<FolderToMonitor> foldersWhoseNumberOfFilesExceedTheirLimits, List<CountRecord> countRecords) {
        try {
            Resource resource = new ClassPathResource("/" + PROJECT_PROPERTIES_FILE);
            Properties props = PropertiesLoaderUtils.loadProperties(resource);

            String subject = "Problems encountered in Patient View XML Importer";

            String body = getWarningEmailBody(foldersThatHaveStaticFiles,
                    foldersWhoseNumberOfFilesExceedTheirLimits, countRecords);

            String fromAddress = props.getProperty("noreply.email");

            // todo For testing purposes this property is overridden
            //            String[] toAddresses = {props.getProperty("support.email")};
            String[] toAddresses = { "patientview-testing@solidstategroup.com" };

            sendEmail(fromAddress, toAddresses, null, subject, body);
        } catch (IOException e) {
            LOGGER.error("Could not find properties file: {}", e);
        }
    }

    public static void sendEmail(String from, String[] to, String[] bcc, String subject, String body) {
        if (StringUtils.isBlank(from)) {
            throw new IllegalArgumentException("Cannot send mail missing 'from'");
        }

        if ((to == null || to.length == 0) && (bcc == null || bcc.length == 0)) {
            throw new IllegalArgumentException("Cannot send mail missing recipients");
        }

        if (StringUtils.isBlank(subject)) {
            throw new IllegalArgumentException("Cannot send mail missing 'subject'");
        }

        if (StringUtils.isBlank(body)) {
            throw new IllegalArgumentException("Cannot send mail missing 'body'");
        }

        ApplicationContext context = new ClassPathXmlApplicationContext(
                new String[] { "classpath*:context-standalone.xml" });

        JavaMailSender javaMailSender = (JavaMailSender) context.getBean("javaMailSender");

        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper;

        try {
            messageHelper = new MimeMessageHelper(message, true);
            messageHelper.setTo(to);
            if (bcc != null && bcc.length > 0) {
                Address[] bccAddresses = new Address[bcc.length];
                for (int i = 0; i < bcc.length; i++) {
                    bccAddresses[i] = new InternetAddress(bcc[i]);
                }
                message.addRecipients(Message.RecipientType.BCC, bccAddresses);
            }
            messageHelper.setFrom(from);
            messageHelper.setSubject(subject);
            messageHelper.setText(body, false); // Note: the second param indicates to send plaintext

            javaMailSender.send(messageHelper.getMimeMessage());

            LOGGER.info("Sent an email about Importer issues. From: {} To: {}", from, Arrays.toString(to));
        } catch (Exception e) {
            LOGGER.error("Could not send email: {}", e);
        }
    }

    private static String getWarningEmailBody(List<FolderToMonitor> foldersThatHaveStaticFiles,
            List<FolderToMonitor> foldersWhoseNumberOfFilesExceedTheirLimits, List<CountRecord> countRecords) {
        String emailBody = "";
        String newLine = System.getProperty("line.separator");

        emailBody += "[This is an automated email from Renal PatientView - do not reply to this email]";
        emailBody += newLine;
        emailBody += newLine + "There are some problems in XML Importer. Please see below for details.";
        emailBody += newLine;

        if (foldersThatHaveStaticFiles.size() > 0) {
            emailBody += newLine
                    + "Importer has not imported any files in some folders recently. These folders are:";
            emailBody += newLine;

            for (FolderToMonitor folder : foldersThatHaveStaticFiles) {
                emailBody += newLine + "Folder ID: " + folder.getId() + " Path: " + folder.getPathIncludingName()
                        + " Number of files: " + folder.getCurrentNumberOfFiles();
            }

            emailBody += newLine;
            emailBody += newLine;
        }

        if (foldersWhoseNumberOfFilesExceedTheirLimits.size() > 0) {
            emailBody += newLine + "Files are in some folders are above the threshold. These folders are:";
            emailBody += newLine;

            for (FolderToMonitor folder : foldersWhoseNumberOfFilesExceedTheirLimits) {
                emailBody += newLine + "Folder ID: " + folder.getId() + " Path: " + folder.getPathIncludingName()
                        + " Number of files: " + folder.getCurrentNumberOfFiles() + " Threshold: "
                        + folder.getMaxNumberOfFiles();
            }
        }

        emailBody += newLine;
        emailBody += newLine;
        emailBody += newLine + "Please see the following most recent file count records for reference:";
        emailBody += newLine;

        List<CountRecord> countRecordsToSend = new ArrayList<CountRecord>(countRecords);
        Collections.sort(countRecordsToSend, CountRecord.Order.ByRecordTime.descending());

        for (CountRecord countRecord : countRecordsToSend) {
            emailBody += countRecord;
        }

        return emailBody;
    }

    /**
     * Returns the last N lines of a file. Assumes lines are terminated by |n ascii character
     */
    private static List<String> getLastNLinesOfFile(int numberOfLinesToReturn) {
        List<String> lastNLines = new ArrayList<String>();
        java.io.RandomAccessFile fileHandler = null;

        try {
            File file = getTodaysCountFile();

            fileHandler = new java.io.RandomAccessFile(file, "r");

            long totalNumberOfCharactersInFile = file.length() - 1;

            StringBuilder sb = new StringBuilder();
            int numberOfLinesRead = 0;

            /**
             * loop through characters in file, construct lines out of characters, add lines to a list
             */
            for (long currentCharacter = totalNumberOfCharactersInFile; currentCharacter != -1; currentCharacter--) {
                fileHandler.seek(currentCharacter);

                int readByte = fileHandler.readByte();

                if (readByte == LINE_FEED || readByte == CARRIAGE_RETURN) {
                    if (numberOfLinesRead == numberOfLinesToReturn) {
                        break;
                    }

                    numberOfLinesRead++;

                    /**
                     * add line to line list
                     */
                    String currentLine = sb.reverse().toString();
                    sb = new StringBuilder();

                    if (StringUtils.isNotBlank(currentLine)) {
                        lastNLines.add(currentLine);
                    } else {
                        LOGGER.error("Read line does not contain any data");
                        continue;
                    }
                } else {
                    sb.append((char) readByte);
                }
            }

            /**
             * add the last line
             */
            lastNLines.add(sb.reverse().toString());
        } catch (Exception e) {
            LOGGER.error("Can not find today's file", e);
        } finally {
            if (fileHandler != null) {
                try {
                    fileHandler.close();
                } catch (IOException e) {
                    fileHandler = null;
                }
            }
        }

        return lastNLines;
    }

    private static String getProperty(String propertyName) {
        Resource resource = new ClassPathResource("/" + PROJECT_PROPERTIES_FILE);
        Properties props = null;
        String propertyValue = "";

        try {
            props = PropertiesLoaderUtils.loadProperties(resource);

            propertyValue = props.getProperty(propertyName);
        } catch (IOException e) {
            LOGGER.error("Could not find properties file: {}", e);
        }

        return propertyValue;
    }

    /**
     * Returns all property names that starts with the given string
     *
     * @return empty array list if no property name is found
     */
    private static List<String> getPropertyNamesStartingWith(String queriedProperyName) {
        Resource resource = new ClassPathResource("/" + PROJECT_PROPERTIES_FILE);
        Properties props = null;
        Set<String> allPropertyNames;
        List<String> propertyNames = new ArrayList<String>();

        try {
            props = PropertiesLoaderUtils.loadProperties(resource);

            allPropertyNames = props.stringPropertyNames();

            for (String propertyName : allPropertyNames) {
                if (propertyName.startsWith(queriedProperyName)) {
                    propertyNames.add(propertyName);
                }
            }
        } catch (IOException e) {
            LOGGER.error("Could not find properties file: {}", e);
        }

        return propertyNames;
    }

    /**
     * Convert lines on the file into sensible objects
     */
    private static List<CountRecord> getCountRecordsFromLines(List<String> lines,
            List<FolderToMonitor> foldersToMonitor) {
        List<CountRecord> countRecords = new ArrayList<CountRecord>();

        for (String line : lines) {
            String date = extractDateAsString(line);

            List<Integer> numberOfFilesInDirectories = extractNumberOfFilesInDirectories(line);

            if (isValidCountRecord(date, RECORD_DATE_FORMAT, numberOfFilesInDirectories)) {

                countRecords.add(CountRecord.fromDateAndFileCounts(date, RECORD_DATE_FORMAT,
                        numberOfFilesInDirectories, foldersToMonitor));
            } else {
                LOGGER.error("Invalid record: {}", line);
            }
        }

        return countRecords;
    }

    /**
     * Check if the parameters are in correct formats
     */
    private static boolean isValidCountRecord(String dateString, String dateFormant,
            List<Integer> numberOfFilesInDirectories) {
        return numberOfFilesInDirectories != null && numberOfFilesInDirectories.size() > 0
                && parseDate(dateString, dateFormant) != null;
    }

    private static Date parseDate(String maybeDate, String format) {
        Date date = null;

        try {
            DateTimeFormatter fmt = DateTimeFormat.forPattern(format);
            DateTime dateTime = fmt.parseDateTime(maybeDate);
            date = dateTime.toDate();
        } catch (Exception e) {
            LOGGER.error("Invalid date: {}", maybeDate, e);
        }

        return date;
    }

    private static String extractDateAsString(String line) {
        return extractRecordDataOnThisPosition(line, DATE_POSITION_IN_RECORD);
    }

    private static List<Integer> extractNumberOfFilesInDirectories(String line) {
        List<Integer> numberOfFilesList = new ArrayList<Integer>();

        if (StringUtils.isBlank(line)) {
            LOGGER.info("Empty record is encountered, this might be the first line");
        } else {
            String[] recordDataSections = line.split(RECORD_DATA_DELIMITER);

            for (int i = DATE_POSITION_IN_RECORD + 1; recordDataSections.length > i; i++) {
                try {
                    if (!recordDataSections[i].startsWith(COMMENT_PREFIX)) {
                        numberOfFilesList.add(Integer.parseInt(recordDataSections[i]));
                    }
                } catch (NumberFormatException e) {
                    LOGGER.error("Could not parse file count {} into an integer. The log line is: {}",
                            new Object[] { recordDataSections[i], line }, e);
                    break;
                }
            }
        }

        return numberOfFilesList;
    }

    private static String extractRecordDataOnThisPosition(String line, int position) {
        String[] recordDataSections = line.split(RECORD_DATA_DELIMITER);

        if (recordDataSections != null && recordDataSections.length > position) {
            return recordDataSections[position];
        } else {
            return null;
        }
    }

    private static class FolderToMonitor {

        private int id;
        private String pathIncludingName;
        private int maxNumberOfFiles;
        private int currentNumberOfFiles;

        public String getPathIncludingName() {
            return pathIncludingName;
        }

        public void setPathIncludingName(String pathIncludingName) {
            this.pathIncludingName = pathIncludingName;
        }

        public int getMaxNumberOfFiles() {
            return maxNumberOfFiles;
        }

        public void setMaxNumberOfFiles(int maxNumberOfFiles) {
            this.maxNumberOfFiles = maxNumberOfFiles;
        }

        public String getName() {
            if (StringUtils.isNotBlank(pathIncludingName)) {
                String[] pathParts = pathIncludingName.split("\\/");

                if (pathParts != null && pathParts.length > 0) {
                    return pathParts[pathParts.length - 1];
                } else {
                    LOGGER.error("Could not retrieve folder name from path {}");
                }
            }

            return pathIncludingName;
        }

        public int getCurrentNumberOfFiles() {
            return currentNumberOfFiles;
        }

        public void setCurrentNumberOfFiles(int currentNumberOfFiles) {
            this.currentNumberOfFiles = currentNumberOfFiles;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public static enum Order implements Comparator<FolderToMonitor> {
            ById() {
                public int compare(FolderToMonitor leftRecord, FolderToMonitor rightRecord) {
                    return Double.compare(leftRecord.getId(), rightRecord.getId()) * -1;
                }
            };

            public abstract int compare(FolderToMonitor leftRecord, FolderToMonitor rightRecord);

            public Comparator ascending() {
                return this;
            }

            public Comparator descending() {
                return Collections.reverseOrder(this);
            }
        }

        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof FolderToMonitor)) {
                return false;
            }

            FolderToMonitor rhs = (FolderToMonitor) obj;
            return new EqualsBuilder().append(pathIncludingName, rhs.getPathIncludingName()).isEquals();
        }

        public int hashCode() {
            return new HashCodeBuilder(HASH_SEED_17, HASH_SEED_31).append(pathIncludingName)
                    .append(maxNumberOfFiles).toHashCode();
        }
    }

    private static final class CountRecord {
        private Date recordTime = null;
        private List<FolderToMonitor> foldersToMonitor;

        private CountRecord() {

        }

        public static CountRecord fromDateAndFileCounts(String dateString, String dateFormat,
                List<Integer> currentFileCountsInFolders, List<FolderToMonitor> foldersInPropertiesFileToMonitor) {
            CountRecord countRecord = new CountRecord();
            countRecord.setRecordTime(parseDate(dateString, dateFormat));

            List<FolderToMonitor> foldersInLogFileToMonitor = new ArrayList<FolderToMonitor>();

            for (int i = 0; i < currentFileCountsInFolders.size()
                    && i < foldersInPropertiesFileToMonitor.size(); i++) {

                FolderToMonitor folderToMonitorInPropertiesFile = foldersInPropertiesFileToMonitor.get(i);

                FolderToMonitor folderToMonitor = new FolderToMonitor();
                folderToMonitor.setId(folderToMonitorInPropertiesFile.getId());
                folderToMonitor.setPathIncludingName(folderToMonitorInPropertiesFile.getPathIncludingName());
                folderToMonitor.setMaxNumberOfFiles(folderToMonitorInPropertiesFile.getMaxNumberOfFiles());
                folderToMonitor.setCurrentNumberOfFiles(currentFileCountsInFolders.get(i));

                foldersInLogFileToMonitor.add(folderToMonitor);
            }

            countRecord.setFoldersToMonitor(foldersInLogFileToMonitor);

            return countRecord;
        }

        public static enum Order implements Comparator<CountRecord> {
            ByRecordTime() {
                public int compare(CountRecord leftRecord, CountRecord rightRecord) {
                    return leftRecord.getRecordTime().compareTo(rightRecord.getRecordTime()) * -1;
                }
            };

            public abstract int compare(CountRecord leftRecord, CountRecord rightRecord);

            public Comparator ascending() {
                return this;
            }

            public Comparator descending() {
                return Collections.reverseOrder(this);
            }
        }

        public Date getRecordTime() {
            return recordTime;
        }

        public void setRecordTime(Date recordTime) {
            this.recordTime = recordTime;
        }

        public List<FolderToMonitor> getFoldersToMonitor() {
            return foldersToMonitor;
        }

        public void setFoldersToMonitor(List<FolderToMonitor> foldersToMonitor) {
            this.foldersToMonitor = foldersToMonitor;
        }

        public String toString() {
            SimpleDateFormat dateTimeFormat = new SimpleDateFormat(RECORD_DATE_FORMAT);
            String countRecordAsString = "\n" + dateTimeFormat.format(recordTime);

            for (FolderToMonitor folderToMonitor : foldersToMonitor) {
                countRecordAsString = countRecordAsString + RECORD_DATA_DELIMITER
                        + folderToMonitor.getCurrentNumberOfFiles();
            }

            countRecordAsString = countRecordAsString + "," + COMMENT_PREFIX + "Folders:";
            for (FolderToMonitor folderToMonitor : foldersToMonitor) {
                countRecordAsString = countRecordAsString + folderToMonitor.getName() + "-";
            }

            return countRecordAsString;
        }
    }
}