/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.impexp; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigDecimal; import java.sql.Timestamp; import java.text.ParseException; import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.StringTokenizer; import java.util.TimeZone; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.compiere.model.MBankStatementLoader; import org.compiere.util.Env; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * Parser for OFX bank statements. *

* This class is a parser for OFX bankstatements. OFX versions from 102 to 202 * and MS-Money OFC message sets are supported.
* Only fully XML compliant OFX data is supported. Files that are not XML * compliant, e.g. OFX versions older then 200, will be preprocessed by * the OFX1ToXML class before parsing.
* This class should be extended by a class that obtains the data to be parsed * for example from a file, or using HTTP. * * @author Eldir Tomassen * @version $Id: OFXBankStatementHandler.java,v 1.3 2006/07/30 00:51:05 jjanke Exp $ */ public abstract class OFXBankStatementHandler extends DefaultHandler { protected MBankStatementLoader m_controller; protected StringBuffer m_errorMessage; protected StringBuffer m_errorDescription; protected BufferedReader m_reader = null; protected SAXParser m_parser; protected boolean m_success = false; //private boolean m_valid = false; protected StatementLine m_line; protected String m_routingNo = "0"; protected String m_bankAccountNo = null; protected String m_currency = null; protected int HEADER_SIZE = 20; protected boolean m_test = false; protected Timestamp m_dateLastRun = null; protected Timestamp m_statementDate = null; protected StringBuffer m_valueBuffer = new StringBuffer(); /** XML OFX Tag */ public static final String XML_OFX_TAG = "OFX"; /** XML SIGNONMSGSRSV2 Tag */ public static final String XML_SIGNONMSGSRSV2_TAG = "SIGNONMSGSRSV2"; /** XML SIGNONMSGSRSV1 Tag */ public static final String XML_SIGNONMSGSRSV1_TAG = "SIGNONMSGSRSV1"; /** Record-response aggregate */ public static final String XML_SONRS_TAG = "SONRS"; /** Date and time of the server response */ public static final String XML_DTSERVER_TAG = "DTSERVER"; /** Use USERKEY instead of USERID and USEPASS */ public static final String XML_USERKEY_TAG = "USERKEY"; /** Date and time that USERKEY expires */ public static final String XML_TSKEYEXPIRE_TAG = "TSKEYEXPIRE"; /** Language */ public static final String XML_LANGUAGE_TAG = "LANGUAGE"; /** Date and rime last update to profile information*/ public static final String XML_DTPROFUP_TAG = "DTPROFUP"; /** Status aggregate */ public static final String XML_STATUS_TAG = "STATUS"; /** Statement-response aggregate */ public static final String XML_STMTRS_TAG = "STMTRS"; /** XML CURDEF Tag */ public static final String XML_CURDEF_TAG = "CURDEF"; /** Account-from aggregate */ public static final String XML_BANKACCTFROM_TAG = "BANKACCTFROM"; /** Bank identifier */ public static final String XML_BANKID_TAG = "BANKID"; /** Branch identifier */ public static final String XML_BRANCHID_TAG = "BRANCHID"; /** XML ACCTID Tag */ public static final String XML_ACCTID_TAG = "ACCTID"; /** Type of account */ public static final String XML_ACCTTYPE_TAG = "ACCTTYPE"; /** Type of account */ public static final String XML_ACCTTYPE2_TAG = "ACCTTYPE2"; /** Checksum */ public static final String XML_ACCTKEY_TAG = "ACCTKEY"; /** XML BANKTRANLIST Tag */ public static final String XML_BANKTRANLIST_TAG = "BANKTRANLIST"; /** XML DTSTART Tag */ public static final String XML_DTSTART_TAG = "DTSTART"; /** XML DTEND Tag */ public static final String XML_DTEND_TAG = "DTEND"; /** XML STMTTRN Tag */ public static final String XML_STMTTRN_TAG = "STMTTRN"; /** XML TRNTYPE Tag */ public static final String XML_TRNTYPE_TAG = "TRNTYPE"; /** XML TRNAMT Tag */ public static final String XML_TRNAMT_TAG = "TRNAMT"; /** Transaction date */ public static final String XML_DTPOSTED_TAG = "DTPOSTED"; /** Effective date */ public static final String XML_DTAVAIL_TAG = "DTAVAIL"; /** XML FITID Tag */ public static final String XML_FITID_TAG = "FITID"; /** XML CHECKNUM Tag */ public static final String XML_CHECKNUM_TAG = "CHECKNUM"; /** XML CHKNUM Tag (MS-Money OFC) */ public static final String XML_CHKNUM_TAG = "CHKNUM"; /** XML REFNUM Tag */ public static final String XML_REFNUM_TAG = "REFNUM"; /** Transaction Memo */ public static final String XML_MEMO_TAG = "MEMO"; /** XML NAME Tag */ public static final String XML_NAME_TAG = "NAME"; /** XML PAYEEID Tag */ public static final String XML_PAYEEID_TAG = "PAYEEID"; /** TXML PAYEE Tag */ public static final String XML_PAYEE_TAG = "PAYEE"; /** XML LEDGERBAL Tag */ public static final String XML_LEDGERBAL_TAG = "LEDGERBAL"; /** XML BALAMT Tag */ public static final String XML_BALAMT_TAG = "BALAMT"; /** XML DTASOF Tag */ public static final String XML_DTASOF_TAG = "DTASOF"; /** XML AVAILBAL Tag */ public static final String XML_AVAILBAL_TAG = "AVAILBAL"; /** XML MKTGINFO Tag */ public static final String XML_MKTGINFO_TAG = "MKTGINFO"; /** * Initialize the loader * @param controller Reference to the BankStatementLoaderController * @return Initialized successfully */ protected boolean init(MBankStatementLoader controller) { boolean result = false; if (controller == null) { m_errorMessage = new StringBuffer("ErrorInitializingParser"); m_errorDescription = new StringBuffer("ImportController is a null reference"); return result; } this.m_controller = controller; try { SAXParserFactory factory = SAXParserFactory.newInstance(); m_parser = factory.newSAXParser(); result = true; } catch(ParserConfigurationException e) { m_errorMessage = new StringBuffer("ErrorInitializingParser"); m_errorDescription = new StringBuffer("Unable to configure SAX parser: ").append(e.getMessage()); } catch(SAXException e) { m_errorMessage = new StringBuffer("ErrorInitializingParser"); m_errorDescription = new StringBuffer("Unable to initialize SAX parser: ").append(e.getMessage()); } if (!result) closeBufferedReader(); return result; } // init /** * Attach OFX input source, detect whether we are dealing with OFX1 * (SGML) or OFX2 (XML). In case of OFX1, process the data to create * valid XML. * @param is Reference to the BankStatementLoaderController * @return true if input is valid OFX data */ protected boolean attachInput(InputStream is) { boolean isOfx1 = true; boolean result = false; try { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); reader.mark(HEADER_SIZE + 20000); StringBuilder header = new StringBuilder(); for (int i = 0; i < HEADER_SIZE; i++) { header.append(reader.readLine()); } if ((header.indexOf(" * This method will be invoked from ImportController. * @return load success */ public boolean loadLines() { boolean result = false; try { m_parser.parse(new InputSource(m_reader), this); result = true; m_success = true; } catch(SAXException e) { m_errorMessage = new StringBuffer("ErrorParsingData"); m_errorDescription = new StringBuffer(e.getMessage()); } catch(IOException e) { m_errorMessage = new StringBuffer("ErrorReadingData"); m_errorDescription = new StringBuffer(e.getMessage()); } finally { closeBufferedReader(); } return result; } // loadLines /** * Close {@link #m_reader} */ private void closeBufferedReader() { if (m_reader != null) try { m_reader.close(); } catch (IOException e) { e.printStackTrace(); } } /** * @return last run date */ public Timestamp getDateLastRun() { return m_dateLastRun; } /** * @return routing number */ public String getRoutingNo() { return m_line.routingNo; } /** * @return line bank account number */ public String getBankAccountNo() { return m_line.bankAccountNo; } /** * @return IBAN */ public String getIBAN() { return null; } /** * @return line statement reference */ public String getStatementReference() { return m_line.statementReference; } /** * @return statement date */ public Timestamp getStatementDate() { return m_statementDate; } /** * @return line reference */ public String getReference() { return m_line.reference; } /** * @return line statement date */ public Timestamp getStatementLineDate() { return m_line.statementLineDate; } /** * @return line valuta date */ public Timestamp getValutaDate() { return m_line.valutaDate; } /** * @return line trx type */ public String getTrxType() { return m_line.trxType; } /** * @return true if line is reversal */ public boolean getIsReversal() { return m_line.isReversal; } /** * @return line currency */ public String getCurrency() { return m_line.currency; } /** * @return line statement amount */ public BigDecimal getStmtAmt() { return m_line.stmtAmt; } /** * @return Line Transaction Amount */ public BigDecimal getTrxAmt() { /* assume total amount = transaction amount * todo: detect interest & charge amount */ return m_line.stmtAmt; } /** * @return Interest Amount */ public BigDecimal getInterestAmt() { return Env.ZERO; } /** * @return line memo */ public String getMemo() { return m_line.memo; } /** * @return line charge name */ public String getChargeName() { return m_line.chargeName; } /** * @return line charge amount */ public BigDecimal getChargeAmt() { return m_line.chargeAmt; } /** * @return line transaction id */ public String getTrxID() { return m_line.trxID; } /** * @return line payee account number */ public String getPayeeAccountNo() { return m_line.payeeAccountNo; } /** * @return line payee name */ public String getPayeeName() { return m_line.payeeName; } /** * @return line check number */ public String getCheckNo() { return m_line.checkNo; } /** * New XML element detected. The XML nesting * structure is saved on the m_context stack. * @param uri String * @param localName String * @param qName String * @param attributes Attributes * @throws org.xml.sax.SAXException * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes) */ @Override public void startElement (String uri, String localName, String qName, Attributes attributes) throws org.xml.sax.SAXException { boolean validOFX = true; /* * Currently no validating is being done, valid OFX structure is assumed. */ if (!validOFX) { m_errorDescription = new StringBuffer("Invalid OFX syntax: ").append(qName); throw new SAXException("Invalid OFX syntax: " + qName); } if (qName.equals(XML_STMTTRN_TAG)) { m_line = new StatementLine(m_routingNo, m_bankAccountNo, m_currency); } m_valueBuffer=new StringBuffer(); } // startElement /** * Characters read from XML are assigned to a variable, based on the current m_context. * No checks are being done, it is assumed that the context is correct. * @param ch char[] * @param start int * @param length int * @throws SAXException * @see org.xml.sax.ContentHandler#characters(char[], int, int) */ @Override public void characters (char ch[], int start, int length) throws SAXException { m_valueBuffer.append(ch,start,length); } // characters /** * Check for valid XML structure. (all tags are properly ended). * The statements are passed to ImportController when the statement end (</STMTTRN>) * is detected. * @param uri String * @param localName String * @param qName String * @throws SAXException * @see org.xml.sax.ContentHandler#endElement(String, String, String) */ @Override public void endElement (String uri, String localName, String qName) throws SAXException { String XML_TAG=qName; String value=m_valueBuffer.toString(); try { //Read statment level data /* * Default currency for this set of statement lines * --- */ if (XML_TAG.equals(XML_CURDEF_TAG)) { m_currency = value; } /* Routing Number (or SWIFT Code) for this set of statement lines * ---- */ else if (XML_TAG.equals(XML_BANKID_TAG)) { m_routingNo = value; } /* * Bank Account Number for this set of bank statement lines * ---- */ else if (XML_TAG.equals(XML_ACCTID_TAG)) { m_bankAccountNo = value; } /* * Last date for this set of statement lines * This is the date that should be specified as the * for the next batch of statement lines, in order not to miss any * transactions. * ---- */ else if (XML_TAG.equals(XML_DTEND_TAG)) { m_dateLastRun = parseOfxDate(value); } /* * * ---- */ else if (XML_TAG.equals(XML_DTASOF_TAG)) { m_statementDate = parseOfxDate(value); } //Read statement line level data /* * Transaction type, e.g. DEBIT, CREDIT, SRVCHG * ---- */ else if (XML_TAG.equals(XML_TRNTYPE_TAG)) { m_line.trxType = value; } /* * Statement line date * ---- */ else if (XML_TAG.equals(XML_DTPOSTED_TAG)) { m_line.statementLineDate = parseOfxDate(value); } /* * Valuta date * ---- */ else if (XML_TAG.equals(XML_DTAVAIL_TAG)) { m_line.valutaDate = parseOfxDate(value); } /* * Total statement line amount * ---- */ else if (XML_TAG.equals(XML_TRNAMT_TAG)) { m_line.stmtAmt = new BigDecimal(value); } /* * Transaction Identification * ---- */ else if (XML_TAG.equals(XML_FITID_TAG)) { m_line.trxID = value; } /* * Check number for check transactions * CHECKNUM for generic OFX, CHKNUM for MS-Money OFC * ---- */ else if ((XML_TAG.equals(XML_CHECKNUM_TAG)) || (XML_TAG.equals(XML_CHKNUM_TAG))) { m_line.checkNo = value; } /* * Statement line reference * Additional transaction reference information * ---- */ else if (XML_TAG.equals(XML_REFNUM_TAG)) { m_line.reference = value; } /* * Transaction memo * ---- */ else if (XML_TAG.equals(XML_MEMO_TAG)) { m_line.memo = value; } /* * Payee Name * ---- */ else if (XML_TAG.equals(XML_NAME_TAG)) { m_line.payeeName = value; } } catch(Exception e) { m_errorDescription = new StringBuffer("Invalid data: ").append(value).append(" <-> ").append(e.getMessage()); throw new SAXException("Invalid data: " + value); } if (qName.equals(XML_STMTTRN_TAG)) { if (!m_test) { if (!m_controller.saveLine()) { m_errorMessage = new StringBuffer().append(m_controller.getErrorMessage()); m_errorDescription = new StringBuffer(m_controller.getErrorDescription()); throw new SAXException(m_errorMessage.toString()); } } } } // endElement /** * source: https://github.com/stoicflame/ofx4j/blob/a604e4f6ffefea61403434cf853bbd1b20740386/src/main/java/com/webcohesion/ofx4j/io/DefaultStringConversion.java *

* Parse the timezone offset of the form [HOURS_OFF_GMT:TZ_ID] * * @param tzoffset The offset pattern. * @return The timezone. */ protected TimeZone parseTimeZone(String tzoffset) { StringTokenizer tokenizer = new StringTokenizer(tzoffset, "[]:"); TimeZone tz = GMT_TIME_ZONE; if (tokenizer.hasMoreTokens()) { String hoursOff = tokenizer.nextToken(); tz = TimeZone.getTimeZone("GMT" + hoursOff); } return tz; } public static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT"); public static final int DATE_FORMAT_LENGTH = "yyyyMMddHHmmss.SSS".length(); public static final int TIME_FORMAT_LENGTH = "HHmmss.SSS".length(); /** * Parses a date according to OFX. * * @param value The value of the date. * @return The date value. */ protected Date parseDate(String value) { char[] parseableDate = new char[DATE_FORMAT_LENGTH]; Arrays.fill(parseableDate, '0'); parseableDate[parseableDate.length - 4] = '.'; char[] valueChars = value.toCharArray(); int index = 0; while (index < valueChars.length && valueChars[index] != '[') { if (index < DATE_FORMAT_LENGTH) { parseableDate[index] = valueChars[index]; } index++; } int year = Integer.parseInt(new String(parseableDate, 0, 4)); int month = Integer.parseInt(new String(parseableDate, 4, 2)) - 1; //java month numberss are zero-based int day = Integer.parseInt(new String(parseableDate, 6, 2)); int hour = Integer.parseInt(new String(parseableDate, 8, 2)); int minute = Integer.parseInt(new String(parseableDate, 10, 2)); int second = Integer.parseInt(new String(parseableDate, 12, 2)); int milli = Integer.parseInt(new String(parseableDate, 15, 3)); //set up a new calendar at zero, then set all the fields. GregorianCalendar calendar = new GregorianCalendar(year, month, day, hour, minute, second); if (index < valueChars.length && valueChars[index] == '[') { String tzoffset = value.substring(index); calendar.setTimeZone(parseTimeZone(tzoffset)); } else { calendar.setTimeZone(GMT_TIME_ZONE); } calendar.add(GregorianCalendar.MILLISECOND, milli); return calendar.getTime(); } /** * Parse OFX date * @param value String * @return Timestamp * @throws ParseException */ private Timestamp parseOfxDate(String value) throws ParseException { try { return new Timestamp (parseDate(value).getTime()); } catch(Exception e) { throw new ParseException("Error parsing date: " + value, 0); } } //parseOfxDate /** * @return last error message */ public String getLastErrorMessage() { return m_errorMessage.toString(); } /** * @return last error description */ public String getLastErrorDescription() { return m_errorDescription.toString(); } /** * @author ET * @version $Id: OFXBankStatementHandler.java,v 1.3 2006/07/30 00:51:05 jjanke Exp $ */ protected static class StatementLine { protected String routingNo = null; protected String bankAccountNo = null; protected String statementReference = null; protected Timestamp statementLineDate = null; protected String reference = null; protected Timestamp valutaDate; protected String trxType = null; protected boolean isReversal = false; protected String currency = null; protected BigDecimal stmtAmt = null; protected String memo = null; protected String chargeName = null; protected BigDecimal chargeAmt = null; protected String payeeAccountNo = null; protected String payeeName = null; protected String trxID = null; protected String checkNo = null; /** * Constructor for StatementLine * @param RoutingNo String * @param BankAccountNo String * @param Currency String */ public StatementLine(String RoutingNo, String BankAccountNo, String Currency) { bankAccountNo = BankAccountNo; routingNo = RoutingNo; currency = Currency; } } } //OFXBankStatementHandler