org.alexlg.bankit.services.SyncService.java Source code

Java tutorial

Introduction

Here is the source code for org.alexlg.bankit.services.SyncService.java

Source

/*
 * Copyright (C) 2012-2013 Alexandre Thomazo
 *
 * This file is part of BankIt.
 *
 * BankIt 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.
 *
 * BankIt 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 BankIt. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alexlg.bankit.services;

import org.alexlg.bankit.dao.CostDao;
import org.alexlg.bankit.dao.OperationDao;
import org.alexlg.bankit.db.Cost;
import org.alexlg.bankit.db.Operation;
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Service used for synchronization between costs, operation
 * and imported operations.
 * 
 * @author Alexandre Thomazo
 */
@Controller
public class SyncService {

    /** 
     * Name of the option which stores the last execution date 
     * of materializeCostsIntoOperation.
     */
    public static final String COST_SYNC_OPT = "costSync";

    /**
     * Name of the option which stores the last execution date
     * of readQifAndInsertOp.
     */
    public static final String OP_SYNC_OPT = "opSync";

    @Autowired
    private CostDao costDao;

    @Autowired
    private OperationDao operationDao;

    @Autowired
    private OptionsService optionsService;

    /** Class logger */
    private static Logger logger = LoggerFactory.getLogger(SyncService.class);

    /** Date used for testing */
    private LocalDate endSyncDate = null;

    /**
     * This function take all the costs between
     * the last execution to create the associated
     * operations in the operation list.
     * It runs every day at midnight and 5 seconds
     */
    @Transactional
    @Scheduled(cron = "5 0 0 * * *")
    public void materializeCostsIntoOperation() {
        logger.info("Starting materializeCostsIntoOperation");

        //materialize operations 2 days beyond current date
        LocalDate endDate = getEndSyncDate().plusDays(2);

        Date startSync = optionsService.getDate(COST_SYNC_OPT);
        if (startSync == null) {
            optionsService.set(COST_SYNC_OPT, endDate.toDate());
            return;
        }
        LocalDate startDate = new LocalDate(startSync);

        //retrieve costs list
        List<Cost> costs = null;
        int nbMonth = 1;

        if (endDate.getYear() == startDate.getYear() && endDate.getMonthOfYear() == startDate.getMonthOfYear()) {
            //only retrieve the costs between this 2 "days"
            costs = costDao.getList(startDate.getDayOfMonth(), endDate.getDayOfMonth());
        } else {
            //getting all costs
            costs = costDao.getList();

            //we generate a least for the current month (as nbMonth = 1)
            //then we add whole months between start and end.
            nbMonth += Months.monthsBetween(startDate, endDate).getMonths();

            //as monthsBetween calculate for whole month, if start is for example the 24-09
            //and the end is the 02-10, monthsBetween will return 0 but we need to have
            //2 loops, one for september and one for november so we add a month.
            if (endDate.getDayOfMonth() <= startDate.getDayOfMonth()) {
                nbMonth++;
            }
        }

        //going through each month and each cost to create the operation
        for (int m = 0; m < nbMonth; m++) {
            LocalDate curMonth = startDate.plusMonths(m);
            int lastDayOfMonth = curMonth.dayOfMonth().getMaximumValue();

            for (Cost cost : costs) {
                int costDay = cost.getDay();

                //if the operation is planned after the last day of month
                //set it to the last day
                if (costDay > lastDayOfMonth)
                    costDay = lastDayOfMonth;

                //creating operation date
                LocalDate opDate = new LocalDate(curMonth.getYear(), curMonth.getMonthOfYear(), costDay);

                //check if date is in the date interval before creating op
                if (opDate.isAfter(startDate) && opDate.compareTo(endDate) <= 0) {
                    Operation op = new Operation();
                    op.setOperationDate(opDate.toDate());
                    op.setLabel(cost.getLabel());
                    op.setPlanned(cost.getAmount());
                    op.setCategory(cost.getCategory());
                    operationDao.save(op);
                }
            }
        }

        optionsService.set(COST_SYNC_OPT, endDate.toDate());

    }

    /**
     * Try to merge all old planned operation with no amount
     * with actual operations. It will try to match on the planned
     * amount then the label.
     */
    public void mergeOldPlannedOps() {
        //retrieve all old planned ops
        List<Operation> oldOps = operationDao.getOldPlannedOps(getEndSyncDate());

        for (Operation oldOp : oldOps) {
            //checking if a matching operation exists
            Operation matchedOp = operationDao.matchRealOp(oldOp);

            if (matchedOp != null) {
                matchedOp.setPlanned(oldOp.getPlanned());
                matchedOp.setCategory(oldOp.getCategory());
                operationDao.delete(oldOp);
            }
        }
    }

    /**
     * Read an inputstream with QIF data and create the
     * corresponding operations in the database if after OP_SYNC_OPT date.
     * @param qif QIF to read
     * @return List of operations id inserted
     * @throws IOException If the reading of the file failed
     */
    public List<Integer> readQifAndInsertOp(InputStream qif) throws IOException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yy");

        BufferedReader reader = new BufferedReader(new InputStreamReader(qif, "cp1252"));
        String line = null; //current line in file read
        Operation curOp = new Operation(); //current operation to fill in
        int lineNum = 0; //current line number of the file
        List<Operation> ops = new LinkedList<Operation>();

        while ((line = reader.readLine()) != null) {
            lineNum++;
            //ignore empty line
            if (line.length() == 0)
                continue;

            //extract first char
            String firstChar = line.substring(0, 1);
            line = line.substring(1, line.length());

            if (firstChar.equals("D")) {
                try {
                    Date date = dateFormat.parse(line);
                    curOp.setOperationDate(date);
                } catch (ParseException e) {
                    throw new IOException("Invalid date [" + line + "] on line " + lineNum, e);
                }

            } else if (firstChar.equals("T")) {
                String number = line.replace(",", "");
                try {
                    BigDecimal amount = new BigDecimal(number);
                    curOp.setAmount(amount);
                } catch (NumberFormatException e) {
                    throw new IOException("Invalid number [" + number + "] on line " + lineNum, e);
                }

            } else if (firstChar.equals("P") || firstChar.equals("M")) {
                //remove all double space
                String label = line.replaceAll("( )+", " ").trim();
                //truncate label if needed
                if (label.length() > 100)
                    label = label.substring(0, 100);

                if (curOp.getLabel() == null || label.length() > curOp.getLabel().length()) {
                    curOp.setLabel(label);
                }

            } else if (firstChar.equals("^")) {
                ops.add(curOp);
                curOp = new Operation();
            }
        }

        //syncing all read operations
        syncOpList(ops);

        //getting ids
        List<Integer> ids = new LinkedList<Integer>();
        for (Operation op : ops) {
            int opId = op.getOperationId();
            if (opId != 0) {
                ids.add(opId);
            }
        }

        return ids;
    }

    /**
     * Sync a list of operation with current ones.
     * @param operations Operations to sync.
     */
    public void syncOpList(List<Operation> operations) {
        if (operations == null)
            return;
        Date start = optionsService.getDate(OP_SYNC_OPT);
        LocalDate startSync = start == null ? null : LocalDate.fromDateFields(start);
        LocalDate maxDate = null; //older operation date

        for (Operation op : operations) {
            LocalDate opDate = LocalDate.fromDateFields(op.getOperationDate());

            if (startSync == null || opDate.isAfter(startSync)) {
                operationDao.insert(op);

                //checking if operation if after maxDate
                if (maxDate == null || opDate.isAfter(maxDate))
                    maxDate = opDate;
            }
        }

        //setting last execution
        if (maxDate != null)
            optionsService.set(OP_SYNC_OPT, maxDate.toDate());
    }

    /**
     * Get the date used for the end of the synchronization,
     * for now the current date. Mostly for mocking in test.
     * @return The date of end sync
     */
    protected LocalDate getEndSyncDate() {
        if (endSyncDate != null)
            return endSyncDate;
        return new LocalDate();
    }

    protected void setEndSyncDate(LocalDate endSyncDate) {
        this.endSyncDate = endSyncDate;
    }

}