made place for record delete
[idzebra-moved-to-github.git] / recctrl / alvis.c
1 /* $Id: alvis.c,v 1.16 2006-05-29 13:48:43 marc Exp $
2    Copyright (C) 1995-2005
3    Index Data ApS
4
5 This file is part of the Zebra server.
6
7 Zebra is free software; you can redistribute it and/or modify it under
8 the terms of the GNU General Public License as published by the Free
9 Software Foundation; either version 2, or (at your option) any later
10 version.
11
12 Zebra is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
15 for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with Zebra; see the file LICENSE.zebra.  If not, write to the
19 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20 02111-1307, USA.
21 */
22
23 #include <stdio.h>
24 #include <assert.h>
25 #include <ctype.h>
26
27 #include <yaz/diagbib1.h>
28 #include <libxml/xmlversion.h>
29 #include <libxml/parser.h>
30 #include <libxml/tree.h>
31 #include <libxml/xmlIO.h>
32 #include <libxml/xmlreader.h>
33 #include <libxslt/transform.h>
34 #include <libxslt/xsltutils.h>
35
36 #include <idzebra/util.h>
37 #include <idzebra/recctrl.h>
38
39 struct filter_schema {
40     const char *name;
41     const char *identifier;
42     const char *stylesheet;
43     struct filter_schema *next;
44     const char *default_schema;
45     /* char default_schema; */
46     const char *include_snippet;
47     xsltStylesheetPtr stylesheet_xsp;
48 };
49
50 struct filter_info {
51     xmlDocPtr doc;
52     char *fname;
53     const char *split_level;
54     const char *split_path;
55     ODR odr;
56     struct filter_schema *schemas;
57     xmlTextReaderPtr reader;
58 };
59
60 #define ZEBRA_SCHEMA_XSLT_NS "http://indexdata.dk/zebra/xslt/1"
61
62 #define XML_STRCMP(a,b)   strcmp((char*)a, b)
63 #define XML_STRLEN(a) strlen((char*)a)
64
65 static const char *zebra_xslt_ns = ZEBRA_SCHEMA_XSLT_NS;
66
67 static void set_param_xml(const char **params, const char *name,
68                           const char *value, ODR odr)
69 {
70     while (*params)
71         params++;
72     params[0] = name;
73     params[1] = value;
74     params[2] = 0;
75 }
76
77 static void set_param_str(const char **params, const char *name,
78                           const char *value, ODR odr)
79 {
80     char *quoted = odr_malloc(odr, 3 + strlen(value));
81     sprintf(quoted, "'%s'", value);
82     while (*params)
83         params++;
84     params[0] = name;
85     params[1] = quoted;
86     params[2] = 0;
87 }
88
89 static void set_param_int(const char **params, const char *name,
90                           zint value, ODR odr)
91 {
92     char *quoted = odr_malloc(odr, 30); /* 25 digits enough for 2^64 */
93     while (*params)
94         params++;
95     sprintf(quoted, "'" ZINT_FORMAT "'", value);
96     params[0] = name;
97     params[1] = quoted;
98     params[2] = 0;
99 }
100
101 #define ENABLE_INPUT_CALLBACK 0
102
103 #if ENABLE_INPUT_CALLBACK
104 static int zebra_xmlInputMatchCallback (char const *filename)
105 {
106     yaz_log(YLOG_LOG, "match %s", filename);
107     return 0;
108 }
109
110 static void * zebra_xmlInputOpenCallback (char const *filename)
111 {
112     return 0;
113 }
114
115 static int zebra_xmlInputReadCallback (void * context, char * buffer, int len)
116 {
117     return 0;
118 }
119
120 static int zebra_xmlInputCloseCallback (void * context)
121 {
122     return 0;
123 }
124 #endif
125
126 static void *filter_init(Res res, RecType recType)
127 {
128     struct filter_info *tinfo = (struct filter_info *) xmalloc(sizeof(*tinfo));
129     tinfo->reader = 0;
130     tinfo->fname = 0;
131     tinfo->split_level = 0;
132     tinfo->split_path = 0;
133     tinfo->odr = odr_createmem(ODR_ENCODE);
134     tinfo->doc = 0;
135     tinfo->schemas = 0;
136
137 #if ENABLE_INPUT_CALLBACK
138     xmlRegisterDefaultInputCallbacks();
139     xmlRegisterInputCallbacks(zebra_xmlInputMatchCallback,
140                               zebra_xmlInputOpenCallback,
141                               zebra_xmlInputReadCallback,
142                               zebra_xmlInputCloseCallback);
143 #endif
144     return tinfo;
145 }
146
147 static int attr_content(struct _xmlAttr *attr, const char *name,
148                         const char **dst_content)
149 {
150     if (!XML_STRCMP(attr->name, name) && attr->children &&
151         attr->children->type == XML_TEXT_NODE)
152     {
153         *dst_content = (const char *)(attr->children->content);
154         return 1;
155     }
156     return 0;
157 }
158
159 static void destroy_schemas(struct filter_info *tinfo)
160 {
161     struct filter_schema *schema = tinfo->schemas;
162     while (schema)
163     {
164         struct filter_schema *schema_next = schema->next;
165         if (schema->stylesheet_xsp)
166             xsltFreeStylesheet(schema->stylesheet_xsp);
167         xfree(schema);
168         schema = schema_next;
169     }
170     tinfo->schemas = 0;
171     xfree(tinfo->fname);
172     if (tinfo->doc)
173         xmlFreeDoc(tinfo->doc);    
174     tinfo->doc = 0;
175 }
176
177 static ZEBRA_RES create_schemas(struct filter_info *tinfo, const char *fname)
178 {
179     xmlNodePtr ptr;
180     tinfo->fname = xstrdup(fname);
181     tinfo->doc = xmlParseFile(tinfo->fname);
182     if (!tinfo->doc)
183         return ZEBRA_FAIL;
184     ptr = xmlDocGetRootElement(tinfo->doc);
185     if (!ptr || ptr->type != XML_ELEMENT_NODE ||
186         XML_STRCMP(ptr->name, "schemaInfo"))
187         return ZEBRA_FAIL;
188     for (ptr = ptr->children; ptr; ptr = ptr->next)
189     {
190         if (ptr->type != XML_ELEMENT_NODE)
191             continue;
192         if (!XML_STRCMP(ptr->name, "schema"))
193         {
194             struct _xmlAttr *attr;
195             struct filter_schema *schema = xmalloc(sizeof(*schema));
196             schema->name = 0;
197             schema->identifier = 0;
198             schema->stylesheet = 0;
199             schema->default_schema = 0;
200             schema->next = tinfo->schemas;
201             schema->stylesheet_xsp = 0;
202             schema->include_snippet = 0;
203             tinfo->schemas = schema;
204             for (attr = ptr->properties; attr; attr = attr->next)
205             {
206                 attr_content(attr, "identifier", &schema->identifier);
207                 attr_content(attr, "name", &schema->name);
208                 attr_content(attr, "stylesheet", &schema->stylesheet);
209                 attr_content(attr, "default", &schema->default_schema);
210                 attr_content(attr, "snippet", &schema->include_snippet);
211             }
212             /*yaz_log(YLOG_LOG, "XSLT add %s %s %s", 
213               schema->name, schema->identifier, schema->stylesheet); */
214
215             /* find requested schema */
216
217             if (schema->stylesheet)
218                 schema->stylesheet_xsp =
219                     xsltParseStylesheetFile(
220                         (const xmlChar*) schema->stylesheet);
221
222                 
223         }
224         else if (!XML_STRCMP(ptr->name, "split"))
225         {
226             struct _xmlAttr *attr;
227             for (attr = ptr->properties; attr; attr = attr->next)
228             {
229                 attr_content(attr, "level", &tinfo->split_level);
230                 attr_content(attr, "path", &tinfo->split_path);
231             }
232         }
233         else
234         {
235             yaz_log(YLOG_WARN, "Bad element %s in %s", ptr->name, fname);
236             return ZEBRA_FAIL;
237         }
238     }
239     return ZEBRA_OK;
240 }
241
242 static struct filter_schema *lookup_schema(struct filter_info *tinfo,
243                                            const char *est)
244 {
245     struct filter_schema *schema;
246
247     for (schema = tinfo->schemas; schema; schema = schema->next)
248     { 
249         /* find requested schema */
250         if (est) 
251         {    
252             if (schema->identifier && !strcmp(schema->identifier, est))
253                 return schema;
254             
255             if (schema->name && !strcmp(schema->name, est))
256                 return schema;
257         } 
258         /* or return default schema if defined */
259         else if (schema->default_schema)
260             return schema;
261     }
262
263     /* return first schema if no default schema defined */
264     if (tinfo->schemas)
265         return tinfo->schemas;
266     
267     return 0;
268 }
269
270 static ZEBRA_RES filter_config(void *clientData, Res res, const char *args)
271 {
272     struct filter_info *tinfo = clientData;
273     if (!args || !*args)
274         return ZEBRA_FAIL;
275     if (tinfo->fname && !strcmp(args, tinfo->fname))
276         return ZEBRA_OK;
277     destroy_schemas(tinfo);
278     create_schemas(tinfo, args);
279     return ZEBRA_OK;
280 }
281
282 static void filter_destroy(void *clientData)
283 {
284     struct filter_info *tinfo = clientData;
285     destroy_schemas(tinfo);
286     if (tinfo->reader)
287         xmlFreeTextReader(tinfo->reader);
288     odr_destroy(tinfo->odr);
289     xfree(tinfo);
290 }
291
292 static int ioread_ex(void *context, char *buffer, int len)
293 {
294     struct recExtractCtrl *p = context;
295     return (*p->readf)(p->fh, buffer, len);
296 }
297
298 static int ioclose_ex(void *context)
299 {
300     return 0;
301 }
302
303 static void index_cdata(struct filter_info *tinfo, struct recExtractCtrl *ctrl,
304                         xmlNodePtr ptr, RecWord *recWord)
305 {
306     for(; ptr; ptr = ptr->next)
307     {
308         index_cdata(tinfo, ctrl, ptr->children, recWord);
309         if (ptr->type != XML_TEXT_NODE)
310             continue;
311         recWord->term_buf = (const char *)ptr->content;
312         recWord->term_len = XML_STRLEN(ptr->content);
313         (*ctrl->tokenAdd)(recWord);
314     }
315 }
316
317 static void index_node(struct filter_info *tinfo,  struct recExtractCtrl *ctrl,
318                        xmlNodePtr ptr, RecWord *recWord)
319 {
320     for(; ptr; ptr = ptr->next)
321     {
322         index_node(tinfo, ctrl, ptr->children, recWord);
323         if (ptr->type != XML_ELEMENT_NODE || !ptr->ns ||
324             XML_STRCMP(ptr->ns->href, zebra_xslt_ns))
325             continue;
326         if (!XML_STRCMP(ptr->name, "index"))
327         {
328             const char *name_str = 0;
329             const char *type_str = 0;
330             const char *xpath_str = 0;
331             struct _xmlAttr *attr;
332             for (attr = ptr->properties; attr; attr = attr->next)
333             {
334                 attr_content(attr, "name", &name_str);
335                 attr_content(attr, "xpath", &xpath_str);
336                 attr_content(attr, "type", &type_str);
337             }
338             if (name_str)
339             {
340                 int prev_type = recWord->index_type; /* save default type */
341
342                 if (type_str && *type_str)
343                     recWord->index_type = *type_str; /* type was given */
344                 recWord->index_name = name_str;
345                 index_cdata(tinfo, ctrl, ptr->children, recWord);
346
347                 recWord->index_type = prev_type;     /* restore it again */
348             }
349         }
350     }
351 }
352
353 static void index_record(struct filter_info *tinfo,struct recExtractCtrl *ctrl,
354                          xmlNodePtr ptr, RecWord *recWord)
355 {
356     const char *type_str = "update";
357
358     if (ptr && ptr->type == XML_ELEMENT_NODE && ptr->ns &&
359         !XML_STRCMP(ptr->ns->href, zebra_xslt_ns)
360         && !XML_STRCMP(ptr->name, "record"))
361     {
362         const char *id_str = 0;
363         const char *rank_str = 0;
364         struct _xmlAttr *attr;
365         for (attr = ptr->properties; attr; attr = attr->next)
366         {
367             attr_content(attr, "type", &type_str);
368             attr_content(attr, "id", &id_str);
369             attr_content(attr, "rank", &rank_str);
370         }
371         if (id_str)
372             sscanf(id_str, "%255s", ctrl->match_criteria);
373
374         if (rank_str)
375             ctrl->staticrank = atoi(rank_str);
376         
377         ptr = ptr->children;
378     }
379
380     if (!strcmp("update", type_str))
381         index_node(tinfo, ctrl, ptr, recWord);
382     else if (!strcmp("delete", type_str))
383          yaz_log(YLOG_WARN, "alvis filter delete: to be implemented");
384     else
385          yaz_log(YLOG_WARN, "alvis filter: unknown record type '%s'", 
386                  type_str);
387 }
388     
389 static int extract_doc(struct filter_info *tinfo, struct recExtractCtrl *p,
390                        xmlDocPtr doc)
391 {
392     RecWord recWord;
393     const char *params[10];
394     xmlChar *buf_out;
395     int len_out;
396
397     struct filter_schema *schema = lookup_schema(tinfo, zebra_xslt_ns);
398
399     params[0] = 0;
400     set_param_str(params, "schema", zebra_xslt_ns, tinfo->odr);
401
402     (*p->init)(p, &recWord);
403
404     if (schema && schema->stylesheet_xsp)
405     {
406         xmlNodePtr root_ptr;
407         xmlDocPtr resDoc = 
408             xsltApplyStylesheet(schema->stylesheet_xsp,
409                                 doc, params);
410         if (p->flagShowRecords)
411         {
412             xmlDocDumpMemory(resDoc, &buf_out, &len_out);
413             fwrite(buf_out, len_out, 1, stdout);
414             xmlFree(buf_out);
415         }
416         root_ptr = xmlDocGetRootElement(resDoc);
417         if (root_ptr)
418             index_record(tinfo, p, root_ptr, &recWord);
419         else
420         {
421             yaz_log(YLOG_WARN, "No root for index XML record."
422                     " split_level=%s stylesheet=%s",
423                     tinfo->split_level, schema->stylesheet);
424         }
425         xmlFreeDoc(resDoc);
426     }
427     xmlDocDumpMemory(doc, &buf_out, &len_out);
428     if (p->flagShowRecords)
429         fwrite(buf_out, len_out, 1, stdout);
430     (*p->setStoreData)(p, buf_out, len_out);
431     xmlFree(buf_out);
432     
433     xmlFreeDoc(doc);
434     return RECCTRL_EXTRACT_OK;
435 }
436
437 static int extract_split(struct filter_info *tinfo, struct recExtractCtrl *p)
438 {
439     int ret;
440     int split_depth = 0;
441     if (p->first_record)
442     {
443         if (tinfo->reader)
444             xmlFreeTextReader(tinfo->reader);
445         tinfo->reader = xmlReaderForIO(ioread_ex, ioclose_ex,
446                                        p /* I/O handler */,
447                                        0 /* URL */, 
448                                        0 /* encoding */,
449                                        XML_PARSE_XINCLUDE);
450     }
451     if (!tinfo->reader)
452         return RECCTRL_EXTRACT_ERROR_GENERIC;
453
454     if (tinfo->split_level)
455         split_depth = atoi(tinfo->split_level);
456     ret = xmlTextReaderRead(tinfo->reader);
457     while (ret == 1) {
458         int type = xmlTextReaderNodeType(tinfo->reader);
459         int depth = xmlTextReaderDepth(tinfo->reader);
460         if (split_depth == 0 ||
461             (split_depth > 0 &&
462              type == XML_READER_TYPE_ELEMENT && split_depth == depth))
463         {
464             xmlNodePtr ptr = xmlTextReaderExpand(tinfo->reader);
465             xmlNodePtr ptr2 = xmlCopyNode(ptr, 1);
466             xmlDocPtr doc = xmlNewDoc((const xmlChar*) "1.0");
467
468             xmlDocSetRootElement(doc, ptr2);
469
470             return extract_doc(tinfo, p, doc);   
471         }
472         ret = xmlTextReaderRead(tinfo->reader);
473     }
474     xmlFreeTextReader(tinfo->reader);
475     tinfo->reader = 0;
476     return RECCTRL_EXTRACT_EOF;
477 }
478
479 static int extract_full(struct filter_info *tinfo, struct recExtractCtrl *p)
480 {
481     if (p->first_record) /* only one record per stream */
482     {
483         xmlDocPtr doc = xmlReadIO(ioread_ex, ioclose_ex, p /* I/O handler */,
484                                   0 /* URL */,
485                                   0 /* encoding */,
486                                   XML_PARSE_XINCLUDE);
487         if (!doc)
488         {
489             return RECCTRL_EXTRACT_ERROR_GENERIC;
490         }
491         return extract_doc(tinfo, p, doc);
492     }
493     else
494         return RECCTRL_EXTRACT_EOF;
495 }
496
497 static int filter_extract(void *clientData, struct recExtractCtrl *p)
498 {
499     struct filter_info *tinfo = clientData;
500
501     odr_reset(tinfo->odr);
502
503     if (tinfo->split_level == 0 && tinfo->split_path == 0)
504         return extract_full(tinfo, p);
505     else
506     {
507         return extract_split(tinfo, p);
508     }
509 }
510
511 static int ioread_ret(void *context, char *buffer, int len)
512 {
513     struct recRetrieveCtrl *p = context;
514     return (*p->readf)(p->fh, buffer, len);
515 }
516
517 static int ioclose_ret(void *context)
518 {
519     return 0;
520 }
521
522
523 static const char *snippet_doc(struct recRetrieveCtrl *p, int text_mode,
524                                int window_size)
525 {
526     const char *xml_doc_str;
527     int ord = 0;
528     WRBUF wrbuf = wrbuf_alloc();
529     zebra_snippets *res = 
530         zebra_snippets_window(p->doc_snippet, p->hit_snippet, window_size);
531     zebra_snippet_word *w = zebra_snippets_list(res);
532
533     if (text_mode)
534         wrbuf_printf(wrbuf, "\'");
535     else
536         wrbuf_printf(wrbuf, "<snippet xmlns='%s'>\n", zebra_xslt_ns);
537     for (; w; w = w->next)
538     {
539         if (ord == 0)
540             ord = w->ord;
541         else if (ord != w->ord)
542
543             break;
544         if (text_mode)
545             wrbuf_printf(wrbuf, "%s%s%s ", 
546                          w->match ? "*" : "",
547                          w->term,
548                          w->match ? "*" : "");
549         else
550         {
551             wrbuf_printf(wrbuf, " <term ord='%d' seqno='" ZINT_FORMAT "' %s>", 
552                          w->ord, w->seqno,
553                          (w->match ? "match='1'" : ""));
554             wrbuf_xmlputs(wrbuf, w->term);
555             wrbuf_printf(wrbuf, "</term>\n");
556         }
557     }
558     if (text_mode)
559         wrbuf_printf(wrbuf, "\'");
560     else
561         wrbuf_printf(wrbuf, "</snippet>\n");
562
563     xml_doc_str = odr_strdup(p->odr, wrbuf_buf(wrbuf));
564
565     zebra_snippets_destroy(res);
566     wrbuf_free(wrbuf, 1);
567     return xml_doc_str;
568 }
569
570 static int filter_retrieve (void *clientData, struct recRetrieveCtrl *p)
571 {
572     /* const char *esn = zebra_xslt_ns; */
573     const char *esn = 0;
574     const char *params[32];
575     struct filter_info *tinfo = clientData;
576     xmlDocPtr resDoc;
577     xmlDocPtr doc;
578     struct filter_schema *schema;
579     int window_size = -1;
580
581     if (p->comp)
582     {
583         if (p->comp->which == Z_RecordComp_simple
584             && p->comp->u.simple->which == Z_ElementSetNames_generic)
585         {
586             esn = p->comp->u.simple->u.generic;
587         }
588         else if (p->comp->which == Z_RecordComp_complex 
589                  && p->comp->u.complex->generic->elementSpec
590                  && p->comp->u.complex->generic->elementSpec->which ==
591                  Z_ElementSpec_elementSetName)
592         {
593             esn = p->comp->u.complex->generic->elementSpec->u.elementSetName;
594         }
595     }
596     schema = lookup_schema(tinfo, esn);
597     if (!schema)
598     {
599         p->diagnostic =
600             YAZ_BIB1_SPECIFIED_ELEMENT_SET_NAME_NOT_VALID_FOR_SPECIFIED_;
601         return 0;
602     }
603
604     if (schema->include_snippet)
605         window_size = atoi(schema->include_snippet);
606
607     params[0] = 0;
608     set_param_int(params, "id", p->localno, p->odr);
609     if (p->fname)
610         set_param_str(params, "filename", p->fname, p->odr);
611     if (p->staticrank >= 0)
612         set_param_int(params, "rank", p->staticrank, p->odr);
613
614     if (esn)
615         set_param_str(params, "schema", esn, p->odr);
616     else
617         if (schema->name)
618             set_param_str(params, "schema", schema->name, p->odr);
619         else if (schema->identifier)
620             set_param_str(params, "schema", schema->identifier, p->odr);
621         else
622             set_param_str(params, "schema", "", p->odr);
623
624     if (p->score >= 0)
625         set_param_int(params, "score", p->score, p->odr);
626     set_param_int(params, "size", p->recordSize, p->odr);
627
628     if (window_size >= 0)
629         set_param_xml(params, "snippet", snippet_doc(p, 1, window_size),
630                       p->odr);
631     doc = xmlReadIO(ioread_ret, ioclose_ret, p /* I/O handler */,
632                     0 /* URL */,
633                     0 /* encoding */,
634                     XML_PARSE_XINCLUDE);
635     if (!doc)
636     {
637         p->diagnostic = YAZ_BIB1_SYSTEM_ERROR_IN_PRESENTING_RECORDS;
638         return 0;
639     }
640
641     if (window_size >= 0)
642     {
643         xmlNodePtr node = xmlDocGetRootElement(doc);
644         const char *snippet_str = snippet_doc(p, 0, window_size);
645         xmlDocPtr snippet_doc = xmlParseMemory(snippet_str, strlen(snippet_str));
646         xmlAddChild(node, xmlDocGetRootElement(snippet_doc));
647     }
648     if (!schema->stylesheet_xsp)
649         resDoc = doc;
650     else
651     {
652         resDoc = xsltApplyStylesheet(schema->stylesheet_xsp,
653                                      doc, params);
654         xmlFreeDoc(doc);
655     }
656     if (!resDoc)
657     {
658         p->diagnostic = YAZ_BIB1_SYSTEM_ERROR_IN_PRESENTING_RECORDS;
659     }
660     else if (p->input_format == VAL_NONE || p->input_format == VAL_TEXT_XML)
661     {
662         xmlChar *buf_out;
663         int len_out;
664
665         xsltSaveResultToString(&buf_out, &len_out, resDoc,
666                                schema->stylesheet_xsp); 
667
668         p->output_format = VAL_TEXT_XML;
669         p->rec_len = len_out;
670         p->rec_buf = odr_malloc(p->odr, p->rec_len);
671         memcpy(p->rec_buf, buf_out, p->rec_len);
672         xmlFree(buf_out);
673     }
674     else if (p->output_format == VAL_SUTRS)
675     {
676         xmlChar *buf_out;
677         int len_out;
678
679         xsltSaveResultToString(&buf_out, &len_out, resDoc,
680                                schema->stylesheet_xsp); 
681
682         p->output_format = VAL_SUTRS;
683         p->rec_len = len_out;
684         p->rec_buf = odr_malloc(p->odr, p->rec_len);
685         memcpy(p->rec_buf, buf_out, p->rec_len);
686         
687         xmlFree(buf_out);
688     }
689     else
690     {
691         p->diagnostic = YAZ_BIB1_RECORD_SYNTAX_UNSUPP;
692     }
693     xmlFreeDoc(resDoc);
694     return 0;
695 }
696
697 static struct recType filter_type = {
698     0,
699     "alvis",
700     filter_init,
701     filter_config,
702     filter_destroy,
703     filter_extract,
704     filter_retrieve
705 };
706
707 RecType
708 #ifdef IDZEBRA_STATIC_ALVIS
709 idzebra_filter_alvis
710 #else
711 idzebra_filter
712 #endif
713
714 [] = {
715     &filter_type,
716     0,
717 };
718 /*
719  * Local variables:
720  * c-basic-offset: 4
721  * indent-tabs-mode: nil
722  * End:
723  * vim: shiftwidth=4 tabstop=8 expandtab
724  */
725