Build main libidzebra-2.0.la. Bug #620.
[idzebra-moved-to-github.git] / index / recgrs.c
1 /* $Id: recgrs.c,v 1.1 2006-07-03 14:27:09 adam Exp $
2    Copyright (C) 1995-2006
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 <sys/types.h>
26 #include <ctype.h>
27
28 #include <yaz/log.h>
29 #include <yaz/oid.h>
30
31 #include <d1_absyn.h>
32 #include <idzebra/recgrs.h>
33
34 #define GRS_MAX_WORD 512
35
36 struct source_parser {
37     int len;
38     const char *tok;
39     const char *src;
40     int lookahead;
41     NMEM nmem;
42 };
43
44 static int sp_lex(struct source_parser *sp)
45 {
46     while (*sp->src == ' ')
47         (sp->src)++;
48     sp->tok = sp->src;
49     sp->len = 0;
50     while (*sp->src && !strchr("<>();,-: ", *sp->src))
51     {
52         sp->src++;
53         sp->len++;
54     }
55     if (sp->len)
56         sp->lookahead = 't';
57     else
58     {
59         sp->lookahead = *sp->src;
60         if (*sp->src)
61             sp->src++;
62     }
63     return sp->lookahead;
64 }
65
66 static int sp_expr(struct source_parser *sp, data1_node *n, RecWord *wrd);
67
68 static int sp_range(struct source_parser *sp, data1_node *n, RecWord *wrd)
69 {
70     int start, len;
71     RecWord tmp_w;
72     
73     /* ( */
74     sp_lex(sp);
75     if (sp->lookahead != '(')
76         return 0;
77     sp_lex(sp); /* skip ( */
78     
79     /* 1st arg: string */
80     if (!sp_expr(sp, n, wrd))
81         return 0;
82     
83     if (sp->lookahead != ',')
84         return 0;       
85     sp_lex(sp); /* skip , */
86     
87     /* 2nd arg: start */
88     if (!sp_expr(sp, n, &tmp_w))
89         return 0;
90     start = atoi_n(tmp_w.term_buf, tmp_w.term_len);
91     
92     if (sp->lookahead == ',')
93     {
94         sp_lex(sp); /* skip , */
95         
96         /* 3rd arg: length */
97         if (!sp_expr(sp, n, &tmp_w))
98             return 0;
99         len = atoi_n(tmp_w.term_buf, tmp_w.term_len);
100     }
101     else
102         len = wrd->term_len;
103     
104     /* ) */
105     if (sp->lookahead != ')')
106         return 0;       
107     sp_lex(sp);
108     
109     if (wrd->term_buf && wrd->term_len)
110     {
111         wrd->term_buf += start;
112         wrd->term_len -= start;
113         if (wrd->term_len > len)
114             wrd->term_len = len;
115     }
116     return 1;
117 }
118
119 static int sp_first(struct source_parser *sp, data1_node *n, RecWord *wrd)
120 {
121     char num_str[20];
122     int min_pos = -1;
123     sp_lex(sp);
124     if (sp->lookahead != '(')
125         return 0;
126     sp_lex(sp); /* skip ( */
127     if (!sp_expr(sp, n, wrd))
128         return 0;
129     while (sp->lookahead == ',')
130     {
131         RecWord search_w;
132         int i;
133         sp_lex(sp); /* skip , */
134         
135         if (!sp_expr(sp, n, &search_w))
136             return 0;
137         for (i = 0; i<wrd->term_len; i++)
138         {
139             int j;
140             for (j = 0; j<search_w.term_len && i+j < wrd->term_len; j++)
141                 if (wrd->term_buf[i+j] != search_w.term_buf[j])
142                     break;
143             if (j == search_w.term_len) /* match ? */
144             {
145                 if (min_pos == -1 || i < min_pos)
146                     min_pos = i;
147                 break;
148             }
149         }
150     }
151     if (sp->lookahead != ')')
152         return 0;
153     sp_lex(sp);
154     if (min_pos == -1)
155         min_pos = 0;  /* the default if not found */
156     sprintf(num_str, "%d", min_pos);
157     wrd->term_buf = nmem_strdup(sp->nmem, num_str);
158     wrd->term_len = strlen(wrd->term_buf);
159     return 1;
160 }
161
162 static int sp_expr(struct source_parser *sp, data1_node *n, RecWord *wrd)
163 {
164     if (sp->lookahead != 't')
165         return 0;
166     if (sp->len == 4 && !memcmp(sp->tok, "data", sp->len))
167     {
168         if (n->which == DATA1N_data)
169         {
170             wrd->term_buf = n->u.data.data;
171             wrd->term_len = n->u.data.len;
172         }
173         sp_lex(sp);
174     }
175     else if (sp->len == 3 && !memcmp(sp->tok, "tag", sp->len))
176     {
177         if (n->which == DATA1N_tag)
178         {               
179             wrd->term_buf = n->u.tag.tag;
180             wrd->term_len = strlen(n->u.tag.tag);
181         }
182         sp_lex(sp);
183     }
184     else if (sp->len == 4 && !memcmp(sp->tok, "attr", sp->len))
185     {
186         RecWord tmp_w;
187         sp_lex(sp);
188         if (sp->lookahead != '(')
189             return 0;
190         sp_lex(sp);
191
192         if (!sp_expr(sp, n, &tmp_w))
193             return 0;
194         
195         wrd->term_buf = "";
196         wrd->term_len = 0;
197         if (n->which == DATA1N_tag)
198         {
199             data1_xattr *p = n->u.tag.attributes;
200             while (p && strlen(p->name) != tmp_w.term_len && 
201                    memcmp (p->name, tmp_w.term_buf, tmp_w.term_len))
202                 p = p->next;
203             if (p)
204             {
205                 wrd->term_buf = p->value;
206                 wrd->term_len = strlen(p->value);
207             }
208         }
209         if (sp->lookahead != ')')
210             return 0;
211         sp_lex(sp);
212     }
213     else if (sp->len == 5 && !memcmp(sp->tok, "first", sp->len))
214     {
215         return sp_first(sp, n, wrd);
216     }
217     else if (sp->len == 5 && !memcmp(sp->tok, "range", sp->len))
218     {
219         return sp_range(sp, n, wrd);
220     }
221     else if (sp->len > 0 && isdigit(*(unsigned char *)sp->tok))
222     {
223         char *b;
224         wrd->term_len = sp->len;
225         b = nmem_malloc(sp->nmem, sp->len);
226         memcpy(b, sp->tok, sp->len);
227         wrd->term_buf = b;
228         sp_lex(sp);
229     }
230     else if (sp->len > 2 && sp->tok[0] == '\'' && sp->tok[sp->len-1] == '\'')
231     {
232         char *b;
233         wrd->term_len = sp->len - 2;
234         b = nmem_malloc(sp->nmem, wrd->term_len);
235         memcpy(b, sp->tok+1, wrd->term_len);
236         wrd->term_buf = b;
237         sp_lex(sp);
238     }
239     else 
240     {
241         wrd->term_buf = "";
242         wrd->term_len = 0;
243         sp_lex(sp);
244     }
245     return 1;
246 }
247
248 static struct source_parser *source_parser_create()
249 {
250     struct source_parser *sp = xmalloc(sizeof(*sp));
251
252     sp->nmem = nmem_create();
253     return sp;
254 }
255
256 static void source_parser_destroy(struct source_parser *sp)
257 {
258     if (!sp)
259         return;
260     nmem_destroy(sp->nmem);
261     xfree(sp);
262 }
263     
264 static int sp_parse(struct source_parser *sp, 
265                     data1_node *n, RecWord *wrd, const char *src)
266 {
267     sp->len = 0;
268     sp->tok = 0;
269     sp->src = src;
270     sp->lookahead = 0;
271     nmem_reset(sp->nmem);
272
273     sp_lex(sp);
274     return sp_expr(sp, n, wrd);
275 }
276
277 int d1_check_xpath_predicate(data1_node *n, struct xpath_predicate *p)
278 {
279     int res = 1;
280     char *attname;
281     data1_xattr *attr;
282     
283     if (!p) {
284         return 1;
285     } else {
286         if (p->which == XPATH_PREDICATE_RELATION) {
287             if (p->u.relation.name[0]) {
288                 if (*p->u.relation.name != '@') {
289                     yaz_log(YLOG_WARN, 
290                          "  Only attributes (@) are supported in xelm xpath predicates");
291                     yaz_log(YLOG_WARN, "predicate %s ignored", p->u.relation.name);
292                     return 1;
293                 }
294                 attname = p->u.relation.name + 1;
295                 res = 0;
296                 /* looking for the attribute with a specified name */
297                 for (attr = n->u.tag.attributes; attr; attr = attr->next) {
298                     yaz_log(YLOG_DEBUG,"  - attribute %s <-> %s", attname, attr->name );
299                     
300                     if (!strcmp(attr->name, attname)) {
301                         if (p->u.relation.op[0]) {
302                             if (*p->u.relation.op != '=') {
303                                 yaz_log(YLOG_WARN, 
304                                      "Only '=' relation is supported (%s)",p->u.relation.op);
305                                 yaz_log(YLOG_WARN, "predicate %s ignored", p->u.relation.name);
306                                 res = 1; break;
307                             } else {
308                                 yaz_log(YLOG_DEBUG,"    - value %s <-> %s", 
309                                      p->u.relation.value, attr->value );
310                                 if (!strcmp(attr->value, p->u.relation.value)) {
311                                     res = 1; break;
312                                 } 
313                             }
314                         } else {
315                             /* attribute exists, no value specified */
316                             res = 1; break;
317                         }
318                     }
319                 }
320                 yaz_log(YLOG_DEBUG, "return %d", res);
321                 return res;
322             } else {
323                 return 1;
324             }
325         } 
326         else if (p->which == XPATH_PREDICATE_BOOLEAN) {
327             if (!strcmp(p->u.boolean.op,"and")) {
328                 return d1_check_xpath_predicate(n, p->u.boolean.left) 
329                     && d1_check_xpath_predicate(n, p->u.boolean.right); 
330             }
331             else if (!strcmp(p->u.boolean.op,"or")) {
332                 return (d1_check_xpath_predicate(n, p->u.boolean.left) 
333                         || d1_check_xpath_predicate(n, p->u.boolean.right)); 
334             } else {
335                 yaz_log(YLOG_WARN, "Unknown boolean relation %s, ignored",p->u.boolean.op);
336                 return 1;
337             }
338         }
339     }
340     return 0;
341 }
342
343
344 static int dfa_match_first(struct DFA_state **dfaar, const char *text)
345 {
346     struct DFA_state *s = dfaar[0]; /* start state */
347     struct DFA_tran *t;
348     int i;
349     const char *p = text;
350     unsigned char c;
351     
352     for (c = *p++, t = s->trans, i = s->tran_no; --i >= 0; t++)
353     {
354         if (c >= t->ch[0] && c <= t->ch[1])
355         {
356             while (i >= 0)
357             {
358                 /* move to next state and return if we get a match */
359                 s = dfaar[t->to];
360                 if (s->rule_no)
361                     return 1;
362                 /* next char */
363                 if (!c)
364                     return 0;
365                 c = *p++;
366                 for (t = s->trans, i = s->tran_no; --i >= 0; t++)
367                     if (c >= t->ch[0] && c <= t->ch[1])
368                         break;
369             }
370         }
371     }
372     return 0;
373 }
374
375 /* *ostrich*
376    
377 New function, looking for xpath "element" definitions in abs, by
378 tagpath, using a kind of ugly regxp search.The DFA was built while
379 parsing abs, so here we just go trough them and try to match
380 against the given tagpath. The first matching entry is returned.
381
382 pop, 2002-12-13
383
384 Added support for enhanced xelm. Now [] predicates are considered
385 as well, when selecting indexing rules... (why the hell it's called
386 termlist???)
387
388 pop, 2003-01-17
389
390 */
391
392 data1_termlist *xpath_termlist_by_tagpath(char *tagpath, data1_node *n)
393 {
394     data1_absyn *abs = n->root->u.root.absyn;
395     data1_xpelement *xpe = abs->xp_elements;
396     data1_node *nn;
397 #ifdef ENHANCED_XELM 
398     struct xpath_location_step *xp;
399 #endif
400     char *pexpr = xmalloc(strlen(tagpath)+5);
401     int ok = 0;
402     
403     sprintf (pexpr, "/%s\n", tagpath);
404     for (; xpe; xpe = xpe->next)
405     {
406         int i;
407         ok = dfa_match_first(xpe->dfa->states, pexpr);
408
409         if (ok) {
410 #ifdef ENHANCED_XELM 
411             /* we have to check the perdicates up to the root node */
412             xp = xpe->xpath;
413             
414             /* find the first tag up in the node structure */
415             for (nn = n; nn && nn->which != DATA1N_tag; nn = nn->parent)
416                 ;
417             
418             /* go from inside out in the node structure, while going
419                backwards trough xpath location steps ... */
420             for (i = xpe->xpath_len - 1; i>0; i--)
421             {
422                 yaz_log(YLOG_DEBUG, "Checking step %d: %s on tag %s",
423                         i, xp[i].part, nn->u.tag.tag);
424                 
425                 if (!d1_check_xpath_predicate(nn, xp[i].predicate))
426                 {
427                     yaz_log(YLOG_DEBUG, "  Predicates didn't match");
428                     ok = 0;
429                     break;
430                 }
431                 
432                 if (nn->which == DATA1N_tag)
433                     nn = nn->parent;
434             }
435 #endif
436             if (ok)
437                 break;
438         }
439     } 
440     
441     xfree(pexpr);
442     
443     if (ok) {
444         yaz_log(YLOG_DEBUG, "Got it");
445         return xpe->termlists;
446     } else {
447         return NULL;
448     }
449 }
450
451 /* use
452      1   start element (tag)
453      2   end element
454      3   start attr (and attr-exact)
455      4   end attr
456
457   1016   cdata
458   1015   attr data
459
460   *ostrich*
461
462   Now, if there is a matching xelm described in abs, for the
463   indexed element or the attribute,  then the data is handled according 
464   to those definitions...
465
466   modified by pop, 2002-12-13
467 */
468
469 /* add xpath index for an attribute */
470 static void index_xpath_attr (char *tag_path, char *name, char *value,
471                               char *structure, struct recExtractCtrl *p,
472                               RecWord *wrd)
473 {
474     wrd->index_name = ZEBRA_XPATH_ELM_BEGIN;
475     wrd->index_type = '0';
476     wrd->term_buf = tag_path;
477     wrd->term_len = strlen(tag_path);
478     (*p->tokenAdd)(wrd);
479     
480     if (value) {
481         wrd->index_name = ZEBRA_XPATH_ATTR_CDATA;
482         wrd->index_type = 'w';
483         wrd->term_buf = value;
484         wrd->term_len = strlen(value);
485         (*p->tokenAdd)(wrd);
486     }
487     wrd->index_name = ZEBRA_XPATH_ELM_END;
488     wrd->index_type = '0';
489     wrd->term_buf = tag_path;
490     wrd->term_len = strlen(tag_path);
491     (*p->tokenAdd)(wrd);
492 }
493
494
495 static void mk_tag_path_full(char *tag_path_full, size_t max, data1_node *n)
496 {
497     size_t flen = 0;
498     data1_node *nn;
499
500     /* we have to fetch the whole path to the data tag */
501     for (nn = n; nn; nn = nn->parent)
502     {
503         if (nn->which == DATA1N_tag)
504         {
505             size_t tlen = strlen(nn->u.tag.tag);
506             if (tlen + flen > (max - 2))
507                 break;
508             memcpy (tag_path_full + flen, nn->u.tag.tag, tlen);
509             flen += tlen;
510             tag_path_full[flen++] = '/';
511         }
512         else
513             if (nn->which == DATA1N_root)
514                 break;
515     }
516     tag_path_full[flen] = 0;
517 }
518         
519
520 static void index_xpath(struct source_parser *sp, data1_node *n,
521                         struct recExtractCtrl *p,
522                         int level, RecWord *wrd,
523                         char *xpath_index,
524                         int xpath_is_start
525     )
526 {
527     int i;
528     char tag_path_full[1024];
529     int termlist_only = 1;
530     data1_termlist *tl;
531     int xpdone = 0;
532     if ((!n->root->u.root.absyn) ||
533         (n->root->u.root.absyn->xpath_indexing == DATA1_XPATH_INDEXING_ENABLE)) {
534         termlist_only = 0;
535     }
536
537     switch (n->which)
538     {
539     case DATA1N_data:
540         wrd->term_buf = n->u.data.data;
541         wrd->term_len = n->u.data.len;
542         xpdone = 0;
543
544         mk_tag_path_full(tag_path_full, sizeof(tag_path_full), n);
545         
546         /* If we have a matching termlist... */
547         if (n->root->u.root.absyn && 
548             (tl = xpath_termlist_by_tagpath(tag_path_full, n)))
549         {
550             for (; tl; tl = tl->next)
551             {
552                 /* need to copy recword because it may be changed */
553                 RecWord wrd_tl;
554                 wrd->index_type = *tl->structure;
555                 memcpy (&wrd_tl, wrd, sizeof(*wrd));
556                 if (tl->source)
557                     sp_parse(sp, n, &wrd_tl, tl->source);
558                 if (!tl->index_name)
559                 {
560                     /* this is the ! case, so structure is for the xpath index */
561                     wrd_tl.index_name = xpath_index;
562                     if (p->flagShowRecords)
563                     {
564                         int i;
565                         printf("%*sXPath index", (level + 1) * 4, "");
566                         printf (" XData:\"");
567                         for (i = 0; i<wrd_tl.term_len && i < 40; i++)
568                             fputc (wrd_tl.term_buf[i], stdout);
569                         fputc ('"', stdout);
570                         if (wrd_tl.term_len > 40)
571                             printf (" ...");
572                         fputc ('\n', stdout);
573                     }
574                     else
575                         (*p->tokenAdd)(&wrd_tl);
576                     xpdone = 1;
577                 } else {
578                     /* this is just the old fashioned attribute based index */
579                     wrd_tl.index_name = tl->index_name;
580                     if (p->flagShowRecords)
581                     {
582                         int i;
583                         printf("%*sIdx: [%s]", (level + 1) * 4, "",
584                                tl->structure);
585                         printf("%s %s", tl->index_name, tl->source);
586                         printf (" XData:\"");
587                         for (i = 0; i<wrd_tl.term_len && i < 40; i++)
588                             fputc (wrd_tl.term_buf[i], stdout);
589                         fputc ('"', stdout);
590                         if (wrd_tl.term_len > 40)
591                             printf (" ...");
592                         fputc ('\n', stdout);
593                     }
594                     else
595                         (*p->tokenAdd)(&wrd_tl);
596                 }
597             }
598         }
599         /* xpath indexing is done, if there was no termlist given, 
600            or no ! in the termlist, and default indexing is enabled... */
601         if (!p->flagShowRecords && !xpdone && !termlist_only)
602         {
603             wrd->index_name = xpath_index;
604             wrd->index_type = 'w';
605             (*p->tokenAdd)(wrd);
606         }
607         break;
608     case DATA1N_tag:
609         mk_tag_path_full(tag_path_full, sizeof(tag_path_full), n);
610
611         wrd->index_type = '0';
612         wrd->term_buf = tag_path_full;
613         wrd->term_len = strlen(tag_path_full);
614         wrd->index_name = xpath_index;
615         if (p->flagShowRecords)
616         {
617             printf("%*s tag=", (level + 1) * 4, "");
618             for (i = 0; i<wrd->term_len && i < 40; i++)
619                 fputc (wrd->term_buf[i], stdout);
620             if (i == 40)
621                 printf (" ..");
622             printf("\n");
623         }
624         else
625         {
626             data1_xattr *xp;
627             data1_termlist *tl;
628             int do_xpindex;
629             
630             /* Add tag start/end xpath index, only when there is a ! in
631                the apropriate xelm directive, or default xpath indexing
632                is enabled 
633             */
634             if (!(do_xpindex = 1 - termlist_only))
635             {
636                 if ((tl = xpath_termlist_by_tagpath(tag_path_full, n))) 
637                 {
638                     for (; tl; tl = tl->next) 
639                     {
640                         if (!tl->index_name)
641                             do_xpindex = 1;
642                     }
643                 }
644             }
645             if (do_xpindex) {
646                 (*p->tokenAdd)(wrd);   /* index element pag (AKA tag path) */
647             }
648             
649             if (xpath_is_start == 1) /* only for the starting tag... */
650             {
651 #define MAX_ATTR_COUNT 50
652                 data1_termlist *tll[MAX_ATTR_COUNT];
653                 
654                 int i = 0;
655                 
656                 /* get termlists for attributes, and find out, if we have to do xpath indexing */
657                 for (xp = n->u.tag.attributes; xp; xp = xp->next) {
658                     i++;
659                 }
660                 
661                 i = 0;
662                 for (xp = n->u.tag.attributes; xp; xp = xp->next) {
663                     char comb[512];
664                     int do_xpindex = 1 - termlist_only;
665                     data1_termlist *tl;
666                     char attr_tag_path_full[1024]; 
667                     
668                     /* this could be cached as well */
669                     sprintf (attr_tag_path_full, "@%s/%s",
670                              xp->name, tag_path_full);
671                     
672                     tll[i] = xpath_termlist_by_tagpath(attr_tag_path_full,n);
673                     
674                     /* if there is a ! in the xelm termlist, or default indexing is on, 
675                        proceed with xpath idx */
676                     if ((tl = tll[i]))
677                     {
678                         for (; tl; tl = tl->next)
679                         {
680                             if (!tl->index_name)
681                                 do_xpindex = 1;
682                         }
683                     }
684                     
685                     if (do_xpindex) {
686                         
687                         /* attribute  (no value) */
688                         wrd->index_type = '0';
689                         wrd->index_name = ZEBRA_XPATH_ATTR_NAME;
690                         wrd->term_buf = xp->name;
691                         wrd->term_len = strlen(xp->name);
692                         
693                         wrd->seqno--;
694                         (*p->tokenAdd)(wrd);
695                         
696                         if (xp->value &&
697                             strlen(xp->name) + strlen(xp->value) < sizeof(comb)-2) {
698                             
699                             /* attribute value exact */
700                             strcpy (comb, xp->name);
701                             strcat (comb, "=");
702                             strcat (comb, xp->value);
703
704                             wrd->index_name = ZEBRA_XPATH_ATTR_NAME;
705                             wrd->index_type = '0';
706                             wrd->term_buf = comb;
707                             wrd->term_len = strlen(comb);
708                             wrd->seqno--;
709                             
710                             (*p->tokenAdd)(wrd);
711                         }
712                     }                
713                     i++;
714                 }
715                 
716                 i = 0;
717                 for (xp = n->u.tag.attributes; xp; xp = xp->next) {
718                     data1_termlist *tl;
719                     char attr_tag_path_full[1024];
720                     int xpdone = 0;
721                     
722                     sprintf (attr_tag_path_full, "@%s/%s",
723                              xp->name, tag_path_full);
724                     
725                     if ((tl = tll[i]))
726                     {
727                         /* If there is a termlist given (=xelm directive) */
728                         for (; tl; tl = tl->next)
729                         {
730                             if (!tl->index_name)
731                             {
732                                 /* add xpath index for the attribute */
733                                 index_xpath_attr (attr_tag_path_full, xp->name,
734                                                   xp->value, tl->structure,
735                                                   p, wrd);
736                                 xpdone = 1;
737                             } else {
738                                 /* index attribute value (only path/@attr) */
739                                 if (xp->value) 
740                                 {
741                                     wrd->index_name = tl->index_name;
742                                     wrd->index_type = *tl->structure;
743                                     wrd->term_buf = xp->value;
744                                     wrd->term_len = strlen(xp->value);
745                                     (*p->tokenAdd)(wrd);
746                                 }
747                             }
748                         }
749                     }
750                     /* if there was no termlist for the given path, 
751                        or the termlist didn't have a ! element, index 
752                        the attribute as "w" */
753                     if ((!xpdone) && (!termlist_only))
754                     {
755                         index_xpath_attr (attr_tag_path_full, xp->name,
756                                           xp->value,  "w", p, wrd);
757                     }
758                     i++;
759                 }
760             }
761         }
762     }
763 }
764
765 static void index_termlist (struct source_parser *sp, data1_node *par,
766                             data1_node *n,
767                             struct recExtractCtrl *p, int level, RecWord *wrd)
768 {
769     data1_termlist *tlist = 0;
770     data1_datatype dtype = DATA1K_string;
771
772     /*
773      * cycle up towards the root until we find a tag with an att..
774      * this has the effect of indexing locally defined tags with
775      * the attribute of their ancestor in the record.
776      */
777     
778     while (!par->u.tag.element)
779         if (!par->parent || !(par=get_parent_tag(p->dh, par->parent)))
780             break;
781     if (!par || !(tlist = par->u.tag.element->termlists))
782         return;
783     if (par->u.tag.element->tag)
784         dtype = par->u.tag.element->tag->kind;
785
786     for (; tlist; tlist = tlist->next)
787     {
788         /* consider source */
789         wrd->term_buf = 0;
790         assert(tlist->source);
791         sp_parse(sp, n, wrd, tlist->source);
792
793         if (wrd->term_buf && wrd->term_len)
794         {
795             if (p->flagShowRecords)
796             {
797                 int i;
798                 printf("%*sIdx: [%s]", (level + 1) * 4, "",
799                        tlist->structure);
800                 printf("%s %s", tlist->index_name, tlist->source);
801                 printf (" XData:\"");
802                 for (i = 0; i<wrd->term_len && i < 40; i++)
803                     fputc (wrd->term_buf[i], stdout);
804                 fputc ('"', stdout);
805                 if (wrd->term_len > 40)
806                     printf (" ...");
807                 fputc ('\n', stdout);
808             }
809             else
810             {
811                 wrd->index_type = *tlist->structure;
812                 wrd->index_name = tlist->index_name;
813                 (*p->tokenAdd)(wrd);
814             }
815         }
816     }
817 }
818
819 static int dumpkeys_r(struct source_parser *sp,
820                       data1_node *n, struct recExtractCtrl *p, int level,
821                       RecWord *wrd)
822 {
823     for (; n; n = n->next)
824     {
825         if (p->flagShowRecords) /* display element description to user */
826         {
827             if (n->which == DATA1N_root)
828             {
829                 printf("%*s", level * 4, "");
830                 printf("Record type: '%s'\n", n->u.root.type);
831             }
832             else if (n->which == DATA1N_tag)
833             {
834                 data1_element *e;
835
836                 printf("%*s", level * 4, "");
837                 if (!(e = n->u.tag.element))
838                     printf("Local tag: '%s'\n", n->u.tag.tag);
839                 else
840                 {
841                     printf("Elm: '%s' ", e->name);
842                     if (e->tag)
843                     {
844                         data1_tag *t = e->tag;
845
846                         printf("TagNam: '%s' ", t->names->name);
847                         printf("(");
848                         if (t->tagset)
849                             printf("%s[%d],", t->tagset->name, t->tagset->type);
850                         else
851                             printf("?,");
852                         if (t->which == DATA1T_numeric)
853                             printf("%d)", t->value.numeric);
854                         else
855                             printf("'%s')", t->value.string);
856                     }
857                     printf("\n");
858                 }
859             }
860         }
861
862         if (n->which == DATA1N_tag)
863         {
864             index_termlist(sp, n, n, p, level, wrd);
865             /* index start tag */
866             if (n->root->u.root.absyn)
867                 index_xpath(sp, n, p, level, wrd, ZEBRA_XPATH_ELM_BEGIN, 
868                             1 /* is start */);
869         }
870
871         if (n->child)
872             if (dumpkeys_r(sp, n->child, p, level + 1, wrd) < 0)
873                 return -1;
874
875
876         if (n->which == DATA1N_data)
877         {
878             data1_node *par = get_parent_tag(p->dh, n);
879
880             if (p->flagShowRecords)
881             {
882                 printf("%*s", level * 4, "");
883                 printf("Data: ");
884                 if (n->u.data.len > 256)
885                     printf("'%.170s ... %.70s'\n", n->u.data.data,
886                            n->u.data.data + n->u.data.len-70);
887                 else if (n->u.data.len > 0)
888                     printf("'%.*s'\n", n->u.data.len, n->u.data.data);
889                 else
890                     printf("NULL\n");
891             }
892
893             if (par)
894                 index_termlist(sp, par, n, p, level, wrd);
895
896             index_xpath(sp, n, p, level, wrd, ZEBRA_XPATH_CDATA, 
897                         0 /* is start */);
898         }
899
900         if (n->which == DATA1N_tag)
901         {
902             /* index end tag */
903             index_xpath(sp, n, p, level, wrd, ZEBRA_XPATH_ELM_END, 
904                         0 /* is start */);
905         }
906
907         if (p->flagShowRecords && n->which == DATA1N_root)
908         {
909             printf("%*s-------------\n\n", level * 4, "");
910         }
911     }
912     return 0;
913 }
914
915 static int dumpkeys(data1_node *n, struct recExtractCtrl *p, RecWord *wrd)
916 {
917     struct source_parser *sp = source_parser_create();
918     int r = dumpkeys_r(sp, n, p, 0, wrd);
919     source_parser_destroy(sp);
920     return r;
921 }
922
923 int grs_extract_tree(struct recExtractCtrl *p, data1_node *n)
924 {
925     oident oe;
926     int oidtmp[OID_SIZE];
927     RecWord wrd;
928
929     oe.proto = PROTO_Z3950;
930     oe.oclass = CLASS_SCHEMA;
931     if (n->u.root.absyn)
932     {
933         oe.value = n->u.root.absyn->reference;
934         
935         if ((oid_ent_to_oid (&oe, oidtmp)))
936             (*p->schemaAdd)(p, oidtmp);
937     }
938     (*p->init)(p, &wrd);
939
940     return dumpkeys(n, p, &wrd);
941 }
942
943 static int grs_extract_sub(void *clientData, struct recExtractCtrl *p,
944                            NMEM mem,
945                            data1_node *(*grs_read)(struct grs_read_info *))
946 {
947     data1_node *n;
948     struct grs_read_info gri;
949     oident oe;
950     int oidtmp[OID_SIZE];
951     RecWord wrd;
952
953     gri.readf = p->readf;
954     gri.seekf = p->seekf;
955     gri.tellf = p->tellf;
956     gri.endf = p->endf;
957     gri.fh = p->fh;
958     gri.offset = p->offset;
959     gri.mem = mem;
960     gri.dh = p->dh;
961     gri.clientData = clientData;
962
963     n = (*grs_read)(&gri);
964     if (!n)
965         return RECCTRL_EXTRACT_EOF;
966     oe.proto = PROTO_Z3950;
967     oe.oclass = CLASS_SCHEMA;
968 #if 0
969     if (!n->u.root.absyn)
970         return RECCTRL_EXTRACT_ERROR;
971 #endif
972     if (n->u.root.absyn)
973     {
974         oe.value = n->u.root.absyn->reference;
975         if ((oid_ent_to_oid (&oe, oidtmp)))
976             (*p->schemaAdd)(p, oidtmp);
977     }
978     data1_concat_text(p->dh, mem, n);
979
980     /* ensure our data1 tree is UTF-8 */
981     data1_iconv (p->dh, mem, n, "UTF-8", data1_get_encoding(p->dh, n));
982
983 #if 0
984     data1_pr_tree (p->dh, n, stdout);
985 #endif
986
987     (*p->init)(p, &wrd);
988     if (dumpkeys(n, p, &wrd) < 0)
989     {
990         data1_free_tree(p->dh, n);
991         return RECCTRL_EXTRACT_ERROR_GENERIC;
992     }
993     data1_free_tree(p->dh, n);
994     return RECCTRL_EXTRACT_OK;
995 }
996
997 int zebra_grs_extract(void *clientData, struct recExtractCtrl *p,
998                       data1_node *(*grs_read)(struct grs_read_info *))
999 {
1000     int ret;
1001     NMEM mem = nmem_create ();
1002     ret = grs_extract_sub(clientData, p, mem, grs_read);
1003     nmem_destroy(mem);
1004     return ret;
1005 }
1006
1007 /*
1008  * Return: -1: Nothing done. 0: Ok. >0: Bib-1 diagnostic.
1009  */
1010 static int process_comp(data1_handle dh, data1_node *n, Z_RecordComposition *c,
1011                         char **addinfo, ODR o)
1012 {
1013     data1_esetname *eset;
1014     Z_Espec1 *espec = 0;
1015     Z_ElementSpec *p;
1016
1017     switch (c->which)
1018     {
1019     case Z_RecordComp_simple:
1020         if (c->u.simple->which != Z_ElementSetNames_generic)
1021             return 26; /* only generic form supported. Fix this later */
1022         if (!(eset = data1_getesetbyname(dh, n->u.root.absyn,
1023                                          c->u.simple->u.generic)))
1024         {
1025             yaz_log(YLOG_LOG, "Unknown esetname '%s'", c->u.simple->u.generic);
1026             *addinfo = odr_strdup(o, c->u.simple->u.generic);
1027             return 25; /* invalid esetname */
1028         }
1029         yaz_log(YLOG_DEBUG, "Esetname '%s' in simple compspec",
1030              c->u.simple->u.generic);
1031         espec = eset->spec;
1032         break;
1033     case Z_RecordComp_complex:
1034         if (c->u.complex->generic)
1035         {
1036             /* insert check for schema */
1037             if ((p = c->u.complex->generic->elementSpec))
1038             {
1039                 switch (p->which)
1040                 {
1041                 case Z_ElementSpec_elementSetName:
1042                     if (!(eset =
1043                           data1_getesetbyname(dh, n->u.root.absyn,
1044                                               p->u.elementSetName)))
1045                     {
1046                         yaz_log(YLOG_DEBUG, "Unknown esetname '%s'",
1047                              p->u.elementSetName);
1048                         *addinfo = odr_strdup(o, p->u.elementSetName);
1049                         return 25; /* invalid esetname */
1050                     }
1051                     yaz_log(YLOG_DEBUG, "Esetname '%s' in complex compspec",
1052                          p->u.elementSetName);
1053                     espec = eset->spec;
1054                     break;
1055                 case Z_ElementSpec_externalSpec:
1056                     if (p->u.externalSpec->which == Z_External_espec1)
1057                     {
1058                         yaz_log(YLOG_DEBUG, "Got Espec-1");
1059                         espec = p->u.externalSpec-> u.espec1;
1060                     }
1061                     else
1062                     {
1063                         yaz_log(YLOG_LOG, "Unknown external espec.");
1064                         return 25; /* bad. what is proper diagnostic? */
1065                     }
1066                     break;
1067                 }
1068             }
1069         }
1070         else
1071             return 26; /* fix */
1072     }
1073     if (espec)
1074     {
1075         yaz_log(YLOG_DEBUG, "Element: Espec-1 match");
1076         return data1_doespec1(dh, n, espec);
1077     }
1078     else
1079     {
1080         yaz_log(YLOG_DEBUG, "Element: all match");
1081         return -1;
1082     }
1083 }
1084
1085 /* Add Zebra info in separate namespace ...
1086         <root 
1087          ...
1088          <metadata xmlns="http://www.indexdata.dk/zebra/">
1089           <size>359</size>
1090           <localnumber>447</localnumber>
1091           <filename>records/genera.xml</filename>
1092          </metadata>
1093         </root>
1094 */
1095
1096 static void zebra_xml_metadata (struct recRetrieveCtrl *p, data1_node *top,
1097                                 NMEM mem)
1098 {
1099     const char *idzebra_ns[3];
1100     const char *i2 = "\n  ";
1101     const char *i4 = "\n    ";
1102     data1_node *n;
1103
1104     idzebra_ns[0] = "xmlns";
1105     idzebra_ns[1] = "http://www.indexdata.dk/zebra/";
1106     idzebra_ns[2] = 0;
1107
1108     data1_mk_text (p->dh, mem, i2, top);
1109
1110     n = data1_mk_tag (p->dh, mem, "idzebra", idzebra_ns, top);
1111
1112     data1_mk_text (p->dh, mem, "\n", top);
1113
1114     data1_mk_text (p->dh, mem, i4, n);
1115     
1116     data1_mk_tag_data_int (p->dh, n, "size", p->recordSize, mem);
1117
1118     if (p->score != -1)
1119     {
1120         data1_mk_text (p->dh, mem, i4, n);
1121         data1_mk_tag_data_int (p->dh, n, "score", p->score, mem);
1122     }
1123     data1_mk_text (p->dh, mem, i4, n);
1124     data1_mk_tag_data_zint (p->dh, n, "localnumber", p->localno, mem);
1125     if (p->fname)
1126     {
1127         data1_mk_text (p->dh, mem, i4, n);
1128         data1_mk_tag_data_text(p->dh, n, "filename", p->fname, mem);
1129     }
1130     data1_mk_text (p->dh, mem, i2, n);
1131 }
1132
1133 int zebra_grs_retrieve(void *clientData, struct recRetrieveCtrl *p,
1134                        data1_node *(*grs_read)(struct grs_read_info *))
1135 {
1136     data1_node *node = 0, *onode = 0, *top;
1137     data1_node *dnew;
1138     data1_maptab *map;
1139     int res, selected = 0;
1140     NMEM mem;
1141     struct grs_read_info gri;
1142     const char *tagname;
1143
1144     int requested_schema = VAL_NONE;
1145     data1_marctab *marctab;
1146     int dummy;
1147     
1148     mem = nmem_create();
1149     gri.readf = p->readf;
1150     gri.seekf = p->seekf;
1151     gri.tellf = p->tellf;
1152     gri.endf = NULL;
1153     gri.fh = p->fh;
1154     gri.offset = 0;
1155     gri.mem = mem;
1156     gri.dh = p->dh;
1157     gri.clientData = clientData;
1158
1159     yaz_log(YLOG_DEBUG, "grs_retrieve");
1160     node = (*grs_read)(&gri);
1161     if (!node)
1162     {
1163         p->diagnostic = 14;
1164         nmem_destroy (mem);
1165         return 0;
1166     }
1167     data1_concat_text(p->dh, mem, node);
1168
1169 #if 0
1170     data1_pr_tree (p->dh, node, stdout);
1171 #endif
1172     top = data1_get_root_tag (p->dh, node);
1173
1174     yaz_log(YLOG_DEBUG, "grs_retrieve: size");
1175     tagname = data1_systag_lookup(node->u.root.absyn, "size", "size");
1176     if (tagname &&
1177         (dnew = data1_mk_tag_data_wd(p->dh, top, tagname, mem)))
1178     {
1179         dnew->u.data.what = DATA1I_text;
1180         dnew->u.data.data = dnew->lbuf;
1181         sprintf(dnew->u.data.data, "%d", p->recordSize);
1182         dnew->u.data.len = strlen(dnew->u.data.data);
1183     }
1184     
1185     tagname = data1_systag_lookup(node->u.root.absyn, "rank", "rank");
1186     if (tagname && p->score >= 0 &&
1187         (dnew = data1_mk_tag_data_wd(p->dh, top, tagname, mem)))
1188     {
1189         yaz_log(YLOG_DEBUG, "grs_retrieve: %s", tagname);
1190         dnew->u.data.what = DATA1I_num;
1191         dnew->u.data.data = dnew->lbuf;
1192         sprintf(dnew->u.data.data, "%d", p->score);
1193         dnew->u.data.len = strlen(dnew->u.data.data);
1194     }
1195
1196     tagname = data1_systag_lookup(node->u.root.absyn, "sysno",
1197                                   "localControlNumber");
1198     if (tagname && p->localno > 0 &&
1199         (dnew = data1_mk_tag_data_wd(p->dh, top, tagname, mem)))
1200     {
1201         yaz_log(YLOG_DEBUG, "grs_retrieve: %s", tagname);
1202         dnew->u.data.what = DATA1I_text;
1203         dnew->u.data.data = dnew->lbuf;
1204         
1205         sprintf(dnew->u.data.data, ZINT_FORMAT, p->localno);
1206         dnew->u.data.len = strlen(dnew->u.data.data);
1207     }
1208
1209     if (p->input_format == VAL_TEXT_XML)
1210        zebra_xml_metadata (p, top, mem);
1211
1212 #if 0
1213     data1_pr_tree (p->dh, node, stdout);
1214 #endif
1215     if (p->comp && p->comp->which == Z_RecordComp_complex &&
1216         p->comp->u.complex->generic &&
1217         p->comp->u.complex->generic->which == Z_Schema_oid &&
1218         p->comp->u.complex->generic->schema.oid)
1219     {
1220         oident *oe = oid_getentbyoid (p->comp->u.complex->generic->schema.oid);
1221         if (oe)
1222             requested_schema = oe->value;
1223     }
1224     /* If schema has been specified, map if possible, then check that
1225      * we got the right one 
1226      */
1227     if (requested_schema != VAL_NONE)
1228     {
1229         yaz_log(YLOG_DEBUG, "grs_retrieve: schema mapping");
1230         for (map = node->u.root.absyn->maptabs; map; map = map->next)
1231         {
1232             if (map->target_absyn_ref == requested_schema)
1233             {
1234                 onode = node;
1235                 if (!(node = data1_map_record(p->dh, onode, map, mem)))
1236                 {
1237                     p->diagnostic = 14;
1238                     nmem_destroy (mem);
1239                     return 0;
1240                 }
1241                 break;
1242             }
1243         }
1244         if (node->u.root.absyn &&
1245             requested_schema != node->u.root.absyn->reference)
1246         {
1247             p->diagnostic = 238;
1248             nmem_destroy (mem);
1249             return 0;
1250         }
1251     }
1252     /*
1253      * Does the requested format match a known syntax-mapping? (this reflects
1254      * the overlap of schema and formatting which is inherent in the MARC
1255      * family)
1256      */
1257     yaz_log(YLOG_DEBUG, "grs_retrieve: syntax mapping");
1258     if (node->u.root.absyn)
1259         for (map = node->u.root.absyn->maptabs; map; map = map->next)
1260         {
1261             if (map->target_absyn_ref == p->input_format)
1262             {
1263                 onode = node;
1264                 if (!(node = data1_map_record(p->dh, onode, map, mem)))
1265                 {
1266                     p->diagnostic = 14;
1267                     nmem_destroy (mem);
1268                     return 0;
1269                 }
1270                 break;
1271             }
1272         }
1273     yaz_log(YLOG_DEBUG, "grs_retrieve: schemaIdentifier");
1274     if (node->u.root.absyn &&
1275         node->u.root.absyn->reference != VAL_NONE &&
1276         p->input_format == VAL_GRS1)
1277     {
1278         oident oe;
1279         Odr_oid *oid;
1280         int oidtmp[OID_SIZE];
1281         
1282         oe.proto = PROTO_Z3950;
1283         oe.oclass = CLASS_SCHEMA;
1284         oe.value = node->u.root.absyn->reference;
1285         
1286         if ((oid = oid_ent_to_oid (&oe, oidtmp)))
1287         {
1288             char tmp[128];
1289             data1_handle dh = p->dh;
1290             char *p = tmp;
1291             int *ii;
1292             
1293             for (ii = oid; *ii >= 0; ii++)
1294             {
1295                 if (p != tmp)
1296                         *(p++) = '.';
1297                 sprintf(p, "%d", *ii);
1298                 p += strlen(p);
1299             }
1300             if ((dnew = data1_mk_tag_data_wd(dh, top, 
1301                                              "schemaIdentifier", mem)))
1302             {
1303                 dnew->u.data.what = DATA1I_oid;
1304                 dnew->u.data.data = (char *) nmem_malloc(mem, p - tmp);
1305                 memcpy(dnew->u.data.data, tmp, p - tmp);
1306                 dnew->u.data.len = p - tmp;
1307             }
1308         }
1309     }
1310
1311     yaz_log(YLOG_DEBUG, "grs_retrieve: element spec");
1312     if (p->comp && (res = process_comp(p->dh, node, p->comp, &p->addinfo,
1313                                        p->odr)) > 0)
1314     {
1315         p->diagnostic = res;
1316         if (onode)
1317             data1_free_tree(p->dh, onode);
1318         data1_free_tree(p->dh, node);
1319         nmem_destroy(mem);
1320         return 0;
1321     }
1322     else if (p->comp && !res)
1323         selected = 1;
1324
1325 #if 0
1326     data1_pr_tree (p->dh, node, stdout);
1327 #endif
1328     yaz_log(YLOG_DEBUG, "grs_retrieve: transfer syntax mapping");
1329     switch (p->output_format = (p->input_format != VAL_NONE ?
1330                                 p->input_format : VAL_SUTRS))
1331     {
1332     case VAL_TEXT_XML:
1333 #if 0
1334         data1_pr_tree (p->dh, node, stdout);
1335 #endif
1336         /* default output encoding for XML is UTF-8 */
1337         data1_iconv (p->dh, mem, node,
1338                      p->encoding ? p->encoding : "UTF-8",
1339                      data1_get_encoding(p->dh, node));
1340
1341         if (!(p->rec_buf = data1_nodetoidsgml(p->dh, node, selected,
1342                                               &p->rec_len)))
1343             p->diagnostic = 238;
1344         else
1345         {
1346             char *new_buf = (char*) odr_malloc (p->odr, p->rec_len);
1347             memcpy (new_buf, p->rec_buf, p->rec_len);
1348             p->rec_buf = new_buf;
1349         }
1350         break;
1351     case VAL_GRS1:
1352         data1_iconv (p->dh, mem, node, "UTF-8", data1_get_encoding(p->dh, node));
1353         dummy = 0;
1354         if (!(p->rec_buf = data1_nodetogr(p->dh, node, selected,
1355                                           p->odr, &dummy)))
1356             p->diagnostic = 238; /* not available in requested syntax */
1357         else
1358             p->rec_len = -1;
1359         break;
1360     case VAL_EXPLAIN:
1361         /* ensure our data1 tree is UTF-8 */
1362         data1_iconv (p->dh, mem, node, "UTF-8", data1_get_encoding(p->dh, node));
1363         
1364         if (!(p->rec_buf = data1_nodetoexplain(p->dh, node, selected,
1365                                                p->odr)))
1366             p->diagnostic = 238;
1367         else
1368             p->rec_len = -1;
1369         break;
1370     case VAL_SUMMARY:
1371         /* ensure our data1 tree is UTF-8 */
1372         data1_iconv (p->dh, mem, node, "UTF-8", data1_get_encoding(p->dh, node));
1373         if (!(p->rec_buf = data1_nodetosummary(p->dh, node, selected,
1374                                                p->odr)))
1375             p->diagnostic = 238;
1376         else
1377             p->rec_len = -1;
1378         break;
1379     case VAL_SUTRS:
1380         if (p->encoding)
1381             data1_iconv (p->dh, mem, node, p->encoding,
1382                          data1_get_encoding(p->dh, node));
1383         if (!(p->rec_buf = data1_nodetobuf(p->dh, node, selected,
1384                                            &p->rec_len)))
1385             p->diagnostic = 238;
1386         else
1387         {
1388             char *new_buf = (char*) odr_malloc (p->odr, p->rec_len);
1389             memcpy (new_buf, p->rec_buf, p->rec_len);
1390             p->rec_buf = new_buf;
1391         }
1392         break;
1393     case VAL_SOIF:
1394         if (p->encoding)
1395             data1_iconv (p->dh, mem, node, p->encoding,
1396                          data1_get_encoding(p->dh, node));
1397         if (!(p->rec_buf = data1_nodetosoif(p->dh, node, selected,
1398                                             &p->rec_len)))
1399             p->diagnostic = 238;
1400         else
1401         {
1402             char *new_buf = (char*) odr_malloc (p->odr, p->rec_len);
1403             memcpy (new_buf, p->rec_buf, p->rec_len);
1404             p->rec_buf = new_buf;
1405         }
1406         break;
1407     default:
1408         if (!node->u.root.absyn)
1409         {
1410             p->diagnostic = 238;
1411             break;
1412         }
1413         for (marctab = node->u.root.absyn->marc; marctab;
1414              marctab = marctab->next)
1415             if (marctab->reference == p->input_format)
1416                 break;
1417         if (!marctab)
1418         {
1419             p->diagnostic = 238;
1420             break;
1421         }
1422         if (p->encoding)
1423             data1_iconv (p->dh, mem, node, p->encoding,
1424                          data1_get_encoding(p->dh, node));
1425         if (!(p->rec_buf = data1_nodetomarc(p->dh, marctab, node,
1426                                         selected, &p->rec_len)))
1427             p->diagnostic = 238;
1428         else
1429         {
1430             char *new_buf = (char*) odr_malloc (p->odr, p->rec_len);
1431             memcpy (new_buf, p->rec_buf, p->rec_len);
1432                 p->rec_buf = new_buf;
1433         }
1434     }
1435     if (node)
1436         data1_free_tree(p->dh, node);
1437     if (onode)
1438         data1_free_tree(p->dh, onode);
1439     nmem_destroy(mem);
1440     return 0;
1441 }
1442
1443 /*
1444  * Local variables:
1445  * c-basic-offset: 4
1446  * indent-tabs-mode: nil
1447  * End:
1448  * vim: shiftwidth=4 tabstop=8 expandtab
1449  */
1450