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