ledger/xml.cc
John Wiegley 858978de89 Journal data structures now use date_t instead of datetime_t.
This means transactions can only have day-level granularity -- which has
always been the case from an data file point of view.  The advantage to this
restriction is that reports will now be immune from daylight savings related
bugs, where a transaction falls to the wrong side of a --monthly report, for
example.
2008-08-01 17:37:22 -04:00

467 lines
12 KiB
C++

#include "xml.h"
#include "journal.h"
#include "utils.h"
namespace ledger {
#if defined(HAVE_EXPAT) || defined(HAVE_XMLPARSE)
static XML_Parser current_parser;
static unsigned int count;
static journal_t * curr_journal;
static entry_t * curr_entry;
static commodity_t * curr_comm;
static string comm_flags;
static xact_t::state_t curr_state;
static string data;
static bool ignore;
static string have_error;
static void startElement(void *userData, const char *name, const char **attrs)
{
if (ignore)
return;
if (std::strcmp(name, "entry") == 0) {
assert(! curr_entry);
curr_entry = new entry_t;
curr_state = xact_t::UNCLEARED;
}
else if (std::strcmp(name, "xact") == 0) {
assert(curr_entry);
curr_entry->add_xact(new xact_t);
if (curr_state != xact_t::UNCLEARED)
curr_entry->xacts.back()->state = curr_state;
}
else if (std::strcmp(name, "commodity") == 0) {
if (string(attrs[0]) == "flags")
comm_flags = attrs[1];
}
else if (std::strcmp(name, "total") == 0) {
ignore = true;
}
}
static void endElement(void *userData, const char *name)
{
if (ignore) {
if (std::strcmp(name, "total") == 0)
ignore = false;
return;
}
if (std::strcmp(name, "entry") == 0) {
assert(curr_entry);
if (curr_journal->add_entry(curr_entry)) {
count++;
} else {
account_t * acct = curr_journal->find_account("<Unknown>");
curr_entry->add_xact(new xact_t(acct));
if (curr_journal->add_entry(curr_entry)) {
count++;
} else {
checked_delete(curr_entry);
have_error = "Entry cannot be balanced";
}
}
curr_entry = NULL;
}
else if (std::strcmp(name, "en:date") == 0) {
curr_entry->_date = parse_date(data);
}
else if (std::strcmp(name, "en:date_eff") == 0) {
curr_entry->_date_eff = parse_date(data);
}
else if (std::strcmp(name, "en:code") == 0) {
curr_entry->code = data;
}
else if (std::strcmp(name, "en:cleared") == 0) {
curr_state = xact_t::CLEARED;
}
else if (std::strcmp(name, "en:pending") == 0) {
curr_state = xact_t::PENDING;
}
else if (std::strcmp(name, "en:payee") == 0) {
curr_entry->payee = data;
}
else if (std::strcmp(name, "tr:account") == 0) {
curr_entry->xacts.back()->account = curr_journal->find_account(data);
}
else if (std::strcmp(name, "tr:cleared") == 0) {
curr_entry->xacts.back()->state = xact_t::CLEARED;
}
else if (std::strcmp(name, "tr:pending") == 0) {
curr_entry->xacts.back()->state = xact_t::PENDING;
}
else if (std::strcmp(name, "tr:virtual") == 0) {
curr_entry->xacts.back()->add_flags(XACT_VIRTUAL);
}
else if (std::strcmp(name, "tr:generated") == 0) {
curr_entry->xacts.back()->add_flags(XACT_AUTO);
}
else if (std::strcmp(name, "symbol") == 0) {
assert(! curr_comm);
curr_comm = amount_t::current_pool->find_or_create(data);
assert(curr_comm);
curr_comm->add_flags(COMMODITY_STYLE_SUFFIXED);
if (! comm_flags.empty()) {
for (string::size_type i = 0, l = comm_flags.length(); i < l; i++) {
switch (comm_flags[i]) {
case 'P': curr_comm->drop_flags(COMMODITY_STYLE_SUFFIXED); break;
case 'S': curr_comm->add_flags(COMMODITY_STYLE_SEPARATED); break;
case 'T': curr_comm->add_flags(COMMODITY_STYLE_THOUSANDS); break;
case 'E': curr_comm->add_flags(COMMODITY_STYLE_EUROPEAN); break;
}
}
}
}
#if 0
// jww (2006-03-02): !!!
else if (std::strcmp(name, "price") == 0) {
assert(curr_comm);
amount_t * price = new amount_t(data);
std::ostringstream symstr;
symstr << curr_comm->symbol << " {" << *price << "}";
commodity_t * priced_comm =
commodity_t::find_commodity(symstr.str(), true);
priced_comm->price = price;
priced_comm->base = curr_comm;
curr_comm = priced_comm;
}
#endif
else if (std::strcmp(name, "quantity") == 0) {
curr_entry->xacts.back()->amount.parse(data);
if (curr_comm) {
string::size_type i = data.find('.');
if (i != string::npos) {
int precision = data.length() - i - 1;
if (precision > curr_comm->precision())
curr_comm->set_precision(precision);
}
curr_entry->xacts.back()->amount.set_commodity(*curr_comm);
curr_comm = NULL;
}
}
else if (std::strcmp(name, "tr:amount") == 0) {
curr_comm = NULL;
}
}
static void dataHandler(void *userData, const char *s, int len)
{
if (! ignore)
data = string(s, len);
}
bool xml_parser_t::test(std::istream& in) const
{
char buf[80];
in.getline(buf, 79);
if (std::strncmp(buf, "<?xml", 5) != 0) {
in.clear();
in.seekg(0, std::ios::beg);
return false;
}
in.getline(buf, 79);
if (! std::strstr(buf, "<ledger")) {
in.clear();
in.seekg(0, std::ios::beg);
return false;
}
in.clear();
in.seekg(0, std::ios::beg);
return true;
}
unsigned int xml_parser_t::parse(std::istream& in,
session_t& session,
journal_t& journal,
account_t * master,
const path * original_file)
{
char buf[BUFSIZ];
count = 0;
curr_journal = &journal;
curr_entry = NULL;
curr_comm = NULL;
ignore = false;
XML_Parser parser = XML_ParserCreate(NULL);
current_parser = parser;
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, dataHandler);
while (! in.eof()) {
in.getline(buf, BUFSIZ - 1);
std::strcat(buf, "\n");
bool result;
try {
result = XML_Parse(parser, buf, std::strlen(buf), in.eof());
}
catch (const std::exception& err) {
//unsigned long line = XML_GetCurrentLineNumber(parser) - offset++;
XML_ParserFree(parser);
throw parse_error(err.what());
}
if (! have_error.empty()) {
//unsigned long line = XML_GetCurrentLineNumber(parser) - offset++;
parse_error err(have_error);
std::cerr << "Error: " << err.what() << std::endl;
have_error = "";
}
if (! result) {
//unsigned long line = XML_GetCurrentLineNumber(parser) - offset++;
const char * err = XML_ErrorString(XML_GetErrorCode(parser));
XML_ParserFree(parser);
throw parse_error(err);
}
}
XML_ParserFree(parser);
return count;
}
#endif // defined(HAVE_EXPAT) || defined(HAVE_XMLPARSE)
void xml_write_amount(std::ostream& out, const amount_t& amount,
const int depth = 0)
{
for (int i = 0; i < depth; i++) out << ' ';
out << "<amount>\n";
commodity_t& c = amount.commodity();
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "<commodity flags=\"";
if (! (c.flags() & COMMODITY_STYLE_SUFFIXED)) out << 'P';
if (c.flags() & COMMODITY_STYLE_SEPARATED) out << 'S';
if (c.flags() & COMMODITY_STYLE_THOUSANDS) out << 'T';
if (c.flags() & COMMODITY_STYLE_EUROPEAN) out << 'E';
out << "\">\n";
for (int i = 0; i < depth + 4; i++) out << ' ';
#if 0
// jww (2006-03-02): !!!
if (c.price) {
out << "<symbol>" << c.base->symbol << "</symbol>\n";
for (int i = 0; i < depth + 4; i++) out << ' ';
out << "<price>\n";
xml_write_amount(out, *c.price, depth + 6);
for (int i = 0; i < depth + 4; i++) out << ' ';
out << "</price>\n";
} else {
out << "<symbol>" << c.symbol << "</symbol>\n";
}
#endif
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "</commodity>\n";
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "<quantity>";
out << amount.quantity_string() << "</quantity>\n";
for (int i = 0; i < depth; i++) out << ' ';
out << "</amount>\n";
}
void xml_write_value(std::ostream& out, const value_t& value,
const int depth = 0)
{
const balance_t * bal = NULL;
for (int i = 0; i < depth; i++) out << ' ';
out << "<value type=\"";
switch (value.type()) {
case value_t::BOOLEAN: out << "boolean"; break;
case value_t::INTEGER: out << "integer"; break;
case value_t::AMOUNT: out << "amount"; break;
case value_t::BALANCE:
case value_t::BALANCE_PAIR: out << "balance"; break;
default:
assert(false);
break;
}
out << "\">\n";
switch (value.type()) {
case value_t::BOOLEAN:
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "<boolean>" << value.as_boolean() << "</boolean>\n";
break;
case value_t::INTEGER:
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "<integer>" << value.as_long() << "</integer>\n";
break;
case value_t::AMOUNT:
xml_write_amount(out, value.as_amount(), depth + 2);
break;
case value_t::BALANCE:
bal = &(value.as_balance());
// fall through...
case value_t::BALANCE_PAIR:
if (! bal)
bal = &(value.as_balance_pair().quantity());
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "<balance>\n";
foreach (const balance_t::amounts_map::value_type& pair, bal->amounts)
xml_write_amount(out, pair.second, depth + 4);
for (int i = 0; i < depth + 2; i++) out << ' ';
out << "</balance>\n";
break;
default:
assert(false);
break;
}
for (int i = 0; i < depth; i++) out << ' ';
out << "</value>\n";
}
void output_xml_string(std::ostream& out, const string& str)
{
for (const char * s = str.c_str(); *s; s++) {
switch (*s) {
case '<':
out << "&lt;";
break;
case '>':
out << "&rt;";
break;
case '&':
out << "&amp;";
break;
default:
out << *s;
break;
}
}
}
void format_xml_entries::format_last_entry()
{
#if 0
// jww (2008-05-08): Need to format these dates
output_stream << " <entry>\n"
<< " <en:date>" << last_entry->_date.to_string("%Y/%m/%d")
<< "</en:date>\n";
if (is_valid(last_entry->_date_eff))
output_stream << " <en:date_eff>"
<< last_entry->_date_eff.to_string("%Y/%m/%d")
<< "</en:date_eff>\n";
#endif
if (last_entry->code) {
output_stream << " <en:code>";
output_xml_string(output_stream, *last_entry->code);
output_stream << "</en:code>\n";
}
if (! last_entry->payee.empty()) {
output_stream << " <en:payee>";
output_xml_string(output_stream, last_entry->payee);
output_stream << "</en:payee>\n";
}
bool first = true;
foreach (const xact_t * xact, last_entry->xacts) {
if (xact_has_xdata(*xact) &&
xact_xdata_(*xact).dflags & XACT_TO_DISPLAY) {
if (first) {
output_stream << " <en:xacts>\n";
first = false;
}
output_stream << " <xact>\n";
#if 0
// jww (2008-05-08): Need to format these
if (xact->_date)
output_stream << " <tr:date>"
<< xact->_date.to_string("%Y/%m/%d")
<< "</tr:date>\n";
if (is_valid(xact->_date_eff))
output_stream << " <tr:date_eff>"
<< xact->_date_eff.to_string("%Y/%m/%d")
<< "</tr:date_eff>\n";
#endif
if (xact->state == xact_t::CLEARED)
output_stream << " <tr:cleared/>\n";
else if (xact->state == xact_t::PENDING)
output_stream << " <tr:pending/>\n";
if (xact->has_flags(XACT_VIRTUAL))
output_stream << " <tr:virtual/>\n";
if (xact->has_flags(XACT_AUTO))
output_stream << " <tr:generated/>\n";
if (xact->account) {
string name = xact->account->fullname();
if (name == "<Total>")
name = "[TOTAL]";
else if (name == "<Unknown>")
name = "[UNKNOWN]";
output_stream << " <tr:account>";
output_xml_string(output_stream, name);
output_stream << "</tr:account>\n";
}
output_stream << " <tr:amount>\n";
if (xact_xdata_(*xact).dflags & XACT_COMPOUND)
xml_write_value(output_stream,
xact_xdata_(*xact).value, 10);
else
xml_write_value(output_stream, value_t(xact->amount), 10);
output_stream << " </tr:amount>\n";
if (xact->cost) {
output_stream << " <tr:cost>\n";
xml_write_value(output_stream, value_t(*xact->cost), 10);
output_stream << " </tr:cost>\n";
}
if (xact->note) {
output_stream << " <tr:note>";
output_xml_string(output_stream, *xact->note);
output_stream << "</tr:note>\n";
}
if (show_totals) {
output_stream << " <total>\n";
xml_write_value(output_stream, xact_xdata_(*xact).total, 10);
output_stream << " </total>\n";
}
output_stream << " </xact>\n";
xact_xdata_(*xact).dflags |= XACT_DISPLAYED;
}
}
if (! first)
output_stream << " </en:xacts>\n";
output_stream << " </entry>\n";
}
} // namespace ledger