com.tysanclan.site.projectewok.util.FinancialTimeline.java Source code

Java tutorial

Introduction

Here is the source code for com.tysanclan.site.projectewok.util.FinancialTimeline.java

Source

/**
 * Tysan Clan Website
 * Copyright (C) 2008-2013 Jeroen Steenbeeke and Ties van de Ven
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.tysanclan.site.projectewok.util;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeMap;

import nl.topicus.wqplot.data.BaseSeries;
import nl.topicus.wqplot.data.DateNumberSeries;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.util.ListModel;
import org.joda.time.DateTime;

import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Maps.EntryTransformer;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.jeroensteenbeeke.hyperion.util.MapUtil;
import com.tysanclan.site.projectewok.entities.Donation;
import com.tysanclan.site.projectewok.entities.Expense;
import com.tysanclan.site.projectewok.entities.PaidExpense;
import com.tysanclan.site.projectewok.entities.Subscription;
import com.tysanclan.site.projectewok.entities.User;
import com.tysanclan.site.projectewok.entities.dao.DonationDAO;
import com.tysanclan.site.projectewok.entities.dao.ExpenseDAO;
import com.tysanclan.site.projectewok.entities.dao.PaidExpenseDAO;
import com.tysanclan.site.projectewok.entities.dao.SubscriptionDAO;
import com.tysanclan.site.projectewok.entities.dao.filters.DonationFilter;
import com.tysanclan.site.projectewok.entities.dao.filters.PaidExpenseFilter;

public class FinancialTimeline {

    private static class SerializableEntry<K, V> implements Serializable, Entry<K, V> {
        private static final long serialVersionUID = 1L;

        private K k;

        private V v;

        public SerializableEntry(K k, V v) {
            super();
            this.k = k;
            this.v = v;
        }

        @Override
        public K getKey() {
            return k;
        }

        @Override
        public V getValue() {

            return v;
        }

        @Override
        public V setValue(V value) {
            // Immutable
            return v;
        }
    }

    private static class EntryCopy implements Function<Entry<DateTime, BigDecimal>, Entry<DateTime, BigDecimal>> {
        public Entry<DateTime, BigDecimal> apply(Entry<DateTime, BigDecimal> input) {
            return new SerializableEntry<DateTime, BigDecimal>(input.getKey(), input.getValue());
        }
    }

    public static class SpendingComparator implements Comparator<Entry<DateTime, BigDecimal>> {

        @Override
        public int compare(Entry<DateTime, BigDecimal> o1, Entry<DateTime, BigDecimal> o2) {
            return o1.getKey().compareTo(o2.getKey());
        }
    }

    public static class ReserveToBigDecimalFunction implements EntryTransformer<User, Reserve, BigDecimal> {
        @Override
        public BigDecimal transformEntry(User key, Reserve value) {
            return value.getAmount();
        }
    }

    public static class SumFunction implements Function<Collection<BigDecimal>, BigDecimal> {
        private static final SumFunction INSTANCE = new SumFunction();

        @Override
        public BigDecimal apply(Collection<BigDecimal> input) {
            BigDecimal result = BigDecimal.ZERO;

            for (BigDecimal next : input) {
                result = result.add(next);
            }

            return result;
        }
    }

    private static class Contribution {
        private final User user;

        private final BigDecimal amount;

        /**
         * @param user
         * @param amount
         */
        public Contribution(User user, BigDecimal amount) {
            super();
            this.user = user;
            this.amount = amount;
        }

        public BigDecimal getAmount() {
            return amount;
        }

        public User getUser() {
            return user;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((amount == null) ? 0 : amount.hashCode());
            result = prime * result + ((user == null) ? 0 : user.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Contribution other = (Contribution) obj;
            if (amount == null) {
                if (other.amount != null)
                    return false;
            } else if (!amount.equals(other.amount))
                return false;
            if (user == null) {
                if (other.user != null)
                    return false;
            } else if (!user.equals(other.user))
                return false;
            return true;
        }

    }

    private static class Reserve implements Comparable<Reserve> {
        private final DateTime time;

        private final User user;

        private final BigDecimal amount;

        /**
         * @param time
         * @param user
         * @param amount
         */
        public Reserve(DateTime time, User user, BigDecimal amount) {
            super();
            this.time = time;
            this.user = user;
            this.amount = amount;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((amount == null) ? 0 : amount.hashCode());
            result = prime * result + ((time == null) ? 0 : time.hashCode());
            result = prime * result + ((user == null) ? 0 : user.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Reserve other = (Reserve) obj;
            if (amount == null) {
                if (other.amount != null)
                    return false;
            } else if (!amount.equals(other.amount))
                return false;
            if (time == null) {
                if (other.time != null)
                    return false;
            } else if (!time.equals(other.time))
                return false;
            if (user == null) {
                if (other.user != null)
                    return false;
            } else if (!user.equals(other.user))
                return false;
            return true;
        }

        public DateTime getTime() {
            return time;
        }

        public User getUser() {
            return user;
        }

        public BigDecimal getAmount() {
            return amount;
        }

        @Override
        public int compareTo(Reserve o) {
            User u1 = getUser();
            User u2 = o.getUser();

            int c1 = getTime().compareTo(o.getTime());
            int c2 = getAmount().compareTo(o.getAmount());
            int c3 = compareUsernames(u1, u2);

            return c1 == 0 ? (c2 == 0 ? c3 : c2) : c1;
        }

        private int compareUsernames(User u1, User u2) {
            if (u1 == null && u2 == null) {
                return 0;
            } else {
                if (u1 == null) {
                    return -1;
                } else if (u2 == null) {
                    return 1;
                } else {
                    return u1.getUsername().compareTo(u2.getUsername());
                }
            }

        }

    }

    private static class CashReserve {

        private Map<DateTime, List<Reserve>> reserves = new TreeMap<DateTime, List<Reserve>>();

        private Map<DateTime, List<BigDecimal>> spendings = new TreeMap<DateTime, List<BigDecimal>>();

        private Map<DateTime, Map<User, BigDecimal>> buffer = new HashMap<DateTime, Map<User, BigDecimal>>();

        public Map<User, BigDecimal> getReservesAt(DateTime time) {
            if (buffer.containsKey(time))
                return buffer.get(time);

            ListMultimap<User, Reserve> result = ArrayListMultimap.create();

            // Add donations as active reserves
            for (Entry<DateTime, List<Reserve>> e : reserves.entrySet()) {
                for (Reserve r : e.getValue()) {
                    if (!e.getKey().isAfter(time)) {
                        result.put(r.getUser(), r);
                    }
                }
            }

            SortedSet<Entry<DateTime, BigDecimal>> entries = Sets.newTreeSet(new SpendingComparator());

            Map<DateTime, BigDecimal> transformed = Maps.transformEntries(spendings,
                    new EntryTransformer<DateTime, List<BigDecimal>, BigDecimal>() {
                        private SumFunction function = new SumFunction();

                        @Override
                        public BigDecimal transformEntry(DateTime key, List<BigDecimal> value) {
                            return function.apply(value);
                        }
                    });

            entries.addAll(ImmutableList.copyOf(Iterables.transform(transformed.entrySet(), new EntryCopy())));

            // Loop through actual expenses and subtract from existing reserves
            spendings: for (Entry<DateTime, BigDecimal> e : entries) {
                if (!e.getKey().isAfter(time)) {
                    BigDecimal open = e.getValue();

                    while (open.compareTo(BigDecimal.ZERO) > 0 && !result.isEmpty()) {
                        Entry<User, Reserve> first = null;
                        boolean found = false;

                        Iterator<Entry<User, Reserve>> iterator = result.entries().iterator();
                        while (iterator.hasNext()) {
                            first = iterator.next();

                            if (!first.getValue().getTime().isAfter(e.getKey())) {
                                found = true;
                                break;
                            }
                        }

                        if (!found) {
                            continue spendings;
                        }

                        result.remove(first.getKey(), first.getValue());

                        Reserve reserve = first.getValue();

                        BigDecimal amount = reserve.getAmount();

                        if (open.compareTo(amount) > 0) {
                            open = open.subtract(amount);
                        } else {
                            BigDecimal remainder = amount.subtract(open);
                            result.put(first.getKey(),
                                    new Reserve(reserve.getTime(), reserve.getUser(), remainder));
                            open = BigDecimal.ZERO;
                        }
                    }
                }
            }

            Multimap<User, BigDecimal> aggregated = Multimaps.transformEntries(result,
                    new ReserveToBigDecimalFunction());
            Map<User, BigDecimal> results = Maps.transformValues(aggregated.asMap(), SumFunction.INSTANCE);

            buffer.put(time, results);

            return results;
        }

        public void addReserve(DateTime dt, User user, BigDecimal amount) {
            MapUtil.listAdd(reserves, dt, new Reserve(dt, user, amount));
        }

        public void spend(DateTime dt, BigDecimal amount) {
            MapUtil.listAdd(spendings, dt, amount);
        }

    }

    private CashReserve reserve;

    private Map<DateTime, List<BigDecimal>> mutations = new TreeMap<DateTime, List<BigDecimal>>();

    private Map<DateTime, BigDecimal> cashFlow = new TreeMap<DateTime, BigDecimal>();

    private Multimap<Long, Contribution> contributionsByExpense = LinkedListMultimap.create();

    private Multimap<DateTime, Contribution> contributionsByDate = ArrayListMultimap.create();

    private ExpenseDAO expenseDAO;

    private DonationDAO donationDAO;

    private SubscriptionDAO subscriptionDAO;

    private PaidExpenseDAO paidExpenseDAO;

    private String exhaustionPoint = null;

    public FinancialTimeline(ExpenseDAO expenseDAO, DonationDAO donationDAO, SubscriptionDAO subscriptionDAO,
            PaidExpenseDAO paidExpenseDAO) {
        this.expenseDAO = expenseDAO;
        this.donationDAO = donationDAO;
        this.subscriptionDAO = subscriptionDAO;
        this.paidExpenseDAO = paidExpenseDAO;

        init();
    }

    private void init() {
        reserve = new CashReserve();

        DonationFilter df = new DonationFilter();
        df.addOrderBy("donationTime", true);
        for (Donation d : donationDAO.findByFilter(df)) {
            DateTime dt = new DateTime(d.getDonationTime());
            MapUtil.listAdd(mutations, dt, d.getAmount());
            reserve.addReserve(dt, d.getDonator(), d.getAmount());
        }

        PaidExpenseFilter pef = new PaidExpenseFilter();
        pef.addOrderBy("paymentDate", true);

        for (PaidExpense expense : paidExpenseDAO.findByFilter(pef)) {
            DateTime dt = new DateTime(expense.getPaymentDate());
            MapUtil.listAdd(mutations, dt, expense.getAmount().negate());
            reserve.spend(dt, expense.getAmount());
        }

        BigDecimal current = BigDecimal.ZERO;
        DateTime last = null;

        for (Entry<DateTime, List<BigDecimal>> e : mutations.entrySet()) {
            if (!e.getKey().equals(last)) {
                if (current.compareTo(BigDecimal.ZERO) < 0) {
                    current = BigDecimal.ZERO;
                }
                if (last != null) {
                    cashFlow.put(last, current);
                }
            }

            last = e.getKey();

            current = current.add(SumFunction.INSTANCE.apply(e.getValue()));

        }

        if (last != null) {
            cashFlow.put(last, current);
        }

        expenses: for (PaidExpense expense : paidExpenseDAO.findByFilter(pef)) {
            DateTime dt = new DateTime(expense.getPaymentDate());

            BigDecimal toFulfill = expense.getAmount();

            Map<User, BigDecimal> reservesAtTime = Maps.newLinkedHashMap(reserve.getReservesAt(dt));

            for (Entry<User, BigDecimal> e : reservesAtTime.entrySet()) {
                if (e.getValue().compareTo(toFulfill) >= 0) {
                    Contribution c = new Contribution(e.getKey(), toFulfill);

                    contributionsByDate.put(dt, c);
                    contributionsByExpense.put(expense.getId(), c);

                    continue expenses;
                } else {
                    Contribution c = new Contribution(e.getKey(), e.getValue());

                    contributionsByDate.put(dt, c);
                    contributionsByExpense.put(expense.getId(), c);

                    toFulfill = toFulfill.subtract(e.getValue());
                }
            }

            if (toFulfill.compareTo(BigDecimal.ZERO) > 0) {
                Contribution c = new Contribution(expense.getPaidBy(), toFulfill);
                contributionsByDate.put(dt, c);
                contributionsByExpense.put(expense.getId(), c);

            }
        }

        Map<DateTime, List<BigDecimal>> futureMutations = new TreeMap<DateTime, List<BigDecimal>>();

        DateTime now = new DateTime();
        DateTime threeYearsFromNow = now.plusYears(3);
        List<Expense> expenses = expenseDAO.getActiveExpenses();
        for (Expense e : expenses) {
            DateTime c = now;

            DateTime eStart = new DateTime(e.getStart());
            DateTime eEnd = e.getEnd() != null ? new DateTime(e.getEnd()) : null;

            while (!c.isAfter(threeYearsFromNow) && (e.getEnd() == null || !c.isAfter(eEnd))) {
                if (!c.isBefore(eStart)) {
                    MapUtil.listAdd(futureMutations, c, e.getAmount().negate());
                }

                c = new DateTime(e.getPeriod().nextDate(c.toDate()));
            }
        }

        List<Subscription> subscriptions = subscriptionDAO.findAll();
        for (Subscription s : subscriptions) {
            DateTime c = now;

            DateTime sStart = new DateTime(s.getStart());

            while (!c.isAfter(threeYearsFromNow)) {
                if (!c.isBefore(sStart)) {
                    MapUtil.listAdd(futureMutations, c, s.getAmount());
                }

                c = new DateTime(s.getInterval().nextDate(c.toDate()));
            }
        }

        for (Entry<DateTime, List<BigDecimal>> e : futureMutations.entrySet()) {
            current = current.add(SumFunction.INSTANCE.apply(e.getValue()));

            if (current.compareTo(BigDecimal.ZERO) < 0) {
                exhaustionPoint = "until " + e.getKey().toString("dd MMMM yyyy");
                break;
            }
        }

    }

    public String getExhaustionPoint() {
        return exhaustionPoint;
    }

    public IModel<List<BaseSeries<String, BigDecimal>>> getReservesAt(DateTime time) {
        BaseSeries<String, BigDecimal> series = new BaseSeries<String, BigDecimal>();

        Map<User, BigDecimal> reservesAt = reserve.getReservesAt(time);

        for (Entry<User, BigDecimal> e : reservesAt.entrySet()) {
            series.addEntry(e.getKey() != null ? e.getKey().getUsername() : "Anonymous", e.getValue());
        }

        return listOf(series);

    }

    public IModel<List<BaseSeries<String, BigDecimal>>> getParticipation(DateTime start, DateTime end) {
        BaseSeries<String, BigDecimal> series = new BaseSeries<String, BigDecimal>();

        Multimap<String, BigDecimal> amounts = HashMultimap.create();

        for (Entry<DateTime, Contribution> e : contributionsByDate.entries()) {
            if (!e.getKey().isBefore(start) && !e.getKey().isAfter(end)) {
                amounts.put(e.getValue().getUser() != null ? e.getValue().getUser().getUsername() : "Anonymous",
                        e.getValue().getAmount());
            }
        }

        Map<String, BigDecimal> sums = Maps.transformValues(amounts.asMap(), SumFunction.INSTANCE);

        for (Entry<String, BigDecimal> e : sums.entrySet()) {
            series.addEntry(e.getKey(), e.getValue());
        }

        return listOf(series);
    }

    public IModel<List<DateNumberSeries<BigDecimal>>> getCashFlow(DateTime start, DateTime end) {

        DateNumberSeries<BigDecimal> series = new DateNumberSeries<BigDecimal>();

        for (Entry<DateTime, BigDecimal> e : cashFlow.entrySet()) {
            if (!e.getKey().isBefore(start) && !e.getKey().isAfter(end)) {
                series.addEntry(e.getKey().toDate(), e.getValue());
            }
        }

        return listOf(series);
    }

    private <T extends Number> IModel<List<DateNumberSeries<T>>> listOf(DateNumberSeries<T> series) {
        List<DateNumberSeries<T>> res = new ArrayList<DateNumberSeries<T>>(1);

        res.add(series);

        return new ListModel<DateNumberSeries<T>>(res);
    }

    private <K, V extends Number> IModel<List<BaseSeries<K, V>>> listOf(BaseSeries<K, V> series) {
        List<BaseSeries<K, V>> res = new ArrayList<BaseSeries<K, V>>(1);

        res.add(series);

        return new ListModel<BaseSeries<K, V>>(res);
    }

    public BigDecimal getReservesToday() {

        BigDecimal r = BigDecimal.ZERO;
        for (BigDecimal v : reserve.getReservesAt(new DateTime()).values()) {
            r = r.add(v);
        }
        return r;
    }

}