biz.wolschon.finance.jgnucash.panels.TaxReportPanel.java Source code

Java tutorial

Introduction

Here is the source code for biz.wolschon.finance.jgnucash.panels.TaxReportPanel.java

Source

package biz.wolschon.finance.jgnucash.panels;
/**
 * TaxReportPanel.java
 * created: 09.12.2007 13:09:26
 * (c) 2007 by <a href="http://Wolschon.biz">
 * Wolschon Softwaredesign und Beratung</a>
 * This file is part of jgnucashLib-V1 by Marcus Wolschon
 * <a href="mailto:Marcus@Wolscon.biz">Marcus@Wolscon.biz</a>.
 * You can purchase support for a sensible hourly rate or
 * a commercial license of this file (unless modified by others)
 * by contacting him directly.
 *
 *  jgnucashLib-V1 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.
 *
 *  jgnucashLib-V1 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 jgnucashLib-V1.  If not, see <http://www.gnu.org/licenses/>.
 *
 ***********************************
 * Editing this file:
 *  -For consistent code-quality this file should be checked with the
 *   checkstyle-ruleset enclosed in this project.
 *  -After the design of this file has settled it should get it's own
 *   JUnit-Test that shall be executed regularly. It is best to write
 *   the test-case BEFORE writing this class and to run it on every build
 *   as a regression-test.
 */

//automatically created propertyChangeListener-Support
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.filechooser.FileFilter;
import javax.xml.bind.JAXBException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import biz.wolschon.fileformats.gnucash.GnucashAccount;
import biz.wolschon.fileformats.gnucash.GnucashFile;
import biz.wolschon.finance.jgnucash.widgets.TransactionSum;
import biz.wolschon.finance.jgnucash.widgets.TransactionSum.SUMMATIONTYPE;
import biz.wolschon.numbers.FixedPointNumber;

/**
 * (c) 2007 by <a href="http://Wolschon.biz>
 * Wolschon Softwaredesign und Beratung</a>.<br/>
 * Project: jgnucashLib-V1<br/>
 * TaxReportPanel.java<br/>
 * created: 09.12.2007 13:09:26 <br/>
 *<br/><br/>
 * Panel to show some tax-related sums.
 * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a>
 */
public class TaxReportPanel extends JPanel {

    /**
     * For serializing.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Our logger for debug- and error-ourput.
     */
    private static final Log LOGGER = LogFactory.getLog(TransactionSum.class);

    /**
     * The sums we show.
     */
    private final List<TransactionSum> mySums = new LinkedList<TransactionSum>();

    /**
     * The panel to contain the created {@link TransactionSum}s.
     */
    private final JPanel mySumsPanel = new JPanel();
    /**
     * The panel to contain the buttons to export a CSV.
     */
    private final JPanel myExportPanel = new JPanel();

    /**
     * Button in the {@link #myExportPanel} to export
     * a CSV.
     */
    private final JButton myExportButton = new JButton("Export...");

    private enum ExportGranularities {
        Month {
            @Override
            public int getCalendarConstant() {
                return GregorianCalendar.MONTH;
            }
        },
        Year {
            @Override
            public int getCalendarConstant() {
                return GregorianCalendar.YEAR;
            }
        },
        Day {
            @Override
            public int getCalendarConstant() {
                return GregorianCalendar.DAY_OF_MONTH;
            }
        };
        @Override
        public String toString() {
            return "per " + super.toString();
        }

        /**
         * @return a GregorianCalendar-constant to add 1 of;
         */
        public abstract int getCalendarConstant();
    };

    /**
     * Combobox to select a yearly, monthly or dayly export
     * with the {@link #myExportButton}.
     */
    private final JComboBox myExportGranularityCombobox = new JComboBox(ExportGranularities.values());

    /**
     * @param books The financial data we operate on.
     */
    public TaxReportPanel(final GnucashFile books) {
        if (books != null) {
            initializeUI(books);
        }
    }

    /**
     * @param books the accounts and transactions we work with.
     */
    private void initializeUI(final GnucashFile books) {
        this.setLayout(new BorderLayout());
        Properties props = new Properties();
        File configFile = new File(new File(System.getProperty("user.home"), ".jgnucash"), "TaxReportPanel.xml");
        if (configFile.exists()) {
            try {
                props.loadFromXML(new FileInputStream(configFile));
            } catch (Exception e) {
                LOGGER.error("Problem loading " + configFile.getAbsolutePath(), e);
                JLabel errorLabel = new JLabel(e.getMessage());
                this.add(errorLabel, BorderLayout.CENTER);
                return;
            }
        } else {
            throw new IllegalStateException(
                    "To use the OPTIONAL tax-report panel, please create a file " + configFile.getAbsolutePath());
            /*try {
            props.loadFromXML(getClass().getResourceAsStream(
                    "TaxReportPanel.xml"));
            props.storeToXML(new FileOutputStream(configFile),
                    "UTF-8\n"
                    + "sum.([0-9]*).name - name of the entry\n"
                    + "sum.([0-9]*).target.([0-9]*) - (may be ommited) "
                    + "Look at all transactions containing these accounts You can specify qualified names, unqualified names or ids\n"
                    + "sum.([0-9]*).source.([0-9]*) - of these accounts add all splits that refer to these accounts\n"
                    + "sum.([0-9]*).type = ONLYTO        - sum only the ones that increase the balance of the account (other values: ONLYFROM, ALL)\n"
                    + "sum.([0-9]*).type = ONLYFROM      - sum only the ones that decrease the balance of the account (other values: ONLYFROM, ALL)\n"
                    + "sum.([0-9]*).type = ALL           - sum all the ones that increase or decreas the balance of the account (other values: ONLYFROM, ALL)\n"
                    + "sum.([0-9]*).type = ALLRECURSIVE  - ignore targets and build the recursive balance\n");
            LOGGER.info("demo-config-file for TaxReportPanel has been stored in "
                    + configFile.getAbsolutePath());
            } catch (Exception e) {
            LOGGER.error("Problem loading or storing default-TaxReportPanel.xml", e);
            JLabel errorLabel = new JLabel(e.getMessage());
            this.add(errorLabel, BorderLayout.CENTER);
            return;
            }*/
        }

        LOGGER.info("calculating tax-panel...");
        for (int i = 0; props.containsKey("sum." + i + ".name"); i++) {
            try {
                createSum(books, props, i);
            } catch (Exception e) {
                LOGGER.error("[Exception] Problem in " + getClass().getName(), e);
            }
        }

        mySumsPanel.setLayout(new GridLayout(mySums.size(), 1));
        for (TransactionSum sum : mySums) {
            mySumsPanel.add(sum);
        }

        this.add(mySumsPanel, BorderLayout.CENTER);

        myExportButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent aE) {
                showExportCSVDialog();
            }
        });
        myExportPanel.add(myExportButton);
        myExportGranularityCombobox.setSelectedItem(ExportGranularities.Month);
        myExportGranularityCombobox.setEditable(false);
        myExportPanel.add(myExportGranularityCombobox);
        this.add(myExportPanel, BorderLayout.SOUTH);
        LOGGER.info("calculating tax-panel...DONE");
    }

    /**
     * Show a dialog to export
     * a CSV-file that contains the
     * shown {@link TransactionSum}s
     * for each month, year or day.
     */
    protected void showExportCSVDialog() {
        JFileChooser fc = new JFileChooser();
        fc.setAcceptAllFileFilterUsed(true);
        fc.addChoosableFileFilter(new FileFilter() {

            @Override
            public boolean accept(final File aF) {
                return aF.isDirectory() || aF.getName().endsWith(".csv");
            }

            @Override
            public String getDescription() {
                return "CSV-file";
            }
        });
        int dialogResult = fc.showSaveDialog(this);
        if (dialogResult != JFileChooser.APPROVE_OPTION) {
            return;
        }
        File file = fc.getSelectedFile();
        if (file.exists()) {
            int confirmation = JOptionPane.showConfirmDialog(this, "File exists. Replace file?");
            if (confirmation != JOptionPane.YES_OPTION) {
                showExportCSVDialog();
                return;
            }
        }
        ExportGranularities gran = (ExportGranularities) myExportGranularityCombobox.getSelectedItem();
        try {
            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
            exportCSV(file, gran);
        } finally {
            setCursor(Cursor.getDefaultCursor());
        }
    }

    /**
     * @param aFile the file to write to
     * @param aGran the granularity
     */
    private void exportCSV(final File aFile, final ExportGranularities aGran) {
        //TODO: implement CSV-export

        FileWriter fw = null;
        try {
            fw = new FileWriter(aFile);
            // write headers
            List<TransactionSum> sums = mySums;
            fw.write("day");
            for (TransactionSum transactionSum : sums) {
                fw.write(",");
                fw.write(transactionSum.getName());
            }
            fw.write("\n");

            // write data
            GregorianCalendar cal = new GregorianCalendar();
            int add = aGran.getCalendarConstant();
            DateFormat dateFormat = DateFormat.getDateInstance();
            //NumberFormat numberFormat = NumberFormat.getInstance();
            // we do NOT use getCurrencyInstance because it
            // contains a locale-specific currency-symbol
            for (int i = 0; i < 100; i++) {
                Date maxDate = cal.getTime();

                fw.write(dateFormat.format(maxDate));
                int transactionsCounted = 0;
                for (TransactionSum transactionSum : sums) {
                    fw.write(",");
                    try {
                        transactionSum.setMaxDate(maxDate);
                        FixedPointNumber value = transactionSum.getValue();
                        if (value != null) {
                            //fw.write(numberFormat.format(value));
                            fw.write(value.toString());
                        }
                        transactionsCounted += transactionSum.getTransactionsCounted();
                    } catch (JAXBException e) {
                        LOGGER.error("Error calculating one of the TransactionSums", e);
                        fw.write("ERROR");
                    }
                }
                fw.write("\n");
                if (transactionsCounted == 0) {
                    break;
                    // we are walking back in time,
                    // when there are no matching transactions,
                    // all future runs will we a waste of time.
                    // This happens often when add==YEAR
                }

                long old = cal.getTimeInMillis();
                if (add == GregorianCalendar.MONTH) {
                    cal.set(GregorianCalendar.DAY_OF_MONTH, 1);
                }
                if (add == GregorianCalendar.YEAR) {
                    cal.set(GregorianCalendar.DAY_OF_MONTH, 1);
                    cal.set(GregorianCalendar.MONTH, 1);
                }
                // usually we are in the middle of a month,
                // so do not skip the first day of the current
                // month
                if (i != 0 || old == cal.getTimeInMillis() || add == GregorianCalendar.DAY_OF_MONTH) {
                    cal.add(add, -1);
                }
            }

            fw.close();
            fw = null;
        } catch (IOException e) {
            LOGGER.error("cannot write csv-file", e);
            JOptionPane.showMessageDialog(this, "Cannot write CSV-file\n" + e.getMessage());
        } finally {
            if (fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    LOGGER.error("cannot close csv-file", e);
                }
            }
            for (TransactionSum transactionSum : mySums) {
                try {
                    transactionSum.setMaxDate(null);
                } catch (JAXBException e) {
                    LOGGER.error("cannot set maxDate back to it's original value", e);
                }
            }
        }
    }

    /**
     * Load the settings for a TransactionSum from our properties-file.
     * @param books the accounts and transactions we work with.
     * @param prop where to load the config from
     * @param index the index into the configs
     * @return thw widget created
     * @throws JAXBException if we cannot access the accounts/transactions in the XML-backend.
     */
    private TransactionSum createSum(final GnucashFile books, final Properties prop, final int index)
            throws JAXBException {
        SUMMATIONTYPE type = SUMMATIONTYPE.getByName(prop.getProperty("sum." + index + ".type"));

        String name = prop.getProperty("sum." + index + ".name");

        Set<GnucashAccount> target = getAccountsByProperty(books, prop, "sum." + index + ".target");

        Set<GnucashAccount> source = getAccountsByProperty(books, prop, "sum." + index + ".source");

        TransactionSum sum = new TransactionSum(books, source, target, type, name, getMinDate(), getMaxDate());
        mySums.add(sum);
        return sum;
    }

    private Set<GnucashAccount> getAccountsByProperty(final GnucashFile aBooks, final Properties props,
            final String prefix) {

        Set<GnucashAccount> retval = new HashSet<GnucashAccount>();
        for (int i = 0; props.containsKey(prefix + "." + i); i++) {
            String idOrName = props.getProperty(prefix + "." + i);
            GnucashAccount account = aBooks.getAccountByIDorName(idOrName, idOrName);
            if (account == null) {
                LOGGER.error("account '" + idOrName + "' given in property '" + prefix + "." + i + "' not found");
            } else {
                retval.add(account);
            }
        }
        return retval;
    }

    /**
     * @param aBooks The books to set.
     * @see #myBooks
     */
    public void setBooks(final GnucashFile aBooks) {
        if (mySumsPanel.getParent() == null) {
            initializeUI(aBooks);
        }

        JAXBException err = null;
        for (TransactionSum sum : mySums) {
            try {
                sum.setBooks(aBooks);
            } catch (JAXBException e) {
                LOGGER.error("[JAXBException] Problem in " + getClass().getName(), e);
                mySums.remove(sum);
                err = e;
            }
        }
        if (err != null) {
            throw new IllegalStateException("Cannot access XML-backend.", err);
        }
    }

    /**
     * @return the minimum date for the {@link TransactionSum}.
     */
    private Date getMinDate() {
        //TODO: provide an input-field for the year.
        return null;/*new Date ((new GregorianCalendar(1970, 01, 01))
                    .getTimeInMillis())*/
    }

    /**
     * @return the maximum date for the {@link TransactionSum}.
     */
    private Date getMaxDate() {
        //      TODO: provide an input-field for the year.
        return null/*new Date ((new GregorianCalendar(2100, 12, 31)
                   ).getTimeInMillis())*/;
    }

    //------------------------ support for propertyChangeListeners -------------

    /**
     * support for firing PropertyChangeEvents.
     * (gets initialized only if we really have listeners)
     */
    private volatile PropertyChangeSupport myPropertyChange = null;

    /**
     * Returned value may be null if we never had listeners.
     * @return Our support for firing PropertyChangeEvents
     */
    protected final PropertyChangeSupport getPropertyChangeSupport() {
        return myPropertyChange;
    }

    /**
     * Add a PropertyChangeListener to the listener list.
     * The listener is registered for all properties.
     *
     * @param listener  The PropertyChangeListener to be added
     */
    @Override
    public final void addPropertyChangeListener(final PropertyChangeListener listener) {
        if (myPropertyChange == null) {
            myPropertyChange = new PropertyChangeSupport(this);
        }
        myPropertyChange.addPropertyChangeListener(listener);
    }

    /**
     * Add a PropertyChangeListener for a specific property.  The listener
     * will be invoked only when a call on firePropertyChange names that
     * specific property.
     *
     * @param propertyName  The name of the property to listen on.
     * @param listener  The PropertyChangeListener to be added
     */
    @Override
    public final void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        if (myPropertyChange == null) {
            myPropertyChange = new PropertyChangeSupport(this);
        }
        myPropertyChange.addPropertyChangeListener(propertyName, listener);
    }

    /**
     * Remove a PropertyChangeListener for a specific property.
     *
     * @param propertyName  The name of the property that was listened on.
     * @param listener  The PropertyChangeListener to be removed
     */
    @Override
    public final void removePropertyChangeListener(final String propertyName,
            final PropertyChangeListener listener) {
        if (myPropertyChange != null) {
            myPropertyChange.removePropertyChangeListener(propertyName, listener);
        }
    }

    /**
     * Remove a PropertyChangeListener from the listener list.
     * This removes a PropertyChangeListener that was registered
     * for all properties.
     *
     * @param listener  The PropertyChangeListener to be removed
     */
    @Override
    public final synchronized void removePropertyChangeListener(final PropertyChangeListener listener) {
        if (myPropertyChange != null) {
            myPropertyChange.removePropertyChangeListener(listener);
        }
    }

    //-------------------------------------------------------

}