Java tutorial
/** * GnucashFileImpl.java * License: GPLv3 or later * Created on 13.05.2005 * (c) 2005 by "Wolschon Softwaredesign und Beratung". * * * ----------------------------------------------------------- * major Changes: * 13.05.2005 - initial version * 11.11.2008 - using defaultCurrency from Gnucash-file * 03.01.2010 - support GNCVendor * ... * */ package biz.wolschon.fileformats.gnucash.jwsdpimpl; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xml.sax.InputSource; import biz.wolschon.fileformats.gnucash.GnucashAccount; import biz.wolschon.fileformats.gnucash.GnucashCustomer; import biz.wolschon.fileformats.gnucash.GnucashFile; import biz.wolschon.fileformats.gnucash.GnucashInvoice; import biz.wolschon.fileformats.gnucash.GnucashInvoiceEntry; import biz.wolschon.fileformats.gnucash.GnucashJob; import biz.wolschon.fileformats.gnucash.GnucashTaxTable; import biz.wolschon.fileformats.gnucash.GnucashTransaction; import biz.wolschon.fileformats.gnucash.GnucashTransactionSplit; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncBudget; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncGncBillTerm; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncGncEmployee; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncGncTaxTable; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncGncVendor; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncPricedb; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncSchedxaction; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.BookElementsGncTemplateTransactions; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncAccountType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncCommodityType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncCountDataType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncGncCustomerType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncGncEntryType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncGncInvoiceType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncGncJobType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncGncTaxTableType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncPricedbType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncPricedbType.PriceType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncPricedbType.PriceType.PriceCommodityType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncTransactionType; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.GncV2; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.ObjectFactory; import biz.wolschon.fileformats.gnucash.jwsdpimpl.generated.impl.runtime.DefaultJAXBContextImpl; import biz.wolschon.finance.ComplexCurrencyTable; import biz.wolschon.numbers.FixedPointNumber; /** * created: 13.05.2005<br/> * <br/> * Implementation of GnucashFile that can only * read but not modify Gnucash-Files. <br/> * * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a> * @see biz.wolschon.fileformats.gnucash.GnucashFile */ public class GnucashFileImpl implements GnucashFile { /** * Our logger for debug- and error-ourput. */ protected static final Log LOGGER = LogFactory.getLog(GnucashFileImpl.class); /** * my CurrencyTable. */ private final ComplexCurrencyTable currencyTable = new ComplexCurrencyTable(); /** * @return Returns the currencyTable. * @see ${@link #currencyTable} */ public ComplexCurrencyTable getCurrencyTable() { return currencyTable; } /** * * @return a read-only collection of all accounts */ public Collection<GnucashAccount> getAccounts() { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } return Collections.unmodifiableCollection(new TreeSet<GnucashAccount>(accountid2account.values())); } /** * Filles lazy in getTaxTables() . * @see #getTaxTables() */ protected Map<String, GnucashTaxTable> taxTablesById = null; /** * @param id id of a taxtable * @return the identified taxtable or null */ public GnucashTaxTable getTaxTableByID(final String id) { if (taxTablesById == null) { getTaxTables(); } return taxTablesById.get(id); } /** * @return all TaxTables defined in the book * @see ${@link GnucashTaxTable} */ @SuppressWarnings("unchecked") public Collection<GnucashTaxTable> getTaxTables() { if (taxTablesById == null) { taxTablesById = new HashMap<String, GnucashTaxTable>(); List bookElements = this.getRootElement().getGncBook().getBookElements(); for (Object bookElement : bookElements) { if (!(bookElement instanceof GncGncTaxTableType)) { continue; } GncGncTaxTableType jwsdpPeer = (GncGncTaxTableType) bookElement; GnucashTaxTableImpl gnucashTaxTable = new GnucashTaxTableImpl(jwsdpPeer, this); taxTablesById.put(gnucashTaxTable.getId(), gnucashTaxTable); } } return taxTablesById.values(); } /** * * @return a read-only collection of all accounts that have no parent (the * result is sorted) */ public Collection<? extends GnucashAccount> getRootAccounts() { try { Collection<GnucashAccount> retval = new TreeSet<GnucashAccount>(); for (GnucashAccount account : getAccounts()) { if (account.getParentAccountId() == null) { retval.add(account); } } return retval; } catch (RuntimeException e) { LOGGER.error("Problem getting all root-account", e); throw e; } catch (Throwable e) { LOGGER.error("SERIOUS Problem getting all root-account", e); return new LinkedList<GnucashAccount>(); } } /** * * @param id * if null, gives all account that have no parent * @return the sorted collection of children of that account */ public Collection getAccountsByParentID(final String id) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } SortedSet retval = new TreeSet(); for (Object element : accountid2account.values()) { GnucashAccount account = (GnucashAccount) element; String parent = account.getParentAccountId(); if (parent == null) { if (id == null) { retval.add(account); } } else { if (parent.equals(id)) { retval.add(account); } } } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getAccountByName(java.lang.String) */ public GnucashAccount getAccountByName(final String name) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } for (GnucashAccount account : accountid2account.values()) { if (account.getName().equals(name)) { return account; } if (account.getQualifiedName().equals(name)) { return account; } } return null; } /** * warning: this function has to traverse all * accounts. If it much faster to try * getAccountByID first and only call this method * if the returned account does not have the right name. * * @param nameRegEx the regular expression of the name to look for * @return null if not found * @see #getAccountByID(String) * @see #getAccountByName(String) */ public GnucashAccount getAccountByNameEx(final String nameRegEx) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } GnucashAccount foundAccount = getAccountByName(nameRegEx); if (foundAccount != null) { return foundAccount; } Pattern pattern = Pattern.compile(nameRegEx); for (GnucashAccount account : accountid2account.values()) { Matcher matcher = pattern.matcher(account.getName()); if (matcher.matches()) { return account; } } return null; } /** * First try to fetch the account by id, then * fall back to traversing all accounts to get * if by it's name. * * @param id the id to look for * @param name the name to look for if nothing is found for the id * @return null if not found * @see #getAccountByID(String) * @see #getAccountByName(String) */ public GnucashAccount getAccountByIDorName(final String id, final String name) { GnucashAccount retval = getAccountByID(id); if (retval == null) { retval = getAccountByName(name); } return retval; } /** * First try to fetch the account by id, then * fall back to traversing all accounts to get * if by it's name. * * @param id the id to look for * @param name the regular expression of the name to look for * if nothing is found for the id * @return null if not found * @see #getAccountByID(String) * @see #getAccountByName(String) */ public GnucashAccount getAccountByIDorNameEx(final String id, final String name) { GnucashAccount retval = getAccountByID(id); if (retval == null) { retval = getAccountByNameEx(name); } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getInvoiceByID(java.lang.String) */ public GnucashInvoice getInvoiceByID(final String id) { return invoiceid2invoice.get(id); } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getInvoices() */ @SuppressWarnings("unchecked") public Collection<GnucashInvoice> getInvoices() { Collection<GnucashInvoice> c = invoiceid2invoice.values(); ArrayList<GnucashInvoice> retval = new ArrayList<GnucashInvoice>(c); Collections.sort(retval); return retval; } /** * * @throws JAXBException if we have issues with the XML-backend * @see biz.wolschon.fileformats.gnucash.GnucashFile#getUnpayedInvoices() */ public Collection<GnucashInvoice> getUnpayedInvoices() throws JAXBException { Collection<GnucashInvoice> retval = new LinkedList<GnucashInvoice>(); for (GnucashInvoice invoice : getInvoices()) { if (invoice.getAmmountUnPayed().isPositive()) { retval.add(invoice); } } return retval; } /** * * @throws JAXBException if we have issues with the XML-backend * @see biz.wolschon.fileformats.gnucash.GnucashFile#getUnpayedInvoicesForCustomer(biz.wolschon.fileformats.gnucash.GnucashCustomer) */ public Collection<GnucashInvoice> getUnpayedInvoicesForCustomer(final GnucashCustomer customer) throws JAXBException { Collection<GnucashInvoice> retval = new LinkedList<GnucashInvoice>(); for (GnucashInvoice invoice : getUnpayedInvoices()) { if (invoice.getJob().getCustomerId().equals(customer.getId())) { retval.add(invoice); } } return retval; } /** * * * @throws JAXBException if we have issues with the XML-backend * @see biz.wolschon.fileformats.gnucash.GnucashFile#getPayedInvoices() */ public Collection<GnucashInvoice> getPayedInvoices() throws JAXBException { Collection<GnucashInvoice> retval = new LinkedList<GnucashInvoice>(); for (GnucashInvoice invoice : getInvoices()) { if (!invoice.getAmmountUnPayed().isPositive()) { retval.add(invoice); } } return retval; } /** * @see #getGnucashFile() */ private File file; /** * @param pCmdtySpace the namespace for pCmdtyId * @param pCmdtyId the currency-name * @return the latest price-quote in the gnucash-file in EURO * @see {@link GnucashFile#getLatestPrice(String, String)} */ public FixedPointNumber getLatestPrice(final String pCmdtySpace, final String pCmdtyId) { return getLatestPrice(pCmdtySpace, pCmdtyId, 0); } /** * the top-level Element of the gnucash-files parsed and checked for * validity by JAXB. */ private GncV2 rootElement; /** * All accounts indexed by their unique id-String. * * @see GnucashAccount * @see GnucashAccountImpl */ protected Map<String, GnucashAccount> accountid2account; /** * All transactions indexed by their unique id-String. * * @see GnucashTransaction * @see GnucashTransactionImpl */ protected Map<String, GnucashTransaction> transactionid2transaction; /** * All invoices indexed by their unique id-String. * * @see GnucashInvoice * @see GnucashInvoiceImpl */ protected Map<String, GnucashInvoice> invoiceid2invoice; /** * All jobs indexed by their unique id-String. * * @see GnucashJob * @see GnucashJobImpl */ protected Map<String, GnucashJob> jobid2job; /** * All customers indexed by their unique id-String. * * @see GnucashCustomer * @see GnucashCustomerImpl */ protected Map<String, GnucashCustomer> customerid2customer; /** * Helper to implement the {@link GnucashObject}-interface * without having the same code twice. */ private GnucashObjectImpl myGnucashObject; /** * @return the underlying JAXB-element */ protected GncV2 getRootElement() { return rootElement; } /** * Set the new root-element and load all accounts, transactions,... from it. * * @param pRootElement * the new root-element * @throws JAXBException if we cannot create a potentially missingg SLOTS-element in XML. */ @SuppressWarnings("unchecked") protected void setRootElement(final GncV2 pRootElement) throws JAXBException { if (pRootElement == null) { throw new IllegalArgumentException("null not allowed for field this.rootElement"); } rootElement = pRootElement; // fill prices loadPriceDatabase(pRootElement); if (pRootElement.getGncBook().getBookSlots() == null) { pRootElement.getGncBook().setBookSlots((new ObjectFactory()).createSlotsType()); } myGnucashObject = new GnucashObjectImpl(pRootElement.getGncBook().getBookSlots(), this); // fill maps accountid2account = new HashMap<String, GnucashAccount>(); for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncAccountType)) { continue; } GncAccountType jwsdpAccount = (GncAccountType) bookElement; try { GnucashAccount account = createAccount(jwsdpAccount); accountid2account.put(account.getId(), account); } catch (RuntimeException e) { LOGGER.error( "[RuntimeException] Problem in " + getClass().getName() + "ignoring illegal Account-Entry with id=" + jwsdpAccount.getActId().getValue(), e); } catch (JAXBException e) { LOGGER.error( "[JAXBException] Problem in " + getClass().getName() + "ignoring illegal Account-Entry with id=" + jwsdpAccount.getActId().getValue(), e); } } invoiceid2invoice = new HashMap<String, GnucashInvoice>(); for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncGncInvoiceType)) { continue; } GncGncInvoiceType jwsdpInvoice = (GncGncInvoiceType) bookElement; GnucashInvoice invoice = createInvoice(jwsdpInvoice); invoiceid2invoice.put(invoice.getId(), invoice); } // invoiceEntries reer to invoices, therefore they must be loaded after // them for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncGncEntryType)) { continue; } GncGncEntryType jwsdpInvoiceEntry = (GncGncEntryType) bookElement; try { createInvoiceEntry(jwsdpInvoiceEntry); } catch (RuntimeException e) { LOGGER.error("[RuntimeException] Problem in " + getClass().getName() + "ignoring illegal Invoice-Entry with id=" + jwsdpInvoiceEntry.getEntryGuid().getValue(), e); } catch (JAXBException e) { LOGGER.error("[JAXBException] Problem in " + getClass().getName() + "ignoring illegal Invoice-Entry with id=" + jwsdpInvoiceEntry.getEntryGuid().getValue(), e); } } // transactions refer to invoices, therefore they must be loaded after // them transactionid2transaction = new HashMap<String, GnucashTransaction>(); for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncTransactionType)) { continue; } GncTransactionType jwsdpTransaction = (GncTransactionType) bookElement; try { GnucashTransactionImpl account = createTransaction(jwsdpTransaction); transactionid2transaction.put(account.getId(), account); for (GnucashTransactionSplit split : account.getSplits()) { /*String accountID = */split.getAccountID(); } } catch (RuntimeException e) { LOGGER.error("[RuntimeException] Problem in " + getClass().getName() + "ignoring illegal Transaction-Entry with id=" + jwsdpTransaction.getTrnId().getValue(), e); } catch (JAXBException e) { LOGGER.error("[JAXBException] Problem in " + getClass().getName() + "ignoring illegal Transaction-Entry with id=" + jwsdpTransaction.getTrnId().getValue(), e); } } customerid2customer = new HashMap<String, GnucashCustomer>(); for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncGncCustomerType)) { continue; } GncGncCustomerType jwsdpCustomer = (GncGncCustomerType) bookElement; try { GnucashCustomerImpl customer = createCustomer(jwsdpCustomer); customerid2customer.put(customer.getId(), customer); } catch (JAXBException e) { LOGGER.error("[JAXBException] Problem in " + getClass().getName(), e); } } jobid2job = new HashMap<String, GnucashJob>(); for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncGncJobType)) { continue; } GncGncJobType jwsdpjob = (GncGncJobType) bookElement; GnucashJobImpl job = createJob(jwsdpjob); String jobID = job.getId(); if (jobID == null) { LOGGER.error("File contains a job w/o an ID. indexing it with the ID ''"); jobID = ""; } jobid2job.put(job.getId(), job); } // check for unknown book-elements for (Iterator iter = pRootElement.getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (bookElement instanceof GncTransactionType) { continue; } if (bookElement instanceof BookElementsGncSchedxaction) { continue; } if (bookElement instanceof BookElementsGncTemplateTransactions) { continue; } if (bookElement instanceof GncAccountType) { continue; } if (bookElement instanceof GncGncInvoiceType) { continue; } if (bookElement instanceof GncGncEntryType) { continue; } if (bookElement instanceof GncGncJobType) { continue; } if (bookElement instanceof GncGncCustomerType) { continue; } if (bookElement instanceof GncCommodityType) { continue; } if (bookElement instanceof BookElementsGncPricedb) { continue; } if (bookElement instanceof BookElementsGncGncTaxTable) { continue; } if (bookElement instanceof BookElementsGncBudget) { continue; } if (bookElement instanceof BookElementsGncGncBillTerm) { continue; } if (bookElement instanceof BookElementsGncGncVendor) { continue; //TODO: create a Java-Class for vendors like we have for customers } if (bookElement instanceof BookElementsGncGncEmployee) { continue; //TODO: create a Java-Class for employees like we have for customers } throw new IllegalArgumentException( "<gnc:book> contains unknown element [" + bookElement.getClass().getName() + "]"); } } /** * Use a heuristic to determine the defaultcurrency-id. * If we cannot find one, we default to EUR.<br/> * Comodity-stace is fixed as "ISO4217" . * @return the default-currencyID to use. */ @SuppressWarnings("unchecked") public String getDefaultCurrencyID() { GncV2 root = getRootElement(); if (root == null) { return "EUR"; } for (Iterator iter = getRootElement().getGncBook().getBookElements().iterator(); iter.hasNext();) { Object bookElement = iter.next(); if (!(bookElement instanceof GncAccountType)) { continue; } GncAccountType jwsdpAccount = (GncAccountType) bookElement; if (jwsdpAccount.getActCommodity() != null && jwsdpAccount.getActCommodity().getCmdtySpace().equals("ISO4217")) { return jwsdpAccount.getActCommodity().getCmdtyId(); } } return "EUR"; } /** * @param pRootElement the root-element of the Gnucash-file */ @SuppressWarnings("unchecked") private void loadPriceDatabase(final GncV2 pRootElement) { boolean noPriceDB = true; List<Object> bookElements = pRootElement.getGncBook().getBookElements(); for (Object bookElement : bookElements) { if (!(bookElement instanceof GncPricedbType)) { continue; } noPriceDB = false; GncPricedbType priceDB = (GncPricedbType) bookElement; if (priceDB.getVersion() != 1) { LOGGER.warn("We know only the format of the price-db 1, " + "the file has version " + priceDB.getVersion() + " prices will not be loaded!"); } else { getCurrencyTable().clear(); getCurrencyTable().setConversionFactor("ISO4217", getDefaultCurrencyID(), new FixedPointNumber(1)); for (Iterator<PriceType> iter = priceDB.getPrice().iterator(); iter.hasNext();) { PriceType price = iter.next(); PriceCommodityType comodity = price.getPriceCommodity(); // check if we already have a latest price for this comodity // (=currency, fund, ...) if (getCurrencyTable().getConversionFactor(comodity.getCmdtySpace(), comodity.getCmdtyId()) != null) { continue; } String baseCurrency = getDefaultCurrencyID(); if (comodity.getCmdtySpace().equals("ISO4217") && comodity.getCmdtyId().equals(baseCurrency)) { LOGGER.warn("Ignoring price-quote for " + baseCurrency + " because " + baseCurrency + " is" + "our base-currency."); continue; } // get the latest price in the file and insert it into // our currency table FixedPointNumber factor = getLatestPrice(comodity.getCmdtySpace(), comodity.getCmdtyId()); if (factor != null) { getCurrencyTable().setConversionFactor(comodity.getCmdtySpace(), comodity.getCmdtyId(), factor); } else { LOGGER.warn("The gnucash-file defines a factor for a comodity '" + comodity.getCmdtySpace() + "' - '" + comodity.getCmdtyId() + "' but has no comodity for it"); } } } } if (noPriceDB) { //case: no priceDB in file getCurrencyTable().clear(); } } /** * @see {@link #getLatestPrice(String, String)} */ protected static final DateFormat PRICEQUOTEDATEFORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZZ"); /** * @param pCmdtySpace the namespace for pCmdtyId * @param pCmdtyId the currency-name * @param depth used for recursion. Allways call with '0' * for aborting recursive quotes (quotes to other then the base- * currency) we abort if the depth reached 6. * @return the latest price-quote in the gnucash-file in the default-currency * @see {@link GnucashFile#getLatestPrice(String, String)} * @see #getDefaultCurrencyID() */ @SuppressWarnings("unchecked") private FixedPointNumber getLatestPrice(final String pCmdtySpace, final String pCmdtyId, final int depth) { if (pCmdtySpace == null) { throw new IllegalArgumentException("null parameter 'pCmdtySpace' " + "given"); } if (pCmdtyId == null) { throw new IllegalArgumentException("null parameter 'pCmdtyId' " + "given"); } Date latestDate = null; FixedPointNumber latestQuote = null; FixedPointNumber factor = new FixedPointNumber(1); // factor is used if the quote is not to our base-currency final int maxRecursionDepth = 5; for (Object bookElement : getRootElement().getGncBook().getBookElements()) { if (!(bookElement instanceof GncPricedbType)) { continue; } GncPricedbType priceDB = (GncPricedbType) bookElement; for (PriceType priceQuote : (List<PriceType>) priceDB.getPrice()) { try { if (priceQuote == null) { LOGGER.warn( "gnucash-file contains null price-quotes" + " there may be a problem with JWSDP"); continue; } if (priceQuote.getPriceCurrency() == null) { LOGGER.warn("gnucash-file contains price-quotes" + " with no currency id='" + priceQuote.getPriceId().getValue() + "'"); continue; } if (priceQuote.getPriceCurrency().getCmdtyId() == null) { LOGGER.warn("gnucash-file contains price-quotes" + " with no currency-id id='" + priceQuote.getPriceId().getValue() + "'"); continue; } if (priceQuote.getPriceCurrency().getCmdtySpace() == null) { LOGGER.warn("gnucash-file contains price-quotes" + " with no currency-namespace id='" + priceQuote.getPriceId().getValue() + "'"); continue; } if (priceQuote.getPriceTime() == null) { LOGGER.warn("gnucash-file contains price-quotes" + " with no timestamp id='" + priceQuote.getPriceId().getValue() + "'"); continue; } if (priceQuote.getPriceValue() == null) { LOGGER.warn("gnucash-file contains price-quotes" + " with no value id='" + priceQuote.getPriceId().getValue() + "'"); continue; } /*if (priceQuote.getPriceCommodity().getCmdtySpace().equals("FUND") && priceQuote.getPriceType() == null) { LOGGER.warn("gnucash-file contains FUND-price-quotes" + " with no type id='" + priceQuote.getPriceId().getValue() + "'"); continue; }*/ if (!priceQuote.getPriceCommodity().getCmdtySpace().equals(pCmdtySpace)) { continue; } if (!priceQuote.getPriceCommodity().getCmdtyId().equals(pCmdtyId)) { continue; } /*if (priceQuote.getPriceCommodity().getCmdtySpace().equals("FUND") && (priceQuote.getPriceType() == null || !priceQuote.getPriceType().equals("last") )) { LOGGER.warn("ignoring FUND-price-quote of unknown type '" + priceQuote.getPriceType() + "' expecting 'last' "); continue; }*/ if (!priceQuote.getPriceCurrency().getCmdtySpace().equals("ISO4217")) { if (depth > maxRecursionDepth) { LOGGER.warn("ignoring price-quote that is not in an" + " ISO4217 -currency but in '" + priceQuote.getPriceCurrency().getCmdtyId()); continue; } factor = getLatestPrice(priceQuote.getPriceCurrency().getCmdtySpace(), priceQuote.getPriceCurrency().getCmdtyId(), depth + 1); } else { if (!priceQuote.getPriceCurrency().getCmdtyId().equals(getDefaultCurrencyID())) { if (depth > maxRecursionDepth) { LOGGER.warn("ignoring price-quote that is not in " + getDefaultCurrencyID() + " " + "but in '" + priceQuote.getPriceCurrency().getCmdtyId()); continue; } factor = getLatestPrice(priceQuote.getPriceCurrency().getCmdtySpace(), priceQuote.getPriceCurrency().getCmdtyId(), depth + 1); } } Date date = PRICEQUOTEDATEFORMAT.parse(priceQuote.getPriceTime().getTsDate()); if (latestDate == null || latestDate.before(date)) { latestDate = date; latestQuote = new FixedPointNumber(priceQuote.getPriceValue()); LOGGER.debug("getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "') converted " + latestQuote + " <= " + priceQuote.getPriceValue()); } } catch (NumberFormatException e) { LOGGER.error("[NumberFormatException] Problem in " + getClass().getName() + ".getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "')! Ignoring a bad price-quote '" + priceQuote + "'", e); } catch (ParseException e) { LOGGER.error("[ParseException] Problem in " + getClass().getName() + ".getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "')! Ignoring a bad price-quote '" + priceQuote + "'", e); } catch (NullPointerException e) { LOGGER.error("[NullPointerException] Problem in " + getClass().getName() + ".getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "')! Ignoring a bad price-quote '" + priceQuote + "'", e); } catch (ArithmeticException e) { LOGGER.error("[ArithmeticException] Problem in " + getClass().getName() + ".getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "')! Ignoring a bad price-quote '" + priceQuote + "'", e); } } } LOGGER.debug(getClass().getName() + ".getLatestPrice(pCmdtySpace='" + pCmdtySpace + "', String pCmdtyId='" + pCmdtyId + "')= " + latestQuote + " from " + latestDate); if (latestQuote == null) { return null; } return factor.multiply(latestQuote); } /** * @param jwsdpAccount * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashAccount to wrap the given jaxb-object. */ protected GnucashAccount createAccount(final GncAccountType jwsdpAccount) throws JAXBException { GnucashAccount account = new GnucashAccountImpl(jwsdpAccount, this); return account; } /** * @param jwsdpInvoice * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashInvoice to wrap the given jaxb-object. */ protected GnucashInvoice createInvoice(final GncGncInvoiceType jwsdpInvoice) { GnucashInvoice invoice = new GnucashInvoiceImpl(jwsdpInvoice, this); return invoice; } /** * @param jwsdpInvoiceEntry * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashInvoiceEntry to wrap the given jaxb-object. * @throws JAXBException on problems with the xml-backend */ protected GnucashInvoiceEntry createInvoiceEntry(final GncGncEntryType jwsdpInvoiceEntry) throws JAXBException { GnucashInvoiceEntry entry = new GnucashInvoiceEntryImpl(jwsdpInvoiceEntry, this); return entry; } /** * @param jwsdpjob * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashJob to wrap the given jaxb-object. */ protected GnucashJobImpl createJob(final GncGncJobType jwsdpjob) { GnucashJobImpl job = new GnucashJobImpl(jwsdpjob, this); return job; } /** * @param jwsdpCustomer * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashCustomer to wrap the given jaxb-object. * @throws JAXBException on problems with the xml-backend */ protected GnucashCustomerImpl createCustomer(final GncGncCustomerType jwsdpCustomer) throws JAXBException { GnucashCustomerImpl customer = new GnucashCustomerImpl(jwsdpCustomer, this); return customer; } /** * @param jwsdpTransaction * the JWSDP-peer (parsed xml-element) to fill our object with * @return the new GnucashTransaction to wrap the given jaxb-object. */ protected GnucashTransactionImpl createTransaction(final GncTransactionType jwsdpTransaction) throws JAXBException { GnucashTransactionImpl account = new GnucashTransactionImpl(jwsdpTransaction, this); return account; } /** * @param pFile * the file to load and initialize from * @throws IOException * on low level reading-errors (FileNotFoundException if not * found) * @throws JAXBException * on jaxb-errors (invalid xml,...) * @see #loadFile(File) */ public GnucashFileImpl(final File pFile) throws IOException, JAXBException { super(); loadFile(pFile); } /** * {@inheritDoc} */ @Override public File getFile() { return file; } /** * Internal method, just sets this.file . * * @param pFile * the file loaded */ protected void setFile(final File pFile) { if (pFile == null) { throw new IllegalArgumentException("null not allowed for field this.file"); } file = pFile; } /** * loads the file and calls setRootElement. * * @param pFile * the file to read * @throws IOException * on low level reading-errors (FileNotFoundException if not * found) * @throws JAXBException * on jaxb-errors (invalid xml,...) * @see #setRootElement(GncV2) */ protected void loadFile(final File pFile) throws IOException, JAXBException { long start = System.currentTimeMillis(); if (pFile == null) { throw new IllegalArgumentException("null not allowed for field this.file"); } if (!pFile.exists()) { throw new IllegalArgumentException("Given file '" + pFile.getAbsolutePath() + "' does not exist!"); } setFile(pFile); InputStream in = new FileInputStream(pFile); if (pFile.getName().endsWith(".gz")) { in = new BufferedInputStream(in); in = new GZIPInputStream(in); } else { // determine if it's gzipped by the magic bytes byte[] magic = new byte[2]; in.read(magic); in.close(); in = new FileInputStream(pFile); in = new BufferedInputStream(in); if (magic[0] == 31 && magic[1] == -117) { in = new GZIPInputStream(in); } } // reader = new NamespaceRemovererReader(new FileReader(file)); /* V1 NamespaceRemovererReader reader = new NamespaceRemovererReader( new EuroConverterReader(new InputStreamReader( in, "ISO8859-15"))); */ // works with V1.9 and V2.0 NamespaceRemovererReader reader = new NamespaceRemovererReader(new InputStreamReader(in, "utf-8")); try { try { JAXBContext myContext = getJAXBContext(); Unmarshaller unmarshaller = myContext.createUnmarshaller(); GncV2 o = (GncV2) unmarshaller.unmarshal(new InputSource(new BufferedReader(reader))); long start2 = System.currentTimeMillis(); setRootElement(o); long end = System.currentTimeMillis(); LOGGER.info("GnucashFileImpl.loadFile took " + (end - start) + " ms (total) " + (start2 - start) + " ms (jaxb-loading)" + (end - start2) + " ms (building facades)"); } finally { reader.close(); } } catch (JAXBException ex) { // output what has been reat to far if (reader instanceof NamespaceRemovererReader) { try { NamespaceRemovererReader nsr = reader; int pos = (int) nsr.getPosition(); char[] c = new char[pos]; NamespaceRemovererReader reader2 = new NamespaceRemovererReader(new EuroConverterReader( new InputStreamReader(new FileInputStream(pFile), "ISO8859-15"))); try { reader2.read(c); } finally { reader2.close(); } System.err.println("reat so far:"); String s = new String(c); System.err.println(s.substring(Math.max(0, s.length() - 500))); } catch (Throwable x) { // ignore } } if (reader instanceof NamespaceRemovererReader && (reader).debugLastReatLength > 0) { NamespaceRemovererReader nsr = reader; System.err.println( "last reat chars: '" + new String(nsr.debugLastTeat, 0, nsr.debugLastReatLength) + "'"); } else { System.err.println("last reat chars: none!"); } throw ex; } } /** * @see #getObjectFactory() */ private volatile ObjectFactory myJAXBFactory; /** * @return the jaxb object-factory used to create new peer-objects to extend * this * @throws JAXBException * o jaxb-errors */ protected ObjectFactory getObjectFactory() throws JAXBException { if (myJAXBFactory == null) { myJAXBFactory = new ObjectFactory(); } return myJAXBFactory; } /** * @see #getJAXBContext() */ private volatile JAXBContext myJAXBContext; /** * @return the JAXB-context * @throws JAXBException * on jaxb-errors */ protected JAXBContext getJAXBContext() throws JAXBException { if (myJAXBContext == null) { myJAXBContext = DefaultJAXBContextImpl.newInstance( "biz.wolschon.fileformats.gnucash.jwsdpimpl.generated", this.getClass().getClassLoader()); } return myJAXBContext; } /** * * @param type * the type-string to look for * @return the count-data saved in the xml-file */ protected GncCountDataType findCountDataByType(final String type) { for (Iterator iter = getRootElement().getGncBook().getGncCountData().iterator(); iter.hasNext();) { GncCountDataType count = (GncCountDataType) iter.next(); if (count.getCdType().equals(type)) { return count; } } return null; } /** * * @return the number of transactions */ protected int getTransactionCount() { GncCountDataType count = findCountDataByType("transaction"); return count.getValue(); } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getAccountByID(java.lang.String) */ public GnucashAccount getAccountByID(final String id) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } GnucashAccount retval = accountid2account.get(id); if (retval == null) { System.err .println("No Account with id '" + id + "'. We know " + accountid2account.size() + " accounts."); } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getCustomerByID(java.lang.String) */ public GnucashCustomer getCustomerByID(final String id) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } GnucashCustomer retval = customerid2customer.get(id); if (retval == null) { LOGGER.warn("No Customer with id '" + id + "'. We know " + customerid2customer.size() + " accounts."); } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getCustomerByName(java.lang.String) */ public GnucashCustomer getCustomerByName(final String name) { if (accountid2account == null) { throw new IllegalStateException("no root-element loaded"); } for (GnucashCustomer customer : getCustomers()) { if (customer.getName().equals(name)) { return customer; } } return null; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getCustomers() */ public Collection<GnucashCustomer> getCustomers() { return customerid2customer.values(); } /** * * @param customer the customer to look for. * @return all jobs that have this customer, never null */ public Collection getJobsByCustomer(final GnucashCustomer customer) { if (jobid2job == null) { throw new IllegalStateException("no root-element loaded"); } Collection retval = new LinkedList(); for (Object element : jobid2job.values()) { GnucashJob job = (GnucashJob) element; if (job.getCustomerId().equals(customer.getId())) { retval.add(job); } } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getJobByID(java.lang.String) */ public GnucashJob getJobByID(final String id) { if (jobid2job == null) { throw new IllegalStateException("no root-element loaded"); } GnucashJob retval = jobid2job.get(id); if (retval == null) { LOGGER.warn("No Job with id '" + id + "'. We know " + customerid2customer.size() + " accounts."); } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getJobs() */ public Collection<GnucashJob> getJobs() { if (jobid2job == null) { throw new IllegalStateException("no root-element loaded"); } return jobid2job.values(); } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getTransactionByID(java.lang.String) */ public GnucashTransaction getTransactionByID(final String id) { if (transactionid2transaction == null) { throw new IllegalStateException("no root-element loaded"); } GnucashTransaction retval = transactionid2transaction.get(id); if (retval == null) { LOGGER.warn( "No Transaction with id '" + id + "'. We know " + customerid2customer.size() + " accounts."); } return retval; } /** * * @see biz.wolschon.fileformats.gnucash.GnucashFile#getTransactions() */ public Collection<? extends GnucashTransaction> getTransactions() { if (transactionid2transaction == null) { throw new IllegalStateException("no root-element loaded"); } return Collections.unmodifiableCollection(transactionid2transaction.values()); } /** * * created: 13.05.2005 <br/> * * * replaces ':' in tag-names and attribute-names by '_' . * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a> */ public static class NamespaceRemovererReader extends Reader { /** * How much we have reat. */ private long position = 0; /** * @return How much we have reat. */ public long getPosition() { return position; } /** * @param pInput what to read from. */ public NamespaceRemovererReader(final Reader pInput) { super(); input = pInput; } /** * * @return What to read from. */ public Reader getInput() { return input; } /** * * @param newInput What to read from. */ public void setInput(final Reader newInput) { if (newInput == null) { throw new IllegalArgumentException("null not allowed for field this.input"); } input = newInput; } /** * What to read from. */ private Reader input; /** * true if we are in a quotation and thus * shall not remove any namespaces. */ private boolean isInQuotation = false; /** * true if we are in a quotation and thus * shall remove any namespaces. */ private boolean isInTag = false; /** * * @see java.io.Reader#close() */ @Override public void close() throws IOException { input.close(); } /** * For debugging. */ public char[] debugLastTeat = new char[255]; /** * For debugging. */ public int debugLastReatLength = -1; /** * Log the last chunk of bytes reat for debugging-purposes. * * @param cbuf the data * @param off where to start in cbuf * @param reat how much */ private void logReatBytes(final char[] cbuf, final int off, final int reat) { debugLastReatLength = Math.min(debugLastTeat.length, reat); try { System.arraycopy(cbuf, off, debugLastTeat, 0, debugLastTeat.length); } catch (Exception e) { e.printStackTrace(); LOGGER.debug("debugLastReatLength=" + debugLastReatLength + "\n" + "off=" + off + "\n" + "reat=" + reat + "\n" + "cbuf.length=" + cbuf.length + "\n" + "debugLastTeat.length=" + debugLastTeat.length + "\n"); } } /** * * @see java.io.Reader#read(char[], int, int) */ @Override public int read(final char[] cbuf, final int off, final int len) throws IOException { int reat = input.read(cbuf, off, len); logReatBytes(cbuf, off, reat); for (int i = off; i < off + reat; i++) { position++; if (isInTag && (cbuf[i] == '"' || cbuf[i] == '\'')) { toggleIsInQuotation(); } else if (cbuf[i] == '<' && !isInQuotation) { isInTag = true; } else if (cbuf[i] == '>' && !isInQuotation) { isInTag = false; } else if (cbuf[i] == ':' && isInTag && !isInQuotation) { cbuf[i] = '_'; } } return reat; } /** * */ private void toggleIsInQuotation() { if (isInQuotation) { isInQuotation = false; } else { isInQuotation = true; } } } /** * * created: 13.05.2005 <br/> * * * replaces ¤ by the euro-sign . * @author <a href="mailto:Marcus@Wolschon.biz">Marcus Wolschon</a> */ public static class EuroConverterReader extends Reader { /** * This is "¤".length . */ private static final int REPLACESTRINGLENGTH = 5; /** * @param pInput Where to read from. */ public EuroConverterReader(final Reader pInput) { super(); input = pInput; } /** * * @return Where to read from. */ public Reader getInput() { return input; } /** * * @param newInput Where to read from. */ public void setInput(Reader newInput) { if (newInput == null) { throw new IllegalArgumentException("null not allowed for field this.input"); } input = newInput; } /** * Where to read from. */ private Reader input; /** * * @see java.io.Reader#close() */ @Override public void close() throws IOException { input.close(); } /** * * @see java.io.Reader#read(char[], int, int) */ @Override public int read(final char[] cbuf, final int off, final int len) throws IOException { int reat = input.read(cbuf, off, len); // this does not work if the euro-sign is wrapped around the // edge of 2 read-call buffers int state = 0; for (int i = off; i < off + reat; i++) { switch (state) { case 0: { if (cbuf[i] == '&') { state++; } break; } case 1: { if (cbuf[i] == '#') { state++; } else { state = 0; } break; } case 2: { if (cbuf[i] == '1') { state++; } else { state = 0; } break; } case REPLACESTRINGLENGTH - 2: { if (cbuf[i] == '6') { state++; } else { state = 0; } break; } case REPLACESTRINGLENGTH - 1: { if (cbuf[i] == '4') { state++; } else { state = 0; } break; } case REPLACESTRINGLENGTH: { if (cbuf[i] == ';') { // found it!!! cbuf[i - REPLACESTRINGLENGTH] = ''; if (i != reat - 1) { System.arraycopy(cbuf, (i + 1), cbuf, (i - (REPLACESTRINGLENGTH - 1)), (reat - i - 1)); } int reat2 = input.read(cbuf, reat - REPLACESTRINGLENGTH, REPLACESTRINGLENGTH); if (reat2 != REPLACESTRINGLENGTH) { reat -= (REPLACESTRINGLENGTH - reat2); } i -= (REPLACESTRINGLENGTH - 1); state = 0; } else { state = 0; } break; } default: } } return reat; }; } /** * {@inheritDoc} */ @Override public GnucashFile getGnucashFile() { return this; } /** * {@inheritDoc} */ @Override public String getUserDefinedAttribute(final String aName) { return myGnucashObject.getUserDefinedAttribute(aName); } /** * {@inheritDoc} */ @Override public Collection<String> getUserDefinedAttributeKeys() { return myGnucashObject.getUserDefinedAttributeKeys(); } }