Testing of RPN to CQL conversion.
[yaz-moved-to-github.git] / src / cqltransform.c
1 /* This file is part of the YAZ toolkit.
2  * Copyright (C) 1995-2008 Index Data
3  * See the file LICENSE for details.
4  */
5
6 /**
7  * \file cqltransform.c
8  * \brief Implements CQL transform (CQL to RPN conversion).
9  *
10  * Evaluation order of rules:
11  *
12  * always
13  * relation
14  * structure
15  * position
16  * truncation
17  * index
18  * relationModifier
19  */
20
21 #include <assert.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <yaz/cql.h>
25 #include <yaz/xmalloc.h>
26 #include <yaz/diagsrw.h>
27 #include <yaz/tokenizer.h>
28 #include <yaz/wrbuf.h>
29
30 struct cql_prop_entry {
31     char *pattern;
32     char *value;
33     struct cql_prop_entry *next;
34 };
35
36 struct cql_transform_t_ {
37     struct cql_prop_entry *entry;
38     yaz_tok_cfg_t tok_cfg;
39     int error;
40     char *addinfo;
41     WRBUF w;
42 };
43
44
45 cql_transform_t cql_transform_create(void)
46 {
47     cql_transform_t ct = (cql_transform_t) xmalloc(sizeof(*ct));
48     ct->tok_cfg = yaz_tok_cfg_create();
49     ct->w = wrbuf_alloc();
50     ct->error = 0;
51     ct->addinfo = 0;
52     ct->entry = 0;
53     return ct;
54 }
55
56 cql_transform_t cql_transform_open_FILE(FILE *f)
57 {
58     cql_transform_t ct = cql_transform_create();
59     char line[1024];
60     struct cql_prop_entry **pp = &ct->entry;
61
62     yaz_tok_cfg_single_tokens(ct->tok_cfg, "=");
63
64     while (fgets(line, sizeof(line)-1, f))
65     {
66         yaz_tok_parse_t tp = yaz_tok_parse_buf(ct->tok_cfg, line);
67         int t;
68         wrbuf_rewind(ct->w);
69         t = yaz_tok_move(tp);
70         if (t == YAZ_TOK_STRING)
71         {
72             char * pattern = xstrdup(yaz_tok_parse_string(tp));
73             t = yaz_tok_move(tp);
74             if (t != '=')
75             {
76                 yaz_tok_parse_destroy(tp);
77                 cql_transform_close(ct);
78                 return 0;
79             }
80             t = yaz_tok_move(tp);
81
82             while (t == YAZ_TOK_STRING)
83             {
84                 /* attset type=value  OR  type=value */
85                 wrbuf_puts(ct->w, yaz_tok_parse_string(tp));
86                 t = yaz_tok_move(tp);
87                 if (t == YAZ_TOK_EOF)
88                     break;
89                 if (t == YAZ_TOK_STRING)  
90                 {  
91                     wrbuf_puts(ct->w, " ");
92                     wrbuf_puts(ct->w, yaz_tok_parse_string(tp));
93                     t = yaz_tok_move(tp);
94                 }
95                 if (t != '=')
96                 {
97                     yaz_tok_parse_destroy(tp);
98                     cql_transform_close(ct);
99                     return 0;
100                 }
101                 t = yaz_tok_move(tp);
102                 if (t != YAZ_TOK_STRING) /* value */
103                 {
104                     yaz_tok_parse_destroy(tp);
105                     cql_transform_close(ct);
106                     return 0;
107                 }
108                 wrbuf_puts(ct->w, "=");
109                 wrbuf_puts(ct->w, yaz_tok_parse_string(tp));
110                 t = yaz_tok_move(tp);
111                 wrbuf_puts(ct->w, " ");
112             }
113             *pp = (struct cql_prop_entry *) xmalloc(sizeof(**pp));
114             (*pp)->pattern = pattern;
115             (*pp)->value = xstrdup(wrbuf_cstr(ct->w));
116             pp = &(*pp)->next;
117         }
118         else if (t != YAZ_TOK_EOF)
119         {
120             yaz_tok_parse_destroy(tp);
121             cql_transform_close(ct);
122             return 0;
123         }
124         yaz_tok_parse_destroy(tp);
125     }
126     *pp = 0;
127     return ct;
128 }
129
130 void cql_transform_close(cql_transform_t ct)
131 {
132     struct cql_prop_entry *pe;
133     if (!ct)
134         return;
135     pe = ct->entry;
136     while (pe)
137     {
138         struct cql_prop_entry *pe_next = pe->next;
139         xfree(pe->pattern);
140         xfree(pe->value);
141         xfree(pe);
142         pe = pe_next;
143     }
144     xfree(ct->addinfo);
145     yaz_tok_cfg_destroy(ct->tok_cfg);
146     wrbuf_destroy(ct->w);
147     xfree(ct);
148 }
149
150 cql_transform_t cql_transform_open_fname(const char *fname)
151 {
152     cql_transform_t ct;
153     FILE *f = fopen(fname, "r");
154     if (!f)
155         return 0;
156     ct = cql_transform_open_FILE(f);
157     fclose(f);
158     return ct;
159 }
160
161 static const char *cql_lookup_property(cql_transform_t ct,
162                                        const char *pat1, const char *pat2,
163                                        const char *pat3)
164 {
165     char pattern[120];
166     struct cql_prop_entry *e;
167
168     if (pat1 && pat2 && pat3)
169         sprintf(pattern, "%.39s.%.39s.%.39s", pat1, pat2, pat3);
170     else if (pat1 && pat2)
171         sprintf(pattern, "%.39s.%.39s", pat1, pat2);
172     else if (pat1 && pat3)
173         sprintf(pattern, "%.39s.%.39s", pat1, pat3);
174     else if (pat1)
175         sprintf(pattern, "%.39s", pat1);
176     else
177         return 0;
178     
179     for (e = ct->entry; e; e = e->next)
180     {
181         if (!cql_strcmp(e->pattern, pattern))
182             return e->value;
183     }
184     return 0;
185 }
186
187 int cql_pr_attr_uri(cql_transform_t ct, const char *category,
188                    const char *uri, const char *val, const char *default_val,
189                    void (*pr)(const char *buf, void *client_data),
190                    void *client_data,
191                    int errcode)
192 {
193     const char *res = 0;
194     const char *eval = val ? val : default_val;
195     const char *prefix = 0;
196     
197     if (uri)
198     {
199         struct cql_prop_entry *e;
200         
201         for (e = ct->entry; e; e = e->next)
202             if (!memcmp(e->pattern, "set.", 4) && e->value &&
203                 !strcmp(e->value, uri))
204             {
205                 prefix = e->pattern+4;
206                 break;
207             }
208         /* must have a prefix now - if not it's an error */
209     }
210
211     if (!uri || prefix)
212     {
213         if (!res)
214             res = cql_lookup_property(ct, category, prefix, eval);
215         /* we have some aliases for some relations unfortunately.. */
216         if (!res && !prefix && !strcmp(category, "relation"))
217         {
218             if (!strcmp(val, "=="))
219                 res = cql_lookup_property(ct, category, prefix, "exact");
220             if (!strcmp(val, "="))
221                 res = cql_lookup_property(ct, category, prefix, "eq");
222             if (!strcmp(val, "<="))
223                 res = cql_lookup_property(ct, category, prefix, "le");
224             if (!strcmp(val, ">="))
225                 res = cql_lookup_property(ct, category, prefix, "ge");
226         }
227         if (!res)
228             res = cql_lookup_property(ct, category, prefix, "*");
229     }
230     if (res)
231     {
232         char buf[64];
233
234         const char *cp0 = res, *cp1;
235         while ((cp1 = strchr(cp0, '=')))
236         {
237             int i;
238             while (*cp1 && *cp1 != ' ')
239                 cp1++;
240             if (cp1 - cp0 >= sizeof(buf))
241                 break;
242             memcpy(buf, cp0, cp1 - cp0);
243             buf[cp1-cp0] = 0;
244             (*pr)("@attr ", client_data);
245
246             for (i = 0; buf[i]; i++)
247             {
248                 if (buf[i] == '*')
249                     (*pr)(eval, client_data);
250                 else
251                 {
252                     char tmp[2];
253                     tmp[0] = buf[i];
254                     tmp[1] = '\0';
255                     (*pr)(tmp, client_data);
256                 }
257             }
258             (*pr)(" ", client_data);
259             cp0 = cp1;
260             while (*cp0 == ' ')
261                 cp0++;
262         }
263         return 1;
264     }
265     /* error ... */
266     if (errcode && !ct->error)
267     {
268         ct->error = errcode;
269         if (val)
270             ct->addinfo = xstrdup(val);
271         else
272             ct->addinfo = 0;
273     }
274     return 0;
275 }
276
277 int cql_pr_attr(cql_transform_t ct, const char *category,
278                 const char *val, const char *default_val,
279                 void (*pr)(const char *buf, void *client_data),
280                 void *client_data,
281                 int errcode)
282 {
283     return cql_pr_attr_uri(ct, category, 0 /* uri */,
284                            val, default_val, pr, client_data, errcode);
285 }
286
287
288 static void cql_pr_int(int val,
289                        void (*pr)(const char *buf, void *client_data),
290                        void *client_data)
291 {
292     char buf[21];              /* enough characters to 2^64 */
293     sprintf(buf, "%d", val);
294     (*pr)(buf, client_data);
295     (*pr)(" ", client_data);
296 }
297
298
299 static int cql_pr_prox(cql_transform_t ct, struct cql_node *mods,
300                        void (*pr)(const char *buf, void *client_data),
301                        void *client_data)
302 {
303     int exclusion = 0;
304     int distance;               /* to be filled in later depending on unit */
305     int distance_defined = 0;
306     int ordered = 0;
307     int proxrel = 2;            /* less than or equal */
308     int unit = 2;               /* word */
309
310     while (mods)
311     {
312         const char *name = mods->u.st.index;
313         const char *term = mods->u.st.term;
314         const char *relation = mods->u.st.relation;
315
316         if (!strcmp(name, "distance")) {
317             distance = strtol(term, (char**) 0, 0);
318             distance_defined = 1;
319             if (!strcmp(relation, "="))
320                 proxrel = 3;
321             else if (!strcmp(relation, ">"))
322                 proxrel = 5;
323             else if (!strcmp(relation, "<"))
324                 proxrel = 1;
325             else if (!strcmp(relation, ">=")) 
326                 proxrel = 4;
327             else if (!strcmp(relation, "<="))
328                 proxrel = 2;
329             else if (!strcmp(relation, "<>"))
330                 proxrel = 6;
331             else 
332             {
333                 ct->error = YAZ_SRW_UNSUPP_PROX_RELATION;
334                 ct->addinfo = xstrdup(relation);
335                 return 0;
336             }
337         } 
338         else if (!strcmp(name, "ordered"))
339             ordered = 1;
340         else if (!strcmp(name, "unordered"))
341             ordered = 0;
342         else if (!strcmp(name, "unit"))
343         {
344             if (!strcmp(term, "word"))
345                 unit = 2;
346             else if (!strcmp(term, "sentence"))
347                 unit = 3;
348             else if (!strcmp(term, "paragraph"))
349                 unit = 4;
350             else if (!strcmp(term, "element"))
351                 unit = 8;
352             else 
353             {
354                 ct->error = YAZ_SRW_UNSUPP_PROX_UNIT;
355                 ct->addinfo = xstrdup(term);
356                 return 0;
357             }
358         } 
359         else 
360         {
361             ct->error = YAZ_SRW_UNSUPP_BOOLEAN_MODIFIER;
362             ct->addinfo = xstrdup(name);
363             return 0;
364         }
365         mods = mods->u.st.modifiers;
366     }
367
368     if (!distance_defined)
369         distance = (unit == 2) ? 1 : 0;
370
371     cql_pr_int(exclusion, pr, client_data);
372     cql_pr_int(distance, pr, client_data);
373     cql_pr_int(ordered, pr, client_data);
374     cql_pr_int(proxrel, pr, client_data);
375     (*pr)("k ", client_data);
376     cql_pr_int(unit, pr, client_data);
377
378     return 1;
379 }
380
381 /* Returns location of first wildcard character in the `length'
382  * characters starting at `term', or a null pointer of there are
383  * none -- like memchr().
384  */
385 static const char *wcchar(int start, const char *term, int length)
386 {
387     while (length > 0)
388     {
389         if (start || term[-1] != '\\')
390             if (strchr("*?", *term))
391                 return term;
392         term++;
393         length--;
394         start = 0;
395     }
396     return 0;
397 }
398
399
400 /* ### checks for CQL relation-name rather than Type-1 attribute */
401 static int has_modifier(struct cql_node *cn, const char *name) {
402     struct cql_node *mod;
403     for (mod = cn->u.st.modifiers; mod != 0; mod = mod->u.st.modifiers) {
404         if (!strcmp(mod->u.st.index, name))
405             return 1;
406     }
407
408     return 0;
409 }
410
411
412 void emit_term(cql_transform_t ct,
413                struct cql_node *cn,
414                const char *term, int length,
415                void (*pr)(const char *buf, void *client_data),
416                void *client_data)
417 {
418     int i;
419     const char *ns = cn->u.st.index_uri;
420     int process_term = !has_modifier(cn, "regexp");
421     char *z3958_mem = 0;
422
423     assert(cn->which == CQL_NODE_ST);
424
425     if (process_term && length > 0)
426     {
427         if (length > 1 && term[0] == '^' && term[length-1] == '^')
428         {
429             cql_pr_attr(ct, "position", "firstAndLast", 0,
430                         pr, client_data, YAZ_SRW_ANCHORING_CHAR_IN_UNSUPP_POSITION);
431             term++;
432             length -= 2;
433         }
434         else if (term[0] == '^')
435         {
436             cql_pr_attr(ct, "position", "first", 0,
437                         pr, client_data, YAZ_SRW_ANCHORING_CHAR_IN_UNSUPP_POSITION);
438             term++;
439             length--;
440         }
441         else if (term[length-1] == '^')
442         {
443             cql_pr_attr(ct, "position", "last", 0,
444                         pr, client_data, YAZ_SRW_ANCHORING_CHAR_IN_UNSUPP_POSITION);
445             length--;
446         }
447         else
448         {
449             cql_pr_attr(ct, "position", "any", 0,
450                         pr, client_data, YAZ_SRW_ANCHORING_CHAR_IN_UNSUPP_POSITION);
451         }
452     }
453
454     if (process_term && length > 0)
455     {
456         const char *first_wc = wcchar(1, term, length);
457         const char *second_wc = first_wc ?
458             wcchar(0, first_wc+1, length-(first_wc-term)-1) : 0;
459
460         /* Check for well-known globbing patterns that represent
461          * simple truncation attributes as expected by, for example,
462          * Bath-compliant server.  If we find such a pattern but
463          * there's no mapping for it, that's fine: we just use a
464          * general pattern-matching attribute.
465          */
466         if (first_wc == term && second_wc == term + length-1 
467             && *first_wc == '*' && *second_wc == '*' 
468             && cql_pr_attr(ct, "truncation", "both", 0, pr, client_data, 0)) 
469         {
470             term++;
471             length -= 2;
472         }
473         else if (first_wc == term && second_wc == 0 && *first_wc == '*'
474                  && cql_pr_attr(ct, "truncation", "left", 0,
475                                 pr, client_data, 0))
476         {
477             term++;
478             length--;
479         }
480         else if (first_wc == term + length-1 && second_wc == 0
481                  && *first_wc == '*'
482                  && cql_pr_attr(ct, "truncation", "right", 0, 
483                                 pr, client_data, 0))
484         {
485             length--;
486         }
487         else if (first_wc)
488         {
489             /* We have one or more wildcard characters, but not in a
490              * way that can be dealt with using only the standard
491              * left-, right- and both-truncation attributes.  We need
492              * to translate the pattern into a Z39.58-type pattern,
493              * which has been supported in BIB-1 since 1996.  If
494              * there's no configuration element for "truncation.z3958"
495              * we indicate this as error 28 "Masking character not
496              * supported".
497              */
498             int i;
499             cql_pr_attr(ct, "truncation", "z3958", 0,
500                         pr, client_data, YAZ_SRW_MASKING_CHAR_UNSUPP);
501             z3958_mem = (char *) xmalloc(length+1);
502             for (i = 0; i < length; i++)
503             {
504                 if (i > 0 && term[i-1] == '\\')
505                     z3958_mem[i] = term[i];
506                 else if (term[i] == '*')
507                     z3958_mem[i] = '?';
508                 else if (term[i] == '?')
509                     z3958_mem[i] = '#';
510                 else
511                     z3958_mem[i] = term[i];
512             }
513             z3958_mem[length] = '\0';
514             term = z3958_mem;
515         }
516         else {
517             /* No masking characters.  Use "truncation.none" if given. */
518             cql_pr_attr(ct, "truncation", "none", 0,
519                         pr, client_data, 0);
520         }
521     }
522     if (ns) {
523         cql_pr_attr_uri(ct, "index", ns,
524                         cn->u.st.index, "serverChoice",
525                         pr, client_data, YAZ_SRW_UNSUPP_INDEX);
526     }
527     if (cn->u.st.modifiers)
528     {
529         struct cql_node *mod = cn->u.st.modifiers;
530         for (; mod; mod = mod->u.st.modifiers)
531         {
532             cql_pr_attr(ct, "relationModifier", mod->u.st.index, 0,
533                         pr, client_data, YAZ_SRW_UNSUPP_RELATION_MODIFIER);
534         }
535     }
536
537     (*pr)("\"", client_data);
538     for (i = 0; i<length; i++)
539     {
540         /* pr(int) each character */
541         /* we do not need to deal with \-sequences because the
542            CQL and PQF terms have same \-format, bug #1988 */
543         char buf[2];
544
545         buf[0] = term[i];
546         buf[1] = '\0';
547         (*pr)(buf, client_data);
548     }
549     (*pr)("\" ", client_data);
550     xfree(z3958_mem);
551 }
552
553 void emit_terms(cql_transform_t ct,
554                 struct cql_node *cn,
555                 void (*pr)(const char *buf, void *client_data),
556                 void *client_data,
557                 const char *op)
558 {
559     struct cql_node *ne = cn->u.st.extra_terms;
560     if (ne)
561     {
562         (*pr)("@", client_data);
563         (*pr)(op, client_data);
564         (*pr)(" ", client_data);
565     }
566     emit_term(ct, cn, cn->u.st.term, strlen(cn->u.st.term),
567               pr, client_data);
568     for (; ne; ne = ne->u.st.extra_terms)
569     {
570         if (ne->u.st.extra_terms)
571         {
572             (*pr)("@", client_data);
573             (*pr)(op, client_data);
574             (*pr)(" ", client_data);
575         }            
576         emit_term(ct, cn, ne->u.st.term, strlen(ne->u.st.term),
577                   pr, client_data);
578     }
579 }
580
581 void emit_wordlist(cql_transform_t ct,
582                    struct cql_node *cn,
583                    void (*pr)(const char *buf, void *client_data),
584                    void *client_data,
585                    const char *op)
586 {
587     const char *cp0 = cn->u.st.term;
588     const char *cp1;
589     const char *last_term = 0;
590     int last_length = 0;
591     while(cp0)
592     {
593         while (*cp0 == ' ')
594             cp0++;
595         cp1 = strchr(cp0, ' ');
596         if (last_term)
597         {
598             (*pr)("@", client_data);
599             (*pr)(op, client_data);
600             (*pr)(" ", client_data);
601             emit_term(ct, cn, last_term, last_length, pr, client_data);
602         }
603         last_term = cp0;
604         if (cp1)
605             last_length = cp1 - cp0;
606         else
607             last_length = strlen(cp0);
608         cp0 = cp1;
609     }
610     if (last_term)
611         emit_term(ct, cn, last_term, last_length, pr, client_data);
612 }
613
614 void cql_transform_r(cql_transform_t ct,
615                      struct cql_node *cn,
616                      void (*pr)(const char *buf, void *client_data),
617                      void *client_data)
618 {
619     const char *ns;
620     struct cql_node *mods;
621
622     if (!cn)
623         return;
624     switch (cn->which)
625     {
626     case CQL_NODE_ST:
627         ns = cn->u.st.index_uri;
628         if (ns)
629         {
630             if (!strcmp(ns, cql_uri())
631                 && cn->u.st.index && !cql_strcmp(cn->u.st.index, "resultSet"))
632             {
633                 (*pr)("@set \"", client_data);
634                 (*pr)(cn->u.st.term, client_data);
635                 (*pr)("\" ", client_data);
636                 return ;
637             }
638         }
639         else
640         {
641             if (!ct->error)
642             {
643                 ct->error = YAZ_SRW_UNSUPP_CONTEXT_SET;
644                 ct->addinfo = 0;
645             }
646         }
647         cql_pr_attr(ct, "always", 0, 0, pr, client_data, 0);
648         cql_pr_attr(ct, "relation", cn->u.st.relation, 0, pr, client_data,
649                     YAZ_SRW_UNSUPP_RELATION);
650         cql_pr_attr(ct, "structure", cn->u.st.relation, 0,
651                     pr, client_data, YAZ_SRW_UNSUPP_COMBI_OF_RELATION_AND_TERM);
652         if (cn->u.st.relation && !cql_strcmp(cn->u.st.relation, "all"))
653             emit_wordlist(ct, cn, pr, client_data, "and");
654         else if (cn->u.st.relation && !cql_strcmp(cn->u.st.relation, "any"))
655             emit_wordlist(ct, cn, pr, client_data, "or");
656         else
657             emit_terms(ct, cn, pr, client_data, "and");
658         break;
659     case CQL_NODE_BOOL:
660         (*pr)("@", client_data);
661         (*pr)(cn->u.boolean.value, client_data);
662         (*pr)(" ", client_data);
663         mods = cn->u.boolean.modifiers;
664         if (!strcmp(cn->u.boolean.value, "prox")) 
665         {
666             if (!cql_pr_prox(ct, mods, pr, client_data))
667                 return;
668         } 
669         else if (mods)
670         {
671             /* Boolean modifiers other than on proximity not supported */
672             ct->error = YAZ_SRW_UNSUPP_BOOLEAN_MODIFIER;
673             ct->addinfo = xstrdup(mods->u.st.index);
674             return;
675         }
676
677         cql_transform_r(ct, cn->u.boolean.left, pr, client_data);
678         cql_transform_r(ct, cn->u.boolean.right, pr, client_data);
679         break;
680
681     default:
682         fprintf(stderr, "Fatal: impossible CQL node-type %d\n", cn->which);
683         abort();
684     }
685 }
686
687 int cql_transform(cql_transform_t ct, struct cql_node *cn,
688                   void (*pr)(const char *buf, void *client_data),
689                   void *client_data)
690 {
691     struct cql_prop_entry *e;
692     NMEM nmem = nmem_create();
693
694     ct->error = 0;
695     xfree(ct->addinfo);
696     ct->addinfo = 0;
697
698     for (e = ct->entry; e ; e = e->next)
699     {
700         if (!cql_strncmp(e->pattern, "set.", 4))
701             cql_apply_prefix(nmem, cn, e->pattern+4, e->value);
702         else if (!cql_strcmp(e->pattern, "set"))
703             cql_apply_prefix(nmem, cn, 0, e->value);
704     }
705     cql_transform_r(ct, cn, pr, client_data);
706     nmem_destroy(nmem);
707     return ct->error;
708 }
709
710
711 int cql_transform_FILE(cql_transform_t ct, struct cql_node *cn, FILE *f)
712 {
713     return cql_transform(ct, cn, cql_fputs, f);
714 }
715
716 int cql_transform_buf(cql_transform_t ct, struct cql_node *cn, char *out, int max)
717 {
718     struct cql_buf_write_info info;
719     int r;
720
721     info.off = 0;
722     info.max = max;
723     info.buf = out;
724     r = cql_transform(ct, cn, cql_buf_write_handler, &info);
725     if (info.off < 0) {
726         /* Attempt to write past end of buffer.  For some reason, this
727            SRW diagnostic is deprecated, but it's so perfect for our
728            purposes that it would be stupid not to use it. */
729         char numbuf[30];
730         ct->error = YAZ_SRW_TOO_MANY_CHARS_IN_QUERY;
731         sprintf(numbuf, "%ld", (long) info.max);
732         ct->addinfo = xstrdup(numbuf);
733         return -1;
734     }
735     if (info.off >= 0)
736         info.buf[info.off] = '\0';
737     return r;
738 }
739
740 int cql_transform_error(cql_transform_t ct, const char **addinfo)
741 {
742     *addinfo = ct->addinfo;
743     return ct->error;
744 }
745
746 void cql_transform_set_error(cql_transform_t ct, int error, const char *addinfo)
747 {
748     xfree(ct->addinfo);
749     ct->addinfo = addinfo ? xstrdup(addinfo) : 0;
750     ct->error = error;
751 }
752
753 /*
754  * Local variables:
755  * c-basic-offset: 4
756  * indent-tabs-mode: nil
757  * End:
758  * vim: shiftwidth=4 tabstop=8 expandtab
759  */
760