Branch data Line data Source code
1 : : /********************************************************************
2 : : * gnc-transactions-xml-v2.c -- xml routines for transactions *
3 : : * Copyright (C) 2001 Rob Browning *
4 : : * Copyright (C) 2002 Linas Vepstas <linas@linas.org> *
5 : : * *
6 : : * This program is free software; you can redistribute it and/or *
7 : : * modify it under the terms of the GNU General Public License as *
8 : : * published by the Free Software Foundation; either version 2 of *
9 : : * the License, or (at your option) any later version. *
10 : : * *
11 : : * This program is distributed in the hope that it will be useful, *
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 : : * GNU General Public License for more details. *
15 : : * *
16 : : * You should have received a copy of the GNU General Public License*
17 : : * along with this program; if not, contact: *
18 : : * *
19 : : * Free Software Foundation Voice: +1-617-542-5942 *
20 : : * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
21 : : * Boston, MA 02110-1301, USA gnu@gnu.org *
22 : : * *
23 : : *******************************************************************/
24 : : #include <glib.h>
25 : :
26 : : #include <config.h>
27 : : #include <string.h>
28 : : #include "AccountP.hpp"
29 : : #include "Transaction.h"
30 : : #include "TransactionP.hpp"
31 : : #include "gnc-lot.h"
32 : : #include "gnc-lot-p.h"
33 : :
34 : : #include "gnc-xml-helper.h"
35 : :
36 : : #include "sixtp.h"
37 : : #include "sixtp-utils.h"
38 : : #include "sixtp-parsers.h"
39 : : #include "sixtp-utils.h"
40 : : #include "sixtp-dom-parsers.h"
41 : : #include "sixtp-dom-generators.h"
42 : :
43 : : #include "gnc-xml.h"
44 : :
45 : : #include "io-gncxml-gen.h"
46 : :
47 : : #include "sixtp-dom-parsers.h"
48 : :
49 : : [[maybe_unused]] static const QofLogModule log_module = G_LOG_DOMAIN;
50 : : const gchar* transaction_version_string = "2.0.0";
51 : :
52 : : static void
53 : 232 : add_gnc_num (xmlNodePtr node, const gchar* tag, gnc_numeric num)
54 : : {
55 : 232 : xmlAddChild (node, gnc_numeric_to_dom_tree (tag, &num));
56 : 232 : }
57 : :
58 : : static void
59 : 232 : add_time64 (xmlNodePtr node, const gchar * tag, time64 time, gboolean always)
60 : : {
61 : 232 : if (always || time)
62 : 216 : xmlAddChild (node, time64_to_dom_tree (tag, time));
63 : 232 : }
64 : :
65 : : static xmlNodePtr
66 : 116 : split_to_dom_tree (const gchar* tag, Split* spl)
67 : : {
68 : : xmlNodePtr ret;
69 : :
70 : 116 : ret = xmlNewNode (NULL, BAD_CAST tag);
71 : :
72 : 116 : xmlAddChild (ret, guid_to_dom_tree ("split:id", xaccSplitGetGUID (spl)));
73 : :
74 : : {
75 : 116 : char* memo = g_strdup (xaccSplitGetMemo (spl));
76 : :
77 : 116 : if (memo && g_strcmp0 (memo, "") != 0)
78 : : {
79 : 104 : xmlNewTextChild (ret, NULL, BAD_CAST "split:memo",
80 : 104 : checked_char_cast (memo));
81 : : }
82 : 116 : g_free (memo);
83 : : }
84 : :
85 : : {
86 : 116 : char* action = g_strdup (xaccSplitGetAction (spl));
87 : :
88 : 116 : if (action && g_strcmp0 (action, "") != 0)
89 : : {
90 : 104 : xmlNewTextChild (ret, NULL, BAD_CAST "split:action",
91 : 104 : checked_char_cast (action));
92 : : }
93 : 116 : g_free (action);
94 : : }
95 : :
96 : : {
97 : : char tmp[2];
98 : :
99 : 116 : tmp[0] = xaccSplitGetReconcile (spl);
100 : 116 : tmp[1] = '\0';
101 : :
102 : 116 : xmlNewTextChild (ret, NULL, BAD_CAST "split:reconciled-state",
103 : : BAD_CAST tmp);
104 : : }
105 : :
106 : 116 : add_time64 (ret, "split:reconcile-date",
107 : : xaccSplitGetDateReconciled (spl), FALSE);
108 : :
109 : 116 : add_gnc_num (ret, "split:value", xaccSplitGetValue (spl));
110 : :
111 : 116 : add_gnc_num (ret, "split:quantity", xaccSplitGetAmount (spl));
112 : :
113 : : {
114 : 116 : Account* account = xaccSplitGetAccount (spl);
115 : :
116 : 116 : xmlAddChild (ret, guid_to_dom_tree ("split:account",
117 : 116 : xaccAccountGetGUID (account)));
118 : : }
119 : : {
120 : 116 : GNCLot* lot = xaccSplitGetLot (spl);
121 : :
122 : 116 : if (lot)
123 : : {
124 : 2 : xmlAddChild (ret, guid_to_dom_tree ("split:lot",
125 : 2 : gnc_lot_get_guid (lot)));
126 : : }
127 : : }
128 : : /* xmlAddChild won't do anything with a NULL, so tests are superfluous. */
129 : 116 : xmlAddChild (ret, qof_instance_slots_to_dom_tree ("split:slots",
130 : 116 : QOF_INSTANCE (spl)));
131 : 116 : return ret;
132 : : }
133 : :
134 : : static void
135 : 58 : add_trans_splits (xmlNodePtr node, Transaction* trn)
136 : : {
137 : : GList* n;
138 : : xmlNodePtr toaddto;
139 : :
140 : 58 : toaddto = xmlNewChild (node, NULL, BAD_CAST "trn:splits", NULL);
141 : :
142 : 174 : for (n = xaccTransGetSplitList (trn); n; n = n->next)
143 : : {
144 : 116 : Split* s = static_cast<decltype (s)> (n->data);
145 : 116 : xmlAddChild (toaddto, split_to_dom_tree ("trn:split", s));
146 : : }
147 : 58 : }
148 : :
149 : : xmlNodePtr
150 : 58 : gnc_transaction_dom_tree_create (Transaction* trn)
151 : : {
152 : : xmlNodePtr ret;
153 : 58 : gchar* str = NULL;
154 : :
155 : 58 : ret = xmlNewNode (NULL, BAD_CAST "gnc:transaction");
156 : :
157 : 58 : xmlSetProp (ret, BAD_CAST "version",
158 : : BAD_CAST transaction_version_string);
159 : :
160 : 58 : xmlAddChild (ret, guid_to_dom_tree ("trn:id", xaccTransGetGUID (trn)));
161 : :
162 : 58 : xmlAddChild (ret, commodity_ref_to_dom_tree ("trn:currency",
163 : 58 : xaccTransGetCurrency (trn)));
164 : 58 : str = g_strdup (xaccTransGetNum (trn));
165 : 58 : if (str && (g_strcmp0 (str, "") != 0))
166 : : {
167 : 52 : xmlNewTextChild (ret, NULL, BAD_CAST "trn:num",
168 : 52 : checked_char_cast (str));
169 : : }
170 : 58 : g_free (str);
171 : :
172 : 58 : add_time64 (ret, "trn:date-posted", xaccTransRetDatePosted (trn), TRUE);
173 : :
174 : 58 : add_time64 (ret, "trn:date-entered",
175 : : xaccTransRetDateEntered (trn), TRUE);
176 : :
177 : 58 : str = g_strdup (xaccTransGetDescription (trn));
178 : 58 : if (str)
179 : : {
180 : 58 : xmlNewTextChild (ret, NULL, BAD_CAST "trn:description",
181 : 58 : checked_char_cast (str));
182 : : }
183 : 58 : g_free (str);
184 : :
185 : : /* xmlAddChild won't do anything with a NULL, so tests are superfluous. */
186 : 58 : xmlAddChild (ret, qof_instance_slots_to_dom_tree ("trn:slots",
187 : 58 : QOF_INSTANCE (trn)));
188 : :
189 : 58 : add_trans_splits (ret, trn);
190 : :
191 : 58 : return ret;
192 : : }
193 : :
194 : : /***********************************************************************/
195 : :
196 : : struct split_pdata
197 : : {
198 : : Split* split;
199 : : QofBook* book;
200 : : };
201 : :
202 : : static inline gboolean
203 : 3440 : set_spl_gnc_num (xmlNodePtr node, Split* spl,
204 : : void (*func) (Split* spl, gnc_numeric gn))
205 : : {
206 : 3440 : func (spl, dom_tree_to_gnc_numeric (node));
207 : 3440 : return TRUE;
208 : : }
209 : :
210 : : static gboolean
211 : 1720 : spl_id_handler (xmlNodePtr node, gpointer data)
212 : : {
213 : 1720 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
214 : 1720 : auto tmp = dom_tree_to_guid (node);
215 : 1720 : g_return_val_if_fail (tmp, FALSE);
216 : :
217 : 1720 : xaccSplitSetGUID (pdata->split, &*tmp);
218 : :
219 : 1720 : return TRUE;
220 : : }
221 : :
222 : : static gboolean
223 : 369 : spl_memo_handler (xmlNodePtr node, gpointer data)
224 : : {
225 : 369 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
226 : 369 : return apply_xmlnode_text (xaccSplitSetMemo, pdata->split, node);
227 : : }
228 : :
229 : : static gboolean
230 : 113 : spl_action_handler (xmlNodePtr node, gpointer data)
231 : : {
232 : 113 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
233 : 113 : return apply_xmlnode_text (xaccSplitSetAction, pdata->split, node);
234 : : }
235 : :
236 : : static gboolean
237 : 1720 : spl_reconciled_state_handler (xmlNodePtr node, gpointer data)
238 : : {
239 : 1720 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
240 : 1720 : auto set_reconciled = [](Split* s, const char *txt)
241 : : {
242 : 1720 : xaccSplitSetReconcile(s, txt[0]);
243 : 1720 : };
244 : 3440 : return apply_xmlnode_text (set_reconciled, pdata->split, node);
245 : : }
246 : :
247 : : static gboolean
248 : 117 : spl_reconcile_date_handler (xmlNodePtr node, gpointer data)
249 : : {
250 : 117 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
251 : 117 : time64 time = dom_tree_to_time64 (node);
252 : 117 : if (!dom_tree_valid_time64 (time, node->name)) time = 0;
253 : 117 : xaccSplitSetDateReconciledSecs (pdata->split, time);
254 : 117 : return TRUE;
255 : : }
256 : :
257 : : static gboolean
258 : 1720 : spl_value_handler (xmlNodePtr node, gpointer data)
259 : : {
260 : 1720 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
261 : 1720 : return set_spl_gnc_num (node, pdata->split, xaccSplitSetValue);
262 : : }
263 : :
264 : : static gboolean
265 : 1720 : spl_quantity_handler (xmlNodePtr node, gpointer data)
266 : : {
267 : 1720 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
268 : 1720 : return set_spl_gnc_num (node, pdata->split, xaccSplitSetAmount);
269 : : }
270 : :
271 : : gboolean gnc_transaction_xml_v2_testing = FALSE;
272 : :
273 : : static gboolean
274 : 1720 : spl_account_handler (xmlNodePtr node, gpointer data)
275 : : {
276 : 1720 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
277 : 1720 : auto id = dom_tree_to_guid (node);
278 : : Account* account;
279 : :
280 : 1720 : g_return_val_if_fail (id, FALSE);
281 : :
282 : 1720 : account = xaccAccountLookup (&*id, pdata->book);
283 : 1720 : if (!account && gnc_transaction_xml_v2_testing &&
284 : 0 : !guid_equal (&*id, guid_null ()))
285 : : {
286 : 0 : account = xaccMallocAccount (pdata->book);
287 : 0 : xaccAccountSetGUID (account, &*id);
288 : 0 : xaccAccountSetCommoditySCU (account,
289 : 0 : xaccSplitGetAmount (pdata->split).denom);
290 : : }
291 : :
292 : 1720 : xaccAccountInsertSplit (account, pdata->split);
293 : :
294 : 1720 : return TRUE;
295 : : }
296 : :
297 : : static gboolean
298 : 8 : spl_lot_handler (xmlNodePtr node, gpointer data)
299 : : {
300 : 8 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
301 : 8 : auto id = dom_tree_to_guid (node);
302 : : GNCLot* lot;
303 : :
304 : 8 : g_return_val_if_fail (id, FALSE);
305 : :
306 : 8 : lot = gnc_lot_lookup (&*id, pdata->book);
307 : 8 : if (!lot && gnc_transaction_xml_v2_testing &&
308 : 0 : !guid_equal (&*id, guid_null ()))
309 : : {
310 : 0 : lot = gnc_lot_new (pdata->book);
311 : 0 : gnc_lot_set_guid (lot, *id);
312 : : }
313 : :
314 : 8 : gnc_lot_add_split (lot, pdata->split);
315 : :
316 : 8 : return TRUE;
317 : : }
318 : :
319 : : static gboolean
320 : 114 : spl_slots_handler (xmlNodePtr node, gpointer data)
321 : : {
322 : 114 : struct split_pdata* pdata = static_cast<decltype (pdata)> (data);
323 : : gboolean successful;
324 : :
325 : 228 : successful = dom_tree_create_instance_slots (node,
326 : 114 : QOF_INSTANCE (pdata->split));
327 : 114 : g_return_val_if_fail (successful, FALSE);
328 : :
329 : 114 : return TRUE;
330 : : }
331 : :
332 : : struct dom_tree_handler spl_dom_handlers[] =
333 : : {
334 : : { "split:id", spl_id_handler, 1, 0 },
335 : : { "split:memo", spl_memo_handler, 0, 0 },
336 : : { "split:action", spl_action_handler, 0, 0 },
337 : : { "split:reconciled-state", spl_reconciled_state_handler, 1, 0 },
338 : : { "split:reconcile-date", spl_reconcile_date_handler, 0, 0 },
339 : : { "split:value", spl_value_handler, 1, 0 },
340 : : { "split:quantity", spl_quantity_handler, 1, 0 },
341 : : { "split:account", spl_account_handler, 1, 0 },
342 : : { "split:lot", spl_lot_handler, 0, 0 },
343 : : { "split:slots", spl_slots_handler, 0, 0 },
344 : : { NULL, NULL, 0, 0 },
345 : : };
346 : :
347 : : static Split*
348 : 1720 : dom_tree_to_split (xmlNodePtr node, QofBook* book)
349 : : {
350 : : struct split_pdata pdata;
351 : : Split* ret;
352 : :
353 : 1720 : g_return_val_if_fail (book, NULL);
354 : :
355 : 1720 : ret = xaccMallocSplit (book);
356 : 1720 : g_return_val_if_fail (ret, NULL);
357 : :
358 : 1720 : pdata.split = ret;
359 : 1720 : pdata.book = book;
360 : :
361 : : /* this isn't going to work in a testing setup */
362 : 1720 : if (dom_tree_generic_parse (node, spl_dom_handlers, &pdata))
363 : : {
364 : 1720 : return ret;
365 : : }
366 : : else
367 : : {
368 : 0 : xaccSplitDestroy (ret);
369 : 0 : return NULL;
370 : : }
371 : : }
372 : :
373 : : /***********************************************************************/
374 : :
375 : : struct trans_pdata
376 : : {
377 : : Transaction* trans;
378 : : QofBook* book;
379 : : };
380 : :
381 : : static gboolean
382 : 1658 : set_tran_time64 (xmlNodePtr node, Transaction * trn,
383 : : void (*func) (Transaction *, time64))
384 : : {
385 : 1658 : time64 time = dom_tree_to_time64 (node);
386 : 1658 : if (!dom_tree_valid_time64 (time, node->name)) time = 0;
387 : 1658 : func (trn, time);
388 : 1658 : return TRUE;
389 : : }
390 : :
391 : : static gboolean
392 : 829 : trn_id_handler (xmlNodePtr node, gpointer trans_pdata)
393 : : {
394 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
395 : 829 : Transaction* trn = pdata->trans;
396 : 829 : auto tmp = dom_tree_to_guid (node);
397 : :
398 : 829 : g_return_val_if_fail (tmp, FALSE);
399 : :
400 : 829 : xaccTransSetGUID ((Transaction*)trn, &*tmp);
401 : :
402 : 829 : return TRUE;
403 : : }
404 : :
405 : : static gboolean
406 : 829 : trn_currency_handler (xmlNodePtr node, gpointer trans_pdata)
407 : : {
408 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
409 : 829 : Transaction* trn = pdata->trans;
410 : : gnc_commodity* ref;
411 : :
412 : 829 : ref = dom_tree_to_commodity_ref (node, pdata->book);
413 : 829 : xaccTransSetCurrency (trn, ref);
414 : :
415 : 829 : return TRUE;
416 : : }
417 : :
418 : : static gboolean
419 : 355 : trn_num_handler (xmlNodePtr node, gpointer trans_pdata)
420 : : {
421 : 355 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
422 : 355 : Transaction* trn = pdata->trans;
423 : :
424 : 355 : return apply_xmlnode_text (xaccTransSetNum, trn, node);
425 : : }
426 : :
427 : : static gboolean
428 : 829 : trn_date_posted_handler (xmlNodePtr node, gpointer trans_pdata)
429 : : {
430 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
431 : 829 : Transaction* trn = pdata->trans;
432 : :
433 : 829 : return set_tran_time64 (node, trn, xaccTransSetDatePostedSecs);
434 : : }
435 : :
436 : : static gboolean
437 : 829 : trn_date_entered_handler (xmlNodePtr node, gpointer trans_pdata)
438 : : {
439 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
440 : 829 : Transaction* trn = pdata->trans;
441 : :
442 : 829 : return set_tran_time64 (node, trn, xaccTransSetDateEnteredSecs);
443 : : }
444 : :
445 : : static gboolean
446 : 829 : trn_description_handler (xmlNodePtr node, gpointer trans_pdata)
447 : : {
448 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
449 : 829 : Transaction* trn = pdata->trans;
450 : :
451 : 829 : return apply_xmlnode_text (xaccTransSetDescription, trn, node);
452 : : }
453 : :
454 : : static gboolean
455 : 62 : trn_slots_handler (xmlNodePtr node, gpointer trans_pdata)
456 : : {
457 : 62 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
458 : 62 : Transaction* trn = pdata->trans;
459 : : gboolean successful;
460 : :
461 : 62 : successful = dom_tree_create_instance_slots (node, QOF_INSTANCE (trn));
462 : :
463 : 62 : g_return_val_if_fail (successful, FALSE);
464 : :
465 : 62 : return TRUE;
466 : : }
467 : :
468 : : static gboolean
469 : 829 : trn_splits_handler (xmlNodePtr node, gpointer trans_pdata)
470 : : {
471 : 829 : struct trans_pdata* pdata = static_cast<decltype (pdata)> (trans_pdata);
472 : 829 : Transaction* trn = pdata->trans;
473 : : xmlNodePtr mark;
474 : :
475 : 829 : g_return_val_if_fail (node, FALSE);
476 : 829 : g_return_val_if_fail (node->xmlChildrenNode, FALSE);
477 : :
478 : 5098 : for (mark = node->xmlChildrenNode; mark; mark = mark->next)
479 : : {
480 : : Split* spl;
481 : :
482 : 4269 : if (g_strcmp0 ("text", (char*)mark->name) == 0)
483 : 2549 : continue;
484 : :
485 : 1720 : if (g_strcmp0 ("trn:split", (char*)mark->name))
486 : : {
487 : 0 : return FALSE;
488 : : }
489 : :
490 : 1720 : spl = dom_tree_to_split (mark, pdata->book);
491 : :
492 : 1720 : if (spl)
493 : : {
494 : 1720 : xaccTransAppendSplit (trn, spl);
495 : : }
496 : : else
497 : : {
498 : 0 : return FALSE;
499 : : }
500 : : }
501 : 829 : return TRUE;
502 : : }
503 : :
504 : : struct dom_tree_handler trn_dom_handlers[] =
505 : : {
506 : : { "trn:id", trn_id_handler, 1, 0 },
507 : : { "trn:currency", trn_currency_handler, 0, 0},
508 : : { "trn:num", trn_num_handler, 0, 0 },
509 : : { "trn:date-posted", trn_date_posted_handler, 1, 0 },
510 : : { "trn:date-entered", trn_date_entered_handler, 1, 0 },
511 : : { "trn:description", trn_description_handler, 0, 0 },
512 : : { "trn:slots", trn_slots_handler, 0, 0 },
513 : : { "trn:splits", trn_splits_handler, 1, 0 },
514 : : { NULL, NULL, 0, 0 },
515 : : };
516 : :
517 : : static gboolean
518 : 26681 : gnc_transaction_end_handler (gpointer data_for_children,
519 : : GSList* data_from_children, GSList* sibling_data,
520 : : gpointer parent_data, gpointer global_data,
521 : : gpointer* result, const gchar* tag)
522 : : {
523 : 26681 : Transaction* trn = NULL;
524 : 26681 : xmlNodePtr tree = (xmlNodePtr)data_for_children;
525 : 26681 : gxpf_data* gdata = (gxpf_data*)global_data;
526 : :
527 : 26681 : if (parent_data)
528 : : {
529 : 25807 : return TRUE;
530 : : }
531 : :
532 : : /* OK. For some messed up reason this is getting called again with a
533 : : NULL tag. So we ignore those cases */
534 : 874 : if (!tag)
535 : : {
536 : 50 : return TRUE;
537 : : }
538 : :
539 : 824 : g_return_val_if_fail (tree, FALSE);
540 : :
541 : 1648 : trn = dom_tree_to_transaction (tree,
542 : 824 : static_cast<QofBook*> (gdata->bookdata));
543 : 824 : if (trn != NULL)
544 : : {
545 : 824 : gdata->cb (tag, gdata->parsedata, trn);
546 : : }
547 : :
548 : 824 : xmlFreeNode (tree);
549 : :
550 : 824 : return trn != NULL;
551 : : }
552 : :
553 : : Transaction*
554 : 829 : dom_tree_to_transaction (xmlNodePtr node, QofBook* book)
555 : : {
556 : : Transaction* trn;
557 : : gboolean successful;
558 : : struct trans_pdata pdata;
559 : :
560 : 829 : g_return_val_if_fail (node, NULL);
561 : 829 : g_return_val_if_fail (book, NULL);
562 : :
563 : 829 : trn = xaccMallocTransaction (book);
564 : 829 : g_return_val_if_fail (trn, NULL);
565 : 829 : xaccTransBeginEdit (trn);
566 : :
567 : 829 : pdata.trans = trn;
568 : 829 : pdata.book = book;
569 : :
570 : 829 : successful = dom_tree_generic_parse (node, trn_dom_handlers, &pdata);
571 : :
572 : 829 : xaccTransCommitEdit (trn);
573 : :
574 : 829 : if (!successful)
575 : : {
576 : 0 : xmlElemDump (stdout, NULL, node);
577 : 0 : xaccTransBeginEdit (trn);
578 : 0 : xaccTransDestroy (trn);
579 : 0 : xaccTransCommitEdit (trn);
580 : 0 : trn = NULL;
581 : : }
582 : :
583 : 829 : return trn;
584 : : }
585 : :
586 : : sixtp*
587 : 94 : gnc_transaction_sixtp_parser_create (void)
588 : : {
589 : 94 : return sixtp_dom_parser_new (gnc_transaction_end_handler, NULL, NULL);
590 : : }
|