de.janrieke.contractmanager.ext.hibiscus.UmsatzImportListDialog.java Source code

Java tutorial

Introduction

Here is the source code for de.janrieke.contractmanager.ext.hibiscus.UmsatzImportListDialog.java

Source

/*
 *   This file is part of ContractManager for Jameica.
 *   Copyright (C) 2010-2011  Jan Rieke
 *
 *   ContractManager 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.
 *
 *   ContractManager 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/>.
 */

/*
 * Partially copied from Hibiscus/Syntax, (c) by willuhn.webdesign
 */

package de.janrieke.contractmanager.ext.hibiscus;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Listener;

import de.janrieke.contractmanager.Settings;
import de.janrieke.contractmanager.gui.action.ShowContractDetailView;
import de.janrieke.contractmanager.gui.control.ContractControl;
import de.janrieke.contractmanager.gui.view.ContractDetailView;
import de.janrieke.contractmanager.rmi.Address;
import de.janrieke.contractmanager.rmi.Contract;
import de.janrieke.contractmanager.rmi.Contract.IntervalType;
import de.janrieke.contractmanager.rmi.Costs;
import de.janrieke.contractmanager.rmi.Transaction;
import de.willuhn.datasource.GenericIterator;
import de.willuhn.jameica.gui.AbstractView;
import de.willuhn.jameica.gui.Action;
import de.willuhn.jameica.gui.GUI;
import de.willuhn.jameica.gui.dialogs.AbstractDialog;
import de.willuhn.jameica.gui.input.CheckboxInput;
import de.willuhn.jameica.gui.input.TextInput;
import de.willuhn.jameica.gui.parts.Button;
import de.willuhn.jameica.gui.parts.ButtonArea;
import de.willuhn.jameica.gui.parts.TablePart;
import de.willuhn.jameica.gui.util.Container;
import de.willuhn.jameica.gui.util.DelayedListener;
import de.willuhn.jameica.gui.util.SimpleContainer;
import de.willuhn.jameica.hbci.rmi.Umsatz;
import de.willuhn.jameica.hbci.server.VerwendungszweckUtil;
import de.willuhn.jameica.hbci.server.VerwendungszweckUtil.Tag;
import de.willuhn.jameica.system.OperationCanceledException;
import de.willuhn.logging.Logger;
import de.willuhn.util.ApplicationException;

/**
 * Dialog for selecting a contract for a transaction. Uses a table and includes
 * search capabilities.
 */
public class UmsatzImportListDialog extends AbstractDialog<Contract> {
    private List<Contract> list = null;
    private Contract chosen = null;

    private TextInput search = null;
    private TablePart table = null;
    private Button assignButton = null;
    private Button createContractButton = null;
    private Umsatz umsatz;
    private boolean moreImportsFollow;
    private CheckboxInput applyForAll;

    /**
     * ct.
     *
     * @param position
     * @param preselected
     *            the preselected contract.
     * @throws RemoteException
     */
    public UmsatzImportListDialog(Umsatz u, Contract preSelectedContract, boolean moreImportsFollow)
            throws RemoteException {
        super(AbstractDialog.POSITION_CENTER, true);
        this.umsatz = u;
        this.chosen = preSelectedContract;
        this.moreImportsFollow = moreImportsFollow;

        this.setTitle(Settings.i18n().tr("Select contract"));
        this.setPanelText(Settings.i18n().tr("Select contract to assign transaction to."));
        this.setSize(500, 600);
    }

    /**
     * @see de.willuhn.jameica.gui.dialogs.AbstractDialog#getData()
     */
    @Override
    protected Contract getData() throws Exception {
        return chosen;
    }

    /**
     * @see de.willuhn.jameica.gui.dialogs.AbstractDialog#paint(org.eclipse.swt.widgets.Composite)
     */
    @Override
    protected void paint(final Composite parent) throws Exception {
        this.list = initializeContractList();

        Container group = new SimpleContainer(parent, true);

        group.addText(Settings.i18n().tr("Transaction to assign:"), true);
        group.addText(
                Settings.i18n().tr("From/To") + ": " + StringUtils.defaultString(umsatz.getGegenkontoName(), ""),
                false);
        group.addText(Settings.i18n().tr("Reference") + ": " + VerwendungszweckUtil.toString(umsatz), true);
        group.addText(Settings.i18n().tr("Date") + ": " + Settings.dateformat(umsatz.getDatum()), false);
        group.addText(Settings.i18n().tr("Value") + ": " + Settings.formatAsCurrency(umsatz.getBetrag()), false);

        TextInput text = this.getSearch();
        group.addInput(text);
        group.addPart(this.getTable());

        // //////////////
        // geht erst nach dem Paint
        if (chosen != null) {
            getTable().select(chosen);
        }

        text.getControl().addKeyListener(new DelayedAdapter());

        if (moreImportsFollow) {
            group.addCheckbox(getApplyForAllCheckbox(),
                    Settings.i18n().tr("Also assign all further transactions to this contract"));
        }

        ButtonArea buttons = new ButtonArea();
        buttons.addButton(getAssignButton());
        buttons.addButton(getCreateContractButton());
        buttons.addButton(Settings.i18n().tr("Cancel"), context -> {
            throw new OperationCanceledException();
        }, null, false, "process-stop.png");

        group.addButtonArea(buttons);

        // Replace the 'wrong' text on the assign button.
        parent.addPaintListener(new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
                getAssignButton().setText(Settings.i18n().tr("Assign"));
                parent.removePaintListener(this);
            }
        });
    }

    /**
     * Returns the import button.
     *
     * @return the button.
     */
    private Button getAssignButton() {
        if (assignButton != null) {
            return assignButton;
        }

        // Use the 'wrong', but longer text on the assign button, first.
        // It will be replaced immediately on the first draw.
        assignButton = new Button(Settings.i18n().tr("Assign all"), new AssingToContract(), null, true, "ok.png");
        assignButton.setEnabled(false); // initially disabled
        return assignButton;
    }

    /**
     * Returns the "Create new contract" button.
     *
     * @return the button.
     */
    private Button getCreateContractButton() {
        if (this.createContractButton != null) {
            return this.createContractButton;
        }

        this.createContractButton = new Button(Settings.i18n().tr("New Contract from Transaction"),
                new CreateNewContract(), null, true, "document-new.png");
        return this.createContractButton;
    }

    /**
     * Returns the search input
     *
     * @return the search input
     */
    private TextInput getSearch() {
        if (this.search != null) {
            return this.search;
        }

        this.search = new TextInput("");
        this.search.focus();
        this.search.setName(Settings.i18n().tr("Keyword"));
        return this.search;
    }

    /**
     * Returns the select input "apply for all"
     *
     * @return the select input
     */
    private CheckboxInput getApplyForAllCheckbox() {
        if (applyForAll != null) {
            return applyForAll;
        }

        applyForAll = new CheckboxInput(false);
        applyForAll.addListener(event -> {
            if (event == null || event.type != SWT.Selection) {
                return;
            }
            if (Boolean.TRUE.equals(applyForAll.getValue())) {
                getAssignButton().setText(Settings.i18n().tr("Assign all"));
            } else {
                getAssignButton().setText(Settings.i18n().tr("Assign"));
            }
        });
        return applyForAll;
    }

    public boolean isApplyForAll() {
        return applyForAll != null && (Boolean) applyForAll.getValue();
    }

    /**
     * Returns the contract table
     *
     * @return the contract table
     */
    private TablePart getTable() {
        if (this.table != null) {
            return this.table;
        }

        this.table = new TablePart(this.list, new AssingToContract());
        this.table.setSummary(false);
        this.table.addColumn(Settings.i18n().tr("Name of Contract"), "name");
        this.table.addColumn(Settings.i18n().tr("Contract Partner"), Contract.PARTNER_NAME);
        this.table.setFormatter(item -> {
            if (item == null) {
                return;
            }

            try {
                Contract c = (Contract) item.getData();
                if (c == null) {
                    return;
                }

                Color col = null;

                boolean t = c.isActiveInMonth(new Date());
                if (!t) {
                    col = Settings.getNotActiveForegroundColor();
                } else {
                    col = de.willuhn.jameica.gui.util.Color.FOREGROUND.getSWTColor();
                }
                item.setForeground(col);
            } catch (Exception e) {
                Logger.error("unable to apply color", e);
            }
        });

        this.table.addSelectionListener(event -> getAssignButton().setEnabled(event.data != null));
        this.getAssignButton().setEnabled(this.chosen != null);
        return this.table;
    }

    /**
     * Action for assigning
     */
    private class AssingToContract implements Action {
        @Override
        public void handleAction(Object context) throws ApplicationException {
            chosen = (Contract) getTable().getSelection();
            if (chosen != null) {
                close();
            }
        }
    }

    /**
     * Action for creating a new contract from the transaction
     */
    private class CreateNewContract implements Action {
        @Override
        public void handleAction(Object context) throws ApplicationException {
            // Create a new contract from the data of the transaction
            ShowContractDetailView showContractDetailView = new ShowContractDetailView();
            try {
                Contract c = (Contract) Settings.getDBService().createObject(Contract.class, null);

                // Set SEPA infos.
                Map<Tag, String> sepaTags = VerwendungszweckUtil.parse(umsatz);
                String mref = sepaTags.get(Tag.MREF);
                if (StringUtils.isNotBlank(mref)) {
                    c.setSepaCustomerRef(mref);
                }
                String cred = sepaTags.get(Tag.CRED);
                if (StringUtils.isNotBlank(cred)) {
                    c.setSepaCreditorRef(cred);
                }

                if (umsatz.getUmsatzTyp() != null) {
                    c.setHibiscusCategoryID(umsatz.getUmsatzTyp().getID());
                }

                c.setStartDate(umsatz.getDatum());
                c.setComment(VerwendungszweckUtil.toString(umsatz));
                // Set some defaults on the runtime.
                c.setCancelationPeriodType(IntervalType.MONTHS);
                c.setFirstMinRuntimeType(IntervalType.MONTHS);
                c.setFollowingMinRuntimeType(IntervalType.MONTHS);

                if (StringUtils.isNotBlank(umsatz.getGegenkontoName())) {
                    Address a = (Address) Settings.getDBService().createObject(Address.class, null);
                    a.setName(umsatz.getGegenkontoName());
                    c.setAddress(a);
                }

                // The ID for the new contract will only be available upon save.
                // Thus, we have to delay storing the n-to-n references for the
                // Costs and Transactions tables. To do so, we them as temporary
                // data.

                // Use the financial data from this transaction as a cost entry.
                Costs costs = (Costs) Settings.getDBService().createObject(Costs.class, null);
                costs.setContract(c);
                costs.setMoney(umsatz.getBetrag());
                costs.setPeriod(IntervalType.MONTHS);

                // Create a new Transaction assignment.
                Transaction trans = (Transaction) Settings.getDBService().createObject(Transaction.class, null);
                trans.setContract(c);
                trans.setTransactionID(Integer.parseInt(umsatz.getID()));

                chosen = null;
                close();
                showContractDetailView.handleAction(c);

                AbstractView currentView = GUI.getCurrentView();
                if (currentView instanceof ContractDetailView) {
                    ContractControl control = ((ContractDetailView) currentView).getControl();
                    control.addTemporaryCostEntry(costs);
                    control.addTemporaryTransactionAssignment(trans);
                    if (umsatz.getUmsatzTyp() != null) {
                        control.hibiscusCategoryID = umsatz.getUmsatzTyp().getID();
                    }
                }
            } catch (RemoteException e) {
                throw new ApplicationException(Settings.i18n().tr("Error while creating new contract"), e);
            }
        }
    }

    /**
     * Initialize the list of contracts by calculating the probability that the
     * contract is the intended contract. The list will be sorted by this
     * probability.
     *
     * @return initialized list
     * @throws RemoteException
     */
    private List<Contract> initializeContractList() throws RemoteException {
        final class SortedContract implements Comparable<SortedContract> {
            private float value;
            private Contract c;

            public SortedContract(Contract c, float value) {
                this.value = value;
                this.c = c;
            }

            public Contract getContract() {
                return c;
            }

            @Override
            public int compareTo(SortedContract o) {
                return Float.compare(this.value, o.value);
            }

            @Override
            public int hashCode() {
                return c.hashCode() + 1;
            }

            @Override
            public boolean equals(Object obj) {
                if (obj != null && obj instanceof SortedContract && ((SortedContract) obj).c == this.c) {
                    return true;
                } else {
                    return false;
                }
            }
        }

        List<SortedContract> sorted = new ArrayList<>();
        GenericIterator<Contract> contracts = ContractControl.getContracts();
        while (contracts.hasNext()) {
            Contract c = contracts.next();
            float simliarity = calculateSimilarity(c, this.umsatz);
            sorted.add(new SortedContract(c, simliarity));
        }
        Collections.sort(sorted);

        List<Contract> l = new LinkedList<>();
        for (SortedContract c : sorted) {
            l.add(c.getContract());
        }
        return l;
    }

    /**
     * Da KeyAdapter/KeyListener nicht von swt.Listener abgeleitet sind, muessen
     * wir leider dieses schraege Konstrukt verenden, um den DelayedListener
     * verwenden zu koennen
     */
    private class DelayedAdapter extends KeyAdapter {
        private Listener forward = new DelayedListener(150, event -> {
            TablePart table = getTable();
            table.removeAll();

            String text = (String) getSearch().getValue();
            text = text.trim().toLowerCase();
            try {
                for (Contract t : list) {
                    if (text.length() == 0) {
                        table.addItem(t);
                        continue;
                    }

                    if (t.getName().toLowerCase().contains(text)
                            || t.getPartnerName().toLowerCase().contains(text)) {
                        table.addItem(t);
                    }
                }
            } catch (RemoteException re) {
                Logger.error("error while adding items to table", re);
            }
        });

        /**
         * @see org.eclipse.swt.events.KeyAdapter#keyReleased(org.eclipse.swt.events.KeyEvent)
         */
        @Override
        public void keyReleased(KeyEvent e) {
            forward.handleEvent(null); // Das Event-Objekt interessiert uns eh
            // nicht.
        }
    }

    private float getRelativeLevenshteinDistance(String a, String b) {
        int distance = StringUtils.getLevenshteinDistance(a.toLowerCase(), b.toLowerCase());
        int maxLength = Math.max(a.length(), b.length());
        int minLength = Math.min(a.length(), b.length());
        int maxDistance = maxLength;
        int minDistance = maxLength - minLength;
        return (float) Math.sqrt(Math.sqrt((((float) (distance - minDistance)) / ((float) maxDistance))));
    }

    private static final int MINIMUM_TOKEN_SIZE = 3; // do not count 1 or 2
    // character tokens.

    private float calculateSimilarity(Contract c, Umsatz transaction) {
        try {
            String trName = "";
            if (transaction.getGegenkontoName() != null) {
                trName = transaction.getGegenkontoName();
            }

            // No separators between lines until we have a working SEPA
            // rewriter. Lines end after 27 chars, but fields may be 35 chars
            // long.
            String trUse = VerwendungszweckUtil.toString(transaction, "");

            String[] trNameTokens = trName.split("\\s+");
            String[] trUseTokens = trUse.split("\\s+");

            float[] distanceName = new float[trNameTokens.length];
            float[] distanceUse = new float[trUseTokens.length];

            // try finding the partner name in the owner of the opposite account
            for (int i = 0; i < trNameTokens.length; i++) {
                if (trNameTokens[i].length() < MINIMUM_TOKEN_SIZE) {
                    distanceName[i] = 1; // do not count small tokens
                } else {
                    distanceName[i] = getRelativeLevenshteinDistance(trNameTokens[i], c.getPartnerName());
                }
            }

            // try finding the contract name in the reason for payment
            for (int i = 0; i < trUseTokens.length; i++) {
                if (trUseTokens[i].length() < MINIMUM_TOKEN_SIZE) {
                    distanceUse[i] = 1; // do not count small tokens
                } else {
                    distanceUse[i] = getRelativeLevenshteinDistance(trUseTokens[i], c.getName());
                }
            }

            // try finding the contract numbers or SEPA refs in the reason for
            // payment
            String customerNumber = c.getCustomerNumber();
            String contractNumber = c.getContractNumber();
            String sepaCreditorRef = c.getSepaCreditorRef();
            String sepaCustomerRef = c.getSepaCustomerRef();

            int exactHits = 0;
            if (containsString(trUse, customerNumber)) {
                exactHits++;
            }
            if (containsString(trUse, contractNumber)) {
                exactHits++;
            }
            if (containsString(trUse, sepaCreditorRef)) {
                exactHits++;
            }
            if (containsString(trUse, sepaCustomerRef)) {
                exactHits++;
            }

            float result = 1;
            for (int i = 0; i < trNameTokens.length; i++) {
                result *= distanceName[i];
                if (distanceName[i] == 0) {
                    exactHits++;
                }
            }
            for (int i = 0; i < trUseTokens.length; i++) {
                result *= distanceUse[i];
                if (distanceUse[i] == 0) {
                    exactHits++;
                }
            }
            return result - exactHits;
        } catch (RemoteException e) {
            Logger.error("Error while looking for contracts that match the transaction.", e);
            return 1;
        }
    }

    private boolean containsString(String containingString, String containedString) {
        return (containedString != null) && (!"".equals(containedString))
                && (containingString.contains(containedString));
    }
}