]> git.evergreen-ils.org Git - OpenSRF.git/blob - src/libopensrf/osrf_json_object.c
Patch from Scott McKellar:
[OpenSRF.git] / src / libopensrf / osrf_json_object.c
1 /*
2 Copyright (C) 2006  Georgia Public Library Service 
3 Bill Erickson <billserickson@gmail.com>
4
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 */
15
16 #include <stdlib.h>
17 #include <ctype.h>
18 #include <errno.h>
19 #include <limits.h>
20 #include <opensrf/log.h>
21 #include <opensrf/osrf_json.h>
22 #include <opensrf/osrf_json_utils.h>
23
24 /* cleans up an object if it is morphing another object, also
25  * verifies that the appropriate storage container exists where appropriate */
26 #define JSON_INIT_CLEAR(_obj_, newtype)         \
27         if( _obj_->type == JSON_HASH && newtype != JSON_HASH ) {                        \
28                 osrfHashFree(_obj_->value.h);                   \
29                 _obj_->value.h = NULL;                                  \
30 } else if( _obj_->type == JSON_ARRAY && newtype != JSON_ARRAY ) {       \
31                 osrfListFree(_obj_->value.l);                   \
32                 _obj_->value.l = NULL;                                  \
33 } else if( _obj_->type == JSON_STRING || _obj_->type == JSON_NUMBER ) { \
34                 free(_obj_->value.s);                                           \
35                 _obj_->value.s = NULL;                                  \
36 } \
37         _obj_->type = newtype;\
38         if( newtype == JSON_HASH && _obj_->value.h == NULL ) {  \
39                 _obj_->value.h = osrfNewHash();         \
40                 osrfHashSetCallback( _obj_->value.h, _jsonFreeHashItem ); \
41 } else if( newtype == JSON_ARRAY && _obj_->value.l == NULL ) {  \
42                 _obj_->value.l = osrfNewList();         \
43                 _obj_->value.l->freeItem = _jsonFreeListItem;\
44 }
45
46 static int unusedObjCapture = 0;
47 static int unusedObjRelease = 0;
48 static int mallocObjCreate = 0;
49 static int currentListLen = 0;
50
51 union unusedObjUnion{
52
53         union unusedObjUnion* next;
54         jsonObject obj;
55 };
56 typedef union unusedObjUnion unusedObj;
57
58 // We maintain a free list of jsonObjects that are available
59 // for use, in order to reduce the churning through
60 // malloc() and free().
61
62 static unusedObj* freeObjList = NULL;
63
64 static void add_json_to_buffer( const jsonObject* obj, growing_buffer * buf );
65
66 /**
67  * Return all unused jsonObjects to the heap
68  * @return Nothing
69  */
70 void jsonObjectFreeUnused( void ) {
71
72         unusedObj* temp;
73         while( freeObjList ) {
74                 temp = freeObjList->next;
75                 free( freeObjList );
76                 freeObjList = temp;
77         }
78 }
79
80 jsonObject* jsonNewObject(const char* data) {
81
82         jsonObject* o;
83
84         if( freeObjList ) {
85                 o = (jsonObject*) freeObjList;
86                 freeObjList = freeObjList->next;
87         unusedObjRelease++;
88         currentListLen--;
89         } else {
90                 OSRF_MALLOC( o, sizeof(jsonObject) );
91         mallocObjCreate++;
92     }
93
94         o->size = 0;
95         o->classname = NULL;
96         o->parent = NULL;
97
98         if(data) {
99                 o->type = JSON_STRING;
100                 o->value.s = strdup(data);
101         } else {
102                 o->type = JSON_NULL;
103                 o->value.s = NULL;
104         }
105
106         return o;
107 }
108
109 jsonObject* jsonNewObjectFmt(const char* data, ...) {
110
111         jsonObject* o;
112
113         if( freeObjList ) {
114                 o = (jsonObject*) freeObjList;
115                 freeObjList = freeObjList->next;
116         unusedObjRelease++;
117         currentListLen--;
118         } else {
119                 OSRF_MALLOC( o, sizeof(jsonObject) );
120         mallocObjCreate++;
121     }
122
123         o->size = 0;
124         o->classname = NULL;
125         o->parent = NULL;
126
127         if(data) {
128                 VA_LIST_TO_STRING(data);
129                 o->type = JSON_STRING;
130                 o->value.s = strdup(VA_BUF);
131         }
132         else {
133                 o->type = JSON_NULL;
134                 o->value.s = NULL;
135         }
136         
137         return o;
138 }
139
140 jsonObject* jsonNewNumberObject( double num ) {
141         jsonObject* o = jsonNewObject(NULL);
142         o->type = JSON_NUMBER;
143         o->value.s = doubleToString( num );
144         return o;
145 }
146
147 /**
148  * Creates a new number object from a numeric string
149  */
150 jsonObject* jsonNewNumberStringObject( const char* numstr ) {
151         if( !numstr )
152                 numstr = "0";
153         else if( !jsonIsNumeric( numstr ) )
154                 return NULL;
155
156         jsonObject* o = jsonNewObject(NULL);
157         o->type = JSON_NUMBER;
158         o->value.s = strdup( numstr );
159         return o;
160 }
161
162 jsonObject* jsonNewBoolObject(int val) {
163     jsonObject* o = jsonNewObject(NULL);
164     o->type = JSON_BOOL;
165     jsonSetBool(o, val);
166     return o;
167 }
168
169 jsonObject* jsonNewObjectType(int type) {
170         jsonObject* o = jsonNewObject(NULL);
171         o->type = type;
172         return o;
173 }
174
175 void jsonObjectFree( jsonObject* o ) {
176
177         if(!o || o->parent) return;
178         free(o->classname);
179
180         switch(o->type) {
181                 case JSON_HASH          : osrfHashFree(o->value.h); break;
182                 case JSON_ARRAY : osrfListFree(o->value.l); break;
183                 case JSON_STRING        : free(o->value.s); break;
184                 case JSON_NUMBER        : free(o->value.s); break;
185         }
186
187         // Stick the old jsonObject onto a free list
188         // for potential reuse
189         
190         unusedObj* unused = (unusedObj*) o;
191         unused->next = freeObjList;
192         freeObjList = unused;
193
194     unusedObjCapture++;
195     currentListLen++;
196     if (unusedObjCapture > 1 && !(unusedObjCapture % 1000))
197         osrfLogDebug( OSRF_LOG_MARK, "Objects malloc()'d: %d, Reusable objects captured: %d, Objects reused: %d, Current List Length: %d", mallocObjCreate, unusedObjCapture, unusedObjRelease, currentListLen );
198 }
199
200 static void _jsonFreeHashItem(char* key, void* item){
201         if(!item) return;
202         jsonObject* o = (jsonObject*) item;
203         o->parent = NULL; /* detach the item */
204         jsonObjectFree(o);
205 }
206 static void _jsonFreeListItem(void* item){
207         if(!item) return;
208         jsonObject* o = (jsonObject*) item;
209         o->parent = NULL; /* detach the item */
210         jsonObjectFree(o);
211 }
212
213 void jsonSetBool(jsonObject* bl, int val) {
214     if(!bl) return;
215     JSON_INIT_CLEAR(bl, JSON_BOOL);
216     bl->value.b = val;
217 }
218
219 unsigned long jsonObjectPush(jsonObject* o, jsonObject* newo) {
220     if(!o) return -1;
221     if(!newo) newo = jsonNewObject(NULL);
222         JSON_INIT_CLEAR(o, JSON_ARRAY);
223         newo->parent = o;
224         osrfListPush( o->value.l, newo );
225         o->size = o->value.l->size;
226         return o->size;
227 }
228
229 unsigned long jsonObjectSetIndex(jsonObject* dest, unsigned long index, jsonObject* newObj) {
230     if(!dest) return -1;
231     if(!newObj) newObj = jsonNewObject(NULL);
232         JSON_INIT_CLEAR(dest, JSON_ARRAY);
233         newObj->parent = dest;
234         osrfListSet( dest->value.l, newObj, index );
235         dest->size = dest->value.l->size;
236         return dest->value.l->size;
237 }
238
239 unsigned long jsonObjectSetKey( jsonObject* o, const char* key, jsonObject* newo) {
240     if(!o) return -1;
241     if(!newo) newo = jsonNewObject(NULL);
242         JSON_INIT_CLEAR(o, JSON_HASH);
243         newo->parent = o;
244         osrfHashSet( o->value.h, newo, key );
245         o->size = osrfHashGetCount(o->value.h);
246         return o->size;
247 }
248
249 jsonObject* jsonObjectGetKey( jsonObject* obj, const char* key ) {
250         if(!(obj && obj->type == JSON_HASH && obj->value.h && key)) return NULL;
251         return osrfHashGet( obj->value.h, key);
252 }
253
254 const jsonObject* jsonObjectGetKeyConst( const jsonObject* obj, const char* key ) {
255         if(!(obj && obj->type == JSON_HASH && obj->value.h && key)) return NULL;
256         return osrfHashGet( obj->value.h, key);
257 }
258
259 char* jsonObjectToJSON( const jsonObject* obj ) {
260         jsonObject* obj2 = jsonObjectEncodeClass( obj );
261         char* json = jsonObjectToJSONRaw(obj2);
262         jsonObjectFree(obj2);
263         return json;
264 }
265
266 char* jsonObjectToJSONRaw( const jsonObject* obj ) {
267         if(!obj) return NULL;
268         growing_buffer* buf = buffer_init(32);
269         add_json_to_buffer( obj, buf );
270         return buffer_release( buf );
271 }
272
273 static void add_json_to_buffer( const jsonObject* obj, growing_buffer * buf ) {
274
275         switch(obj->type) {
276
277                 case JSON_BOOL :
278                         if(obj->value.b) OSRF_BUFFER_ADD(buf, "true"); 
279                         else OSRF_BUFFER_ADD(buf, "false"); 
280                         break;
281
282                 case JSON_NUMBER: {
283                         if(obj->value.s) OSRF_BUFFER_ADD( buf, obj->value.s );
284                         else OSRF_BUFFER_ADD_CHAR( buf, '0' );
285                         break;
286                 }
287
288                 case JSON_NULL:
289                         OSRF_BUFFER_ADD(buf, "null");
290                         break;
291
292                 case JSON_STRING:
293                         OSRF_BUFFER_ADD_CHAR(buf, '"');
294                         char* data = obj->value.s;
295                         int len = strlen(data);
296                         
297                         char* output = uescape(data, len, 1);
298                         OSRF_BUFFER_ADD(buf, output);
299                         free(output);
300                         OSRF_BUFFER_ADD_CHAR(buf, '"');
301                         break;
302                         
303                 case JSON_ARRAY: {
304                         OSRF_BUFFER_ADD_CHAR(buf, '[');
305                         if( obj->value.l ) {
306                                 int i;
307                                 for( i = 0; i != obj->value.l->size; i++ ) {
308                                         if(i > 0) OSRF_BUFFER_ADD(buf, ",");
309                                         add_json_to_buffer( OSRF_LIST_GET_INDEX(obj->value.l, i), buf );
310                                 }
311                         }
312                         OSRF_BUFFER_ADD_CHAR(buf, ']');
313                         break;
314                 }
315
316                 case JSON_HASH: {
317
318                         OSRF_BUFFER_ADD_CHAR(buf, '{');
319                         osrfHashIterator* itr = osrfNewHashIterator(obj->value.h);
320                         jsonObject* item;
321                         int i = 0;
322
323                         while( (item = osrfHashIteratorNext(itr)) ) {
324                                 if(i++ > 0) OSRF_BUFFER_ADD(buf, ",");
325                                 buffer_fadd(buf, "\"%s\":", osrfHashIteratorKey(itr));
326                                 add_json_to_buffer( item, buf );
327                         }
328
329                         osrfHashIteratorFree(itr);
330                         OSRF_BUFFER_ADD_CHAR(buf, '}');
331                         break;
332                 }
333         }
334 }
335
336
337 jsonIterator* jsonNewIterator(const jsonObject* obj) {
338         if(!obj) return NULL;
339         jsonIterator* itr;
340         OSRF_MALLOC(itr, sizeof(jsonIterator));
341
342         itr->obj                = (jsonObject*) obj;
343         itr->index      = 0;
344         itr->key                = NULL;
345
346         if( obj->type == JSON_HASH )
347                 itr->hashItr = osrfNewHashIterator(obj->value.h);
348         
349         return itr;
350 }
351
352 void jsonIteratorFree(jsonIterator* itr) {
353         if(!itr) return;
354         free(itr->key);
355         osrfHashIteratorFree(itr->hashItr);
356         free(itr);
357 }
358
359 jsonObject* jsonIteratorNext(jsonIterator* itr) {
360         if(!(itr && itr->obj)) return NULL;
361         if( itr->obj->type == JSON_HASH ) {
362                 if(!itr->hashItr) return NULL;
363                 jsonObject* item = osrfHashIteratorNext(itr->hashItr);
364         if(!item) return NULL;
365                 free(itr->key);
366                 itr->key = strdup( osrfHashIteratorKey(itr->hashItr) );
367                 return item;
368         } else {
369                 return jsonObjectGetIndex( itr->obj, itr->index++ );
370         }
371 }
372
373 int jsonIteratorHasNext(const jsonIterator* itr) {
374         if(!(itr && itr->obj)) return 0;
375         if( itr->obj->type == JSON_HASH )
376                 return osrfHashIteratorHasNext( itr->hashItr );
377         return (itr->index < itr->obj->size) ? 1 : 0;
378 }
379
380 jsonObject* jsonObjectGetIndex( const jsonObject* obj, unsigned long index ) {
381         if(!obj) return NULL;
382         return (obj->type == JSON_ARRAY) ? 
383         (OSRF_LIST_GET_INDEX(obj->value.l, index)) : NULL;
384 }
385
386
387
388 unsigned long jsonObjectRemoveIndex(jsonObject* dest, unsigned long index) {
389         if( dest && dest->type == JSON_ARRAY ) {
390                 osrfListRemove(dest->value.l, index);
391                 return dest->value.l->size;
392         }
393         return -1;
394 }
395
396
397 unsigned long jsonObjectRemoveKey( jsonObject* dest, const char* key) {
398         if( dest && key && dest->type == JSON_HASH ) {
399                 osrfHashRemove(dest->value.h, key);
400                 return 1;
401         }
402         return -1;
403 }
404
405 /**
406  Allocate a buffer and format a specified numeric value into it.
407  Caller is responsible for freeing the buffer.
408 **/
409 char* doubleToString( double num ) {
410         
411         char buf[ 64 ];
412         size_t len = snprintf(buf, sizeof( buf ), "%.30g", num) + 1;
413         if( len < sizeof( buf ) )
414                 return strdup( buf );
415         else
416         {
417                 // Need a bigger buffer (should never be necessary)
418                 
419                 char* bigger_buff = safe_malloc( len + 1 );
420                 (void) snprintf(bigger_buff, len + 1, "%.30g", num);
421                 return bigger_buff;
422         }
423 }
424
425 char* jsonObjectGetString(const jsonObject* obj) {
426         if(obj)
427         {
428                 if( obj->type == JSON_STRING )
429                         return obj->value.s;
430                 else if( obj->type == JSON_NUMBER )
431                         return obj->value.s ? obj->value.s : "0";
432                 else
433                         return NULL;
434         }
435         else
436                 return NULL;
437 }
438
439 double jsonObjectGetNumber( const jsonObject* obj ) {
440         return (obj && obj->type == JSON_NUMBER && obj->value.s)
441                         ? strtod( obj->value.s, NULL ) : 0;
442 }
443
444 void jsonObjectSetString(jsonObject* dest, const char* string) {
445         if(!(dest && string)) return;
446         JSON_INIT_CLEAR(dest, JSON_STRING);
447         dest->value.s = strdup(string);
448 }
449
450 /**
451  Turn a jsonObject into a JSON_NUMBER (if it isn't already one) and store
452  a specified numeric string in it.  If the string is not numeric,
453  store the equivalent of zero, and return an error status.
454 **/
455 int jsonObjectSetNumberString(jsonObject* dest, const char* string) {
456         if(!(dest && string)) return -1;
457         JSON_INIT_CLEAR(dest, JSON_NUMBER);
458
459         if( jsonIsNumeric( string ) ) {
460                 dest->value.s = strdup(string);
461                 return 0;
462         }
463         else {
464                 dest->value.s = NULL;  // equivalent to zero
465                 return -1;
466         }
467 }
468
469 void jsonObjectSetNumber(jsonObject* dest, double num) {
470         if(!dest) return;
471         JSON_INIT_CLEAR(dest, JSON_NUMBER);
472         dest->value.s = doubleToString( num );
473 }
474
475 void jsonObjectSetClass(jsonObject* dest, const char* classname ) {
476         if(!(dest && classname)) return;
477         free(dest->classname);
478         dest->classname = strdup(classname);
479 }
480 const char* jsonObjectGetClass(const jsonObject* dest) {
481     if(!dest) return NULL;
482     return dest->classname;
483 }
484
485 jsonObject* jsonObjectClone( const jsonObject* o ) {
486     if(!o) return jsonNewObject(NULL);
487
488     int i;
489     jsonObject* arr; 
490     jsonObject* hash; 
491     jsonIterator* itr;
492     jsonObject* tmp;
493     jsonObject* result = NULL;
494
495     switch(o->type) {
496         case JSON_NULL:
497             result = jsonNewObject(NULL);
498             break;
499         case JSON_STRING:
500             result = jsonNewObject(jsonObjectGetString(o));
501             break;
502         case JSON_NUMBER:
503                         result = jsonNewObject( o->value.s );
504                         result->type = JSON_NUMBER;
505             break;
506         case JSON_BOOL:
507             result = jsonNewBoolObject(jsonBoolIsTrue((jsonObject*) o));
508             break;
509         case JSON_ARRAY:
510             arr = jsonNewObject(NULL);
511             arr->type = JSON_ARRAY;
512             for(i=0; i < o->size; i++) 
513                 jsonObjectPush(arr, jsonObjectClone(jsonObjectGetIndex(o, i)));
514             result = arr;
515             break;
516         case JSON_HASH:
517             hash = jsonNewObject(NULL);
518             hash->type = JSON_HASH;
519             itr = jsonNewIterator(o);
520             while( (tmp = jsonIteratorNext(itr)) )
521                 jsonObjectSetKey(hash, itr->key, jsonObjectClone(tmp));
522             jsonIteratorFree(itr);
523             result = hash;
524             break;
525     }
526
527     jsonObjectSetClass(result, jsonObjectGetClass(o));
528     return result;
529 }
530
531 int jsonBoolIsTrue( const jsonObject* boolObj ) {
532     if( boolObj && boolObj->type == JSON_BOOL && boolObj->value.b )
533         return 1;
534     return 0;
535 }
536
537
538 char* jsonObjectToSimpleString( const jsonObject* o ) {
539         if(!o) return NULL;
540
541         char* value = NULL;
542
543         switch( o->type ) {
544
545                 case JSON_NUMBER:
546                         value = strdup( o->value.s ? o->value.s : "0" );
547                         break;
548
549                 case JSON_STRING:
550                         value = strdup(o->value.s);
551         }
552
553         return value;
554 }
555
556 /**
557  Return 1 if the string is numeric, otherwise return 0.
558  This validation follows the rules defined by the grammar at:
559  http://www.json.org/
560  **/
561 int jsonIsNumeric( const char* s ) {
562
563         if( !s || !*s ) return 0;
564
565         const char* p = s;
566
567         // skip leading minus sign, if present (leading plus sign not allowed)
568
569         if( '-' == *p )
570                 ++p;
571
572         // There must be at least one digit to the left of the decimal
573
574         if( isdigit( (unsigned char) *p ) ) {
575                 if( '0' == *p++ ) {
576
577                         // If the first digit is zero, it must be the
578                         // only digit to the lerft of the decimal
579
580                         if( isdigit( (unsigned char) *p ) )
581                                 return 0;
582                 }
583                 else {
584
585                         // Skip oer the following digits
586
587                         while( isdigit( (unsigned char) *p ) ) ++p;
588                 }
589         }
590         else
591                 return 0;
592
593         if( !*p )
594                 return 1;             // integer
595
596         if( '.' == *p ) {
597
598                 ++p;
599
600                 // If there is a decimal point, there must be
601                 // at least one digit to the right of it
602
603                 if( isdigit( (unsigned char) *p ) )
604                         ++p;
605                 else
606                         return 0;
607
608                 // skip over contiguous digits
609
610                 while( isdigit( (unsigned char) *p ) ) ++p;
611         }
612
613         if( ! *p )
614                 return 1;  // decimal fraction, no exponent
615         else if( *p != 'e' && *p != 'E' )
616                 return 0;  // extra junk, no exponent
617         else
618                 ++p;
619
620         // If we get this far, we have the beginnings of an exponent.
621         // Skip over optional sign of exponent.
622
623         if( '-' == *p || '+' == *p )
624                 ++p;
625
626         // There must be at least one digit in the exponent
627         
628         if( isdigit( (unsigned char) *p ) )
629                 ++p;
630         else
631                 return 0;
632
633         // skip over contiguous digits
634
635         while( isdigit( (unsigned char) *p ) ) ++p;
636
637         if( *p )
638                 return 0;  // extra junk
639         else
640                 return 1;  // number with exponent
641 }
642
643 /**
644  Allocate and reformat a numeric string into one that is valid
645  by JSON rules.  If the string is not numeric, return NULL.
646  Caller is responsible for freeing the buffer.
647  **/
648 char* jsonScrubNumber( const char* s ) {
649         if( !s || !*s ) return NULL;
650
651         growing_buffer* buf = buffer_init( 64 );
652
653         // Skip leading white space, if present
654
655         while( isspace( (unsigned char) *s ) ) ++s;
656
657         // Skip leading plus sign, if present, but keep a minus
658
659         if( '-' == *s )
660         {
661                 buffer_add_char( buf, '-' );
662                 ++s;
663         }
664         else if( '+' == *s )
665                 ++s;
666
667         if( '\0' == *s ) {
668                 // No digits found
669
670                 buffer_free( buf );
671                 return NULL;
672         }
673         // Skip any leading zeros
674
675         while( '0' == *s ) ++s;
676
677         // Capture digits to the left of the decimal,
678         // and note whether there are any.
679
680         int left_digit = 0;  // boolean
681
682         if( isdigit( (unsigned char) *s ) ) {
683                 buffer_add_char( buf, *s++ );
684                 left_digit = 1;
685         }
686         
687         while( isdigit( (unsigned char) *s  ) )
688                 buffer_add_char( buf, *s++ );
689
690         // Now we expect to see a decimal point,
691         // an exponent, or end-of-string.
692
693         switch( *s )
694         {
695                 case '\0' :
696                         break;
697                 case '.' :
698                 {
699                         // Add a single leading zero, if we need to
700
701                         if( ! left_digit )
702                                 buffer_add_char( buf, '0' );
703                         buffer_add_char( buf, '.' );
704                         ++s;
705
706                         if( ! left_digit && ! isdigit( (unsigned char) *s ) )
707                         {
708                                 // No digits on either side of decimal
709
710                                 buffer_free( buf );
711                                 return NULL;
712                         }
713
714                         // Collect digits to right of decimal
715
716                         while( isdigit( (unsigned char) *s ) )
717                                 buffer_add_char( buf, *s++ );
718
719                         break;
720                 }
721                 case 'e' :
722                 case 'E' :
723
724                         // Exponent; we'll deal with it later, but
725                         // meanwhile make sure we have something
726                         // to its left
727
728                         if( ! left_digit )
729                                 buffer_add_char( buf, '1' );
730                         break;
731                 default :
732
733                         // Unexpected character; bail out
734
735                         buffer_free( buf );
736                         return NULL;
737         }
738
739         if( '\0' == *s )    // Are we done yet?
740                 return buffer_release( buf );
741
742         if( 'e' != *s && 'E' != *s ) {
743
744                 // Unexpected character: bail out
745
746                 buffer_free( buf );
747                 return NULL;
748         }
749
750         // We have an exponent.  Load the e or E,
751         // and the sign if there is one.
752
753         buffer_add_char( buf, *s++ );
754
755         if( '+' == *s || '-' == *s )
756                 buffer_add_char( buf, *s++ );
757
758         // Collect digits of the exponent
759
760         while( isdigit( (unsigned char) *s ) )
761                 buffer_add_char( buf, *s++ );
762
763         // There better not be anything left
764
765         if( *s ) {
766                 buffer_free( buf );
767                 return NULL;
768         }
769
770         return buffer_release( buf );
771 }