mirror of
https://github.com/aclindsa/ofxgo.git
synced 2025-07-01 11:48:38 -04:00
Reorganization
This commit is contained in:
296
response.go
Normal file
296
response.go
Normal file
@ -0,0 +1,296 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Version string // String for OFX header, defaults to 203
|
||||
Signon SignonResponse //<SIGNONMSGSETV1>
|
||||
Signup []Message //<SIGNUPMSGSETV1>
|
||||
Banking []Message //<BANKMSGSETV1>
|
||||
//<CREDITCARDMSGSETV1>
|
||||
//<LOANMSGSETV1>
|
||||
//<INVSTMTMSGSETV1>
|
||||
//<INTERXFERMSGSETV1>
|
||||
//<WIREXFERMSGSETV1>
|
||||
//<BILLPAYMSGSETV1>
|
||||
//<EMAILMSGSETV1>
|
||||
//<SECLISTMSGSETV1>
|
||||
//<PRESDIRMSGSETV1>
|
||||
//<PRESDLVMSGSETV1>
|
||||
Profile []Message //<PROFMSGSETV1>
|
||||
//<IMAGEMSGSETV1>
|
||||
}
|
||||
|
||||
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
||||
var seenHeader, seenVersion bool = false, false
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// r.ReadString leaves the '\n' on the end...
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if len(line) == 0 {
|
||||
if seenHeader {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
header := strings.SplitN(line, ":", 2)
|
||||
if header == nil || len(header) != 2 {
|
||||
return errors.New("OFX headers malformed")
|
||||
}
|
||||
|
||||
switch header[0] {
|
||||
case "OFXHEADER":
|
||||
if header[1] != "100" {
|
||||
return errors.New("OFXHEADER is not 100")
|
||||
}
|
||||
seenHeader = true
|
||||
case "DATA":
|
||||
if header[1] != "OFXSGML" {
|
||||
return errors.New("OFX DATA header does not contain OFXSGML")
|
||||
}
|
||||
case "VERSION":
|
||||
switch header[1] {
|
||||
case "102", "103", "151", "160":
|
||||
seenVersion = true
|
||||
or.Version = header[1]
|
||||
default:
|
||||
return errors.New("Invalid OFX VERSION in header")
|
||||
}
|
||||
case "SECURITY":
|
||||
if header[1] != "NONE" {
|
||||
return errors.New("OFX SECURITY header not NONE")
|
||||
}
|
||||
case "COMPRESSION":
|
||||
if header[1] != "NONE" {
|
||||
return errors.New("OFX COMPRESSION header not NONE")
|
||||
}
|
||||
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
|
||||
// TODO check/handle these headers?
|
||||
default:
|
||||
return errors.New("Invalid OFX header: " + header[0])
|
||||
}
|
||||
}
|
||||
|
||||
if !seenVersion {
|
||||
return errors.New("OFX VERSION header missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *Response) readXMLHeaders(decoder *xml.Decoder) error {
|
||||
var tok xml.Token
|
||||
tok, err := nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if xmlElem, ok := tok.(xml.ProcInst); !ok || xmlElem.Target != "xml" {
|
||||
return errors.New("Missing xml processing instruction")
|
||||
}
|
||||
|
||||
// parse the OFX header
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ofxElem, ok := tok.(xml.ProcInst); ok && ofxElem.Target == "OFX" {
|
||||
var seenHeader, seenVersion bool = false, false
|
||||
|
||||
headers := bytes.TrimSpace(ofxElem.Inst)
|
||||
for len(headers) > 0 {
|
||||
tmp := bytes.SplitN(headers, []byte("=\""), 2)
|
||||
if len(tmp) != 2 {
|
||||
return errors.New("Malformed OFX header")
|
||||
}
|
||||
header := string(tmp[0])
|
||||
headers = tmp[1]
|
||||
tmp = bytes.SplitN(headers, []byte("\""), 2)
|
||||
if len(tmp) != 2 {
|
||||
return errors.New("Malformed OFX header")
|
||||
}
|
||||
value := string(tmp[0])
|
||||
headers = bytes.TrimSpace(tmp[1])
|
||||
|
||||
switch header {
|
||||
case "OFXHEADER":
|
||||
if value != "200" {
|
||||
return errors.New("OFXHEADER is not 200")
|
||||
}
|
||||
seenHeader = true
|
||||
case "VERSION":
|
||||
switch value {
|
||||
case "200", "201", "202", "203", "210", "211", "220":
|
||||
seenVersion = true
|
||||
or.Version = value
|
||||
default:
|
||||
return errors.New("Invalid OFX VERSION in header")
|
||||
}
|
||||
case "SECURITY":
|
||||
if value != "NONE" {
|
||||
return errors.New("OFX SECURITY header not NONE")
|
||||
}
|
||||
case "OLDFILEUID", "NEWFILEUID":
|
||||
// TODO check/handle these headers?
|
||||
default:
|
||||
return errors.New("Invalid OFX header: " + header)
|
||||
}
|
||||
}
|
||||
|
||||
if !seenHeader {
|
||||
return errors.New("OFXHEADER version missing")
|
||||
}
|
||||
if !seenVersion {
|
||||
return errors.New("OFX VERSION header missing")
|
||||
}
|
||||
|
||||
} else {
|
||||
return errors.New("Missing xml 'OFX' processing instruction")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Number of bytes of response to read when attempting to figure out whether
|
||||
// we're using OFX or SGML
|
||||
const guessVersionCheckBytes = 1024
|
||||
|
||||
// Defaults to XML if it can't determine the version or if there is any
|
||||
// ambiguity
|
||||
func guessVersion(r *bufio.Reader) (bool, error) {
|
||||
b, _ := r.Peek(guessVersionCheckBytes)
|
||||
if b == nil {
|
||||
return false, errors.New("Failed to read OFX header")
|
||||
}
|
||||
sgmlIndex := bytes.Index(b, []byte("OFXHEADER:"))
|
||||
xmlIndex := bytes.Index(b, []byte("OFXHEADER="))
|
||||
if sgmlIndex < 0 {
|
||||
return true, nil
|
||||
} else if xmlIndex < 0 {
|
||||
return false, nil
|
||||
} else {
|
||||
return xmlIndex <= sgmlIndex, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
||||
// from the given io.Reader
|
||||
//
|
||||
// It is commonly used as part of Client.Request(), but may be used on its own
|
||||
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
||||
// performs version autodetection if it can and attempts to be as forgiving as
|
||||
// possible about the input format.
|
||||
func ParseResponse(reader io.Reader) (*Response, error) {
|
||||
var or Response
|
||||
|
||||
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
||||
xmlVersion, err := guessVersion(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse SGML headers before creating XML decoder
|
||||
if !xmlVersion {
|
||||
if err := or.readSGMLHeaders(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(r)
|
||||
if !xmlVersion {
|
||||
decoder.Strict = false
|
||||
decoder.AutoCloseAfterCharData = ofxLeafElements
|
||||
}
|
||||
decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
if xmlVersion {
|
||||
// parse the xml header
|
||||
if err := or.readXMLHeaders(decoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tok, err := nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
|
||||
return nil, errors.New("Missing opening OFX xml element")
|
||||
}
|
||||
|
||||
// Unmarshal the signon message
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == "SIGNONMSGSRSV1" {
|
||||
if err := decoder.Decode(&or.Signon); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != "SIGNONMSGSRSV1" {
|
||||
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
if ok, err := or.Signon.Valid(); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||
return &or, nil // found closing XML element, so we're done
|
||||
} else if start, ok := tok.(xml.StartElement); ok {
|
||||
// TODO decode other types
|
||||
switch start.Name.Local {
|
||||
case "SIGNUPMSGSRSV1":
|
||||
msgs, err := DecodeSignupMessageSet(decoder, start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
or.Signup = msgs
|
||||
case "BANKMSGSRSV1":
|
||||
msgs, err := DecodeBankingMessageSet(decoder, start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
or.Banking = msgs
|
||||
//case "CREDITCARDMSGSRSV1":
|
||||
//case "LOANMSGSRSV1":
|
||||
//case "INVSTMTMSGSRSV1":
|
||||
//case "INTERXFERMSGSRSV1":
|
||||
//case "WIREXFERMSGSRSV1":
|
||||
//case "BILLPAYMSGSRSV1":
|
||||
//case "EMAILMSGSRSV1":
|
||||
//case "SECLISTMSGSRSV1":
|
||||
//case "PRESDIRMSGSRSV1":
|
||||
//case "PRESDLVMSGSRSV1":
|
||||
case "PROFMSGSRSV1":
|
||||
msgs, err := DecodeProfileMessageSet(decoder, start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
or.Profile = msgs
|
||||
//case "IMAGEMSGSRSV1":
|
||||
default:
|
||||
return nil, errors.New("Unsupported message set: " + start.Name.Local)
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Found unexpected token")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user