/****************************************************************************** * 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. 
	 * 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
 *  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
			 *