#include "redis.h"
void listTypeTryConversion(robj *subject, robj *value) { if (subject->encoding != REDIS_ENCODING_ZIPLIST) return; if (value->encoding == REDIS_ENCODING_RAW && sdslen(value->ptr) > server.list_max_ziplist_value) listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST); }
void listTypePush(robj *subject, robj *value, int where) { listTypeTryConversion(subject,value); if (subject->encoding == REDIS_ENCODING_ZIPLIST && ziplistLen(subject->ptr) >= server.list_max_ziplist_entries) listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST); if (subject->encoding == REDIS_ENCODING_ZIPLIST) { int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL; value = getDecodedObject(value); subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos); decrRefCount(value); } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) { if (where == REDIS_HEAD) { listAddNodeHead(subject->ptr,value); } else { listAddNodeTail(subject->ptr,value); } incrRefCount(value); } else { redisPanic("Unknown list encoding"); } }
robj *listTypePop(robj *subject, int where) { robj *value = NULL; if (subject->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; int pos = (where == REDIS_HEAD) ? 0 : -1; p = ziplistIndex(subject->ptr,pos); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } subject->ptr = ziplistDelete(subject->ptr,&p); } } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) { list *list = subject->ptr; listNode *ln; if (where == REDIS_HEAD) { ln = listFirst(list); } else { ln = listLast(list); } if (ln != NULL) { value = listNodeValue(ln); incrRefCount(value); listDelNode(list,ln); } } else { redisPanic("Unknown list encoding"); } return value; }
unsigned long listTypeLength(robj *subject) { if (subject->encoding == REDIS_ENCODING_ZIPLIST) { return ziplistLen(subject->ptr); } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) { return listLength((list*)subject->ptr); } else { redisPanic("Unknown list encoding"); } }
listTypeIterator *listTypeInitIterator(robj *subject, int index, unsigned char direction) { listTypeIterator *li = zmalloc(sizeof(listTypeIterator)); li->subject = subject; li->encoding = subject->encoding; li->direction = direction; if (li->encoding == REDIS_ENCODING_ZIPLIST) { li->zi = ziplistIndex(subject->ptr,index); } else if (li->encoding == REDIS_ENCODING_LINKEDLIST) { li->ln = listIndex(subject->ptr,index); } else { redisPanic("Unknown list encoding"); } return li; }
void listTypeReleaseIterator(listTypeIterator *li) { zfree(li); }
int listTypeNext(listTypeIterator *li, listTypeEntry *entry) { redisAssert(li->subject->encoding == li->encoding); entry->li = li; if (li->encoding == REDIS_ENCODING_ZIPLIST) { entry->zi = li->zi; if (entry->zi != NULL) { if (li->direction == REDIS_TAIL) li->zi = ziplistNext(li->subject->ptr,li->zi); else li->zi = ziplistPrev(li->subject->ptr,li->zi); return 1; } } else if (li->encoding == REDIS_ENCODING_LINKEDLIST) { entry->ln = li->ln; if (entry->ln != NULL) { if (li->direction == REDIS_TAIL) li->ln = li->ln->next; else li->ln = li->ln->prev; return 1; } } else { redisPanic("Unknown list encoding"); } return 0; }
robj *listTypeGet(listTypeEntry *entry) { listTypeIterator *li = entry->li; robj *value = NULL; if (li->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *vstr; unsigned int vlen; long long vlong; redisAssert(entry->zi != NULL); if (ziplistGet(entry->zi,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } } } else if (li->encoding == REDIS_ENCODING_LINKEDLIST) { redisAssert(entry->ln != NULL); value = listNodeValue(entry->ln); incrRefCount(value); } else { redisPanic("Unknown list encoding"); } return value; }
void listTypeInsert(listTypeEntry *entry, robj *value, int where) { robj *subject = entry->li->subject; if (entry->li->encoding == REDIS_ENCODING_ZIPLIST) { value = getDecodedObject(value); if (where == REDIS_TAIL) { unsigned char *next = ziplistNext(subject->ptr,entry->zi);
if (next == NULL) { subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),REDIS_TAIL); } else { subject->ptr = ziplistInsert(subject->ptr,next,value->ptr,sdslen(value->ptr)); } } else { subject->ptr = ziplistInsert(subject->ptr,entry->zi,value->ptr,sdslen(value->ptr)); } decrRefCount(value); } else if (entry->li->encoding == REDIS_ENCODING_LINKEDLIST) { if (where == REDIS_TAIL) { listInsertNode(subject->ptr,entry->ln,value,AL_START_TAIL); } else { listInsertNode(subject->ptr,entry->ln,value,AL_START_HEAD); } incrRefCount(value); } else { redisPanic("Unknown list encoding"); } }
int listTypeEqual(listTypeEntry *entry, robj *o) { listTypeIterator *li = entry->li; if (li->encoding == REDIS_ENCODING_ZIPLIST) { redisAssert(o->encoding == REDIS_ENCODING_RAW); return ziplistCompare(entry->zi,o->ptr,sdslen(o->ptr)); } else if (li->encoding == REDIS_ENCODING_LINKEDLIST) { return equalStringObjects(o,listNodeValue(entry->ln)); } else { redisPanic("Unknown list encoding"); } }
void listTypeDelete(listTypeEntry *entry) { listTypeIterator *li = entry->li; if (li->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p = entry->zi; li->subject->ptr = ziplistDelete(li->subject->ptr,&p); if (li->direction == REDIS_TAIL) li->zi = p; else li->zi = ziplistPrev(li->subject->ptr,p); } else if (entry->li->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *next; if (li->direction == REDIS_TAIL) next = entry->ln->next; else next = entry->ln->prev; listDelNode(li->subject->ptr,entry->ln); li->ln = next; } else { redisPanic("Unknown list encoding"); } }
void listTypeConvert(robj *subject, int enc) { listTypeIterator *li; listTypeEntry entry; redisAssert(subject->type == REDIS_LIST);
if (enc == REDIS_ENCODING_LINKEDLIST) { list *l = listCreate(); listSetFreeMethod(l,decrRefCount);
li = listTypeInitIterator(subject,0,REDIS_TAIL); while (listTypeNext(li,&entry)) listAddNodeTail(l,listTypeGet(&entry)); listTypeReleaseIterator(li);
subject->encoding = REDIS_ENCODING_LINKEDLIST; zfree(subject->ptr); subject->ptr = l; } else { redisPanic("Unsupported list conversion"); } }
void pushGenericCommand(redisClient *c, int where) { robj *lobj = lookupKeyWrite(c->db,c->argv[1]); c->argv[2] = tryObjectEncoding(c->argv[2]); if (lobj == NULL) { if (handleClientsWaitingListPush(c,c->argv[1],c->argv[2])) { addReply(c,shared.cone); return; } lobj = createZiplistObject(); dbAdd(c->db,c->argv[1],lobj); } else { if (lobj->type != REDIS_LIST) { addReply(c,shared.wrongtypeerr); return; } if (handleClientsWaitingListPush(c,c->argv[1],c->argv[2])) { touchWatchedKey(c->db,c->argv[1]); addReply(c,shared.cone); return; } } listTypePush(lobj,c->argv[2],where);、 addReplyLongLong(c,listTypeLength(lobj)); touchWatchedKey(c->db,c->argv[1]); server.dirty++; }
void lpushCommand(redisClient *c) { pushGenericCommand(c,REDIS_HEAD); }
void rpushCommand(redisClient *c) { pushGenericCommand(c,REDIS_TAIL); }
void pushxGenericCommand(redisClient *c, robj *refval, robj *val, int where) { robj *subject; listTypeIterator *iter; listTypeEntry entry; int inserted = 0; if ((subject = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,subject,REDIS_LIST)) return;
if (refval != NULL) {
redisAssert(refval->encoding == REDIS_ENCODING_RAW);
listTypeTryConversion(subject,val);
iter = listTypeInitIterator(subject,0,REDIS_TAIL); while (listTypeNext(iter,&entry)) { if (listTypeEqual(&entry,refval)) { listTypeInsert(&entry,val,where); inserted = 1; break; } } listTypeReleaseIterator(iter);
if (inserted) { if (subject->encoding == REDIS_ENCODING_ZIPLIST && ziplistLen(subject->ptr) > server.list_max_ziplist_entries) listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST); touchWatchedKey(c->db,c->argv[1]); server.dirty++; } else { addReply(c,shared.cnegone); return; } } else { listTypePush(subject,val,where); touchWatchedKey(c->db,c->argv[1]); server.dirty++; } addReplyLongLong(c,listTypeLength(subject)); }
void lpushxCommand(redisClient *c) { c->argv[2] = tryObjectEncoding(c->argv[2]); pushxGenericCommand(c,NULL,c->argv[2],REDIS_HEAD); }
void rpushxCommand(redisClient *c) { c->argv[2] = tryObjectEncoding(c->argv[2]); pushxGenericCommand(c,NULL,c->argv[2],REDIS_TAIL); }
void linsertCommand(redisClient *c) { c->argv[4] = tryObjectEncoding(c->argv[4]); if (strcasecmp(c->argv[2]->ptr,"after") == 0) { pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_TAIL); } else if (strcasecmp(c->argv[2]->ptr,"before") == 0) { pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_HEAD); } else { addReply(c,shared.syntaxerr); } }
void llenCommand(redisClient *c) { robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.czero); if (o == NULL || checkType(c,o,REDIS_LIST)) return; addReplyLongLong(c,listTypeLength(o)); }
void lindexCommand(redisClient *c) { robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return; int index = atoi(c->argv[2]->ptr); robj *value = NULL;
if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; p = ziplistIndex(o->ptr,index); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } addReplyBulk(c,value); decrRefCount(value); } else { addReply(c,shared.nullbulk); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln != NULL) { value = listNodeValue(ln); addReplyBulk(c,value); } else { addReply(c,shared.nullbulk); } } else { redisPanic("Unknown list encoding"); } }
void lsetCommand(redisClient *c) { robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr); if (o == NULL || checkType(c,o,REDIS_LIST)) return; int index = atoi(c->argv[2]->ptr); robj *value = (c->argv[3] = tryObjectEncoding(c->argv[3]));
listTypeTryConversion(o,value); if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p, *zl = o->ptr; p = ziplistIndex(zl,index); if (p == NULL) { addReply(c,shared.outofrangeerr); } else { o->ptr = ziplistDelete(o->ptr,&p); value = getDecodedObject(value); o->ptr = ziplistInsert(o->ptr,p,value->ptr,sdslen(value->ptr)); decrRefCount(value); addReply(c,shared.ok); touchWatchedKey(c->db,c->argv[1]); server.dirty++; } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln == NULL) { addReply(c,shared.outofrangeerr); } else { decrRefCount((robj*)listNodeValue(ln)); listNodeValue(ln) = value; incrRefCount(value); addReply(c,shared.ok); touchWatchedKey(c->db,c->argv[1]); server.dirty++; } } else { redisPanic("Unknown list encoding"); } }
void popGenericCommand(redisClient *c, int where) { robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return; robj *value = listTypePop(o,where); if (value == NULL) { addReply(c,shared.nullbulk); } else { addReplyBulk(c,value); decrRefCount(value); if (listTypeLength(o) == 0) dbDelete(c->db,c->argv[1]); touchWatchedKey(c->db,c->argv[1]); server.dirty++; } }
void lpopCommand(redisClient *c) { popGenericCommand(c,REDIS_HEAD); }
void rpopCommand(redisClient *c) { popGenericCommand(c,REDIS_TAIL); }
void lrangeCommand(redisClient *c) { robj *o; int start = atoi(c->argv[2]->ptr); int end = atoi(c->argv[3]->ptr); int llen; int rangelen;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL || checkType(c,o,REDIS_LIST)) return; llen = listTypeLength(o);
if (start < 0) start = llen+start; if (end < 0) end = llen+end; if (start < 0) start = 0;
if (start > end || start >= llen) { addReply(c,shared.emptymultibulk); return; } if (end >= llen) end = llen-1; rangelen = (end-start)+1;
addReplyMultiBulkLen(c,rangelen); if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p = ziplistIndex(o->ptr,start); unsigned char *vstr; unsigned int vlen; long long vlong;
while(rangelen--) { ziplistGet(p,&vstr,&vlen,&vlong); if (vstr) { addReplyBulkCBuffer(c,vstr,vlen); } else { addReplyBulkLongLong(c,vlong); } p = ziplistNext(o->ptr,p); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln;
if (start > llen/2) start -= llen; ln = listIndex(o->ptr,start);
while(rangelen--) { addReplyBulk(c,ln->value); ln = ln->next; } } else { redisPanic("List encoding is not LINKEDLIST nor ZIPLIST!"); } }
void ltrimCommand(redisClient *c) { robj *o; int start = atoi(c->argv[2]->ptr); int end = atoi(c->argv[3]->ptr); int llen; int j, ltrim, rtrim; list *list; listNode *ln;
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.ok)) == NULL || checkType(c,o,REDIS_LIST)) return; llen = listTypeLength(o);
if (start < 0) start = llen+start; if (end < 0) end = llen+end; if (start < 0) start = 0;
if (start > end || start >= llen) { ltrim = llen; rtrim = 0; } else { if (end >= llen) end = llen-1; ltrim = start; rtrim = llen-end-1; }
if (o->encoding == REDIS_ENCODING_ZIPLIST) { o->ptr = ziplistDeleteRange(o->ptr,0,ltrim); o->ptr = ziplistDeleteRange(o->ptr,-rtrim,rtrim); } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { list = o->ptr; for (j = 0; j < ltrim; j++) { ln = listFirst(list); listDelNode(list,ln); } for (j = 0; j < rtrim; j++) { ln = listLast(list); listDelNode(list,ln); } } else { redisPanic("Unknown list encoding"); } if (listTypeLength(o) == 0) dbDelete(c->db,c->argv[1]); touchWatchedKey(c->db,c->argv[1]); server.dirty++; addReply(c,shared.ok); }
void lremCommand(redisClient *c) { robj *subject, *obj; obj = c->argv[3] = tryObjectEncoding(c->argv[3]); int toremove = atoi(c->argv[2]->ptr); int removed = 0; listTypeEntry entry;
subject = lookupKeyWriteOrReply(c,c->argv[1],shared.czero); if (subject == NULL || checkType(c,subject,REDIS_LIST)) return;
if (subject->encoding == REDIS_ENCODING_ZIPLIST) obj = getDecodedObject(obj);
listTypeIterator *li; if (toremove < 0) { toremove = -toremove; li = listTypeInitIterator(subject,-1,REDIS_HEAD); } else { li = listTypeInitIterator(subject,0,REDIS_TAIL); } while (listTypeNext(li,&entry)) { if (listTypeEqual(&entry,obj)) { listTypeDelete(&entry); server.dirty++; removed++; if (toremove && removed == toremove) break; } } listTypeReleaseIterator(li);
if (subject->encoding == REDIS_ENCODING_ZIPLIST) decrRefCount(obj); if (listTypeLength(subject) == 0) dbDelete(c->db,c->argv[1]); addReplyLongLong(c,removed); if (removed) touchWatchedKey(c->db,c->argv[1]); }
void rpoplpushHandlePush(redisClient *origclient, redisClient *c, robj *dstkey, robj *dstobj, robj *value) { robj *aux; if (!handleClientsWaitingListPush(origclient,dstkey,value)) { if (!dstobj) { dstobj = createZiplistObject(); dbAdd(c->db,dstkey,dstobj); } else { touchWatchedKey(c->db,dstkey); } listTypePush(dstobj,value,REDIS_HEAD);
if (c != origclient) { aux = createStringObject("LPUSH",5); rewriteClientCommandVector(origclient,3,aux,dstkey,value); decrRefCount(aux); } else {
aux = createStringObject("RPOPLPUSH",9); rewriteClientCommandVector(origclient,3,aux,c->argv[1],c->argv[2]); decrRefCount(aux); } server.dirty++; }
addReplyBulk(c,value); }
void rpoplpushCommand(redisClient *c) { robj *sobj, *value; if ((sobj = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk)) == NULL || checkType(c,sobj,REDIS_LIST)) return; if (listTypeLength(sobj) == 0) { addReply(c,shared.nullbulk); } else { robj *dobj = lookupKeyWrite(c->db,c->argv[2]); robj *touchedkey = c->argv[1];
if (dobj && checkType(c,dobj,REDIS_LIST)) return; value = listTypePop(sobj,REDIS_TAIL);
incrRefCount(touchedkey); rpoplpushHandlePush(c,c,c->argv[2],dobj,value);
decrRefCount(value);
if (listTypeLength(sobj) == 0) dbDelete(c->db,touchedkey); touchWatchedKey(c->db,touchedkey); decrRefCount(touchedkey); server.dirty++; } }
void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeout, robj *target) { dictEntry *de; list *l; int j; c->bpop.keys = zmalloc(sizeof(robj*)*numkeys); c->bpop.count = numkeys; c->bpop.timeout = timeout; c->bpop.target = target;
if (target != NULL) { incrRefCount(target); }
for (j = 0; j < numkeys; j++) { c->bpop.keys[j] = keys[j]; incrRefCount(keys[j]);
de = dictFind(c->db->blocking_keys,keys[j]); if (de == NULL) { int retval;
l = listCreate(); retval = dictAdd(c->db->blocking_keys,keys[j],l); incrRefCount(keys[j]); redisAssert(retval == DICT_OK); } else { l = dictGetEntryVal(de); } listAddNodeTail(l,c); } c->flags |= REDIS_BLOCKED; server.bpop_blocked_clients++; }
void unblockClientWaitingData(redisClient *c) { dictEntry *de; list *l; int j;
redisAssert(c->bpop.keys != NULL); for (j = 0; j < c->bpop.count; j++) { de = dictFind(c->db->blocking_keys,c->bpop.keys[j]); redisAssert(de != NULL); l = dictGetEntryVal(de); listDelNode(l,listSearchKey(l,c)); if (listLength(l) == 0) dictDelete(c->db->blocking_keys,c->bpop.keys[j]); decrRefCount(c->bpop.keys[j]); }
zfree(c->bpop.keys); c->bpop.keys = NULL; if (c->bpop.target) decrRefCount(c->bpop.target); c->bpop.target = NULL; c->flags &= ~REDIS_BLOCKED; c->flags |= REDIS_UNBLOCKED; server.bpop_blocked_clients--; listAddNodeTail(server.unblocked_clients,c); }
int handleClientsWaitingListPush(redisClient *c, robj *key, robj *ele) { struct dictEntry *de; redisClient *receiver; int numclients; list *clients; listNode *ln; robj *dstkey, *dstobj; de = dictFind(c->db->blocking_keys,key); if (de == NULL) return 0; clients = dictGetEntryVal(de); numclients = listLength(clients);
while (numclients--) { ln = listFirst(clients); redisAssert(ln != NULL); receiver = ln->value; dstkey = receiver->bpop.target;
if (dstkey) incrRefCount(dstkey); unblockClientWaitingData(receiver); if (dstkey == NULL) { addReplyMultiBulkLen(receiver,2); addReplyBulk(receiver,key); addReplyBulk(receiver,ele); return 1; } else { dstobj = lookupKeyWrite(receiver->db,dstkey); if (!(dstobj && checkType(receiver,dstobj,REDIS_LIST))) { rpoplpushHandlePush(c,receiver,dstkey,dstobj,ele); decrRefCount(dstkey); return 1; } decrRefCount(dstkey); } }
return 0; }
int getTimeoutFromObjectOrReply(redisClient *c, robj *object, time_t *timeout) { long tval;
if (getLongFromObjectOrReply(c,object,&tval, "timeout is not an integer or out of range") != REDIS_OK) return REDIS_ERR;
if (tval < 0) { addReplyError(c,"timeout is negative"); return REDIS_ERR; }
if (tval > 0) tval += time(NULL); *timeout = tval;
return REDIS_OK; }
void blockingPopGenericCommand(redisClient *c, int where) { robj *o; time_t timeout; int j;
if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout) != REDIS_OK) return;
for (j = 1; j < c->argc-1; j++) { o = lookupKeyWrite(c->db,c->argv[j]); if (o != NULL) { if (o->type != REDIS_LIST) { addReply(c,shared.wrongtypeerr); return; } else { if (listTypeLength(o) != 0) {
struct redisCommand *orig_cmd; robj *argv[2], **orig_argv; int orig_argc;
orig_argv = c->argv; orig_argc = c->argc; orig_cmd = c->cmd; argv[1] = c->argv[j]; c->argv = argv; c->argc = 2;
addReplyMultiBulkLen(c,2); addReplyBulk(c,argv[1]);
popGenericCommand(c,where);
c->argv = orig_argv; c->argc = orig_argc; c->cmd = orig_cmd;
return; } } } }
if (c->flags & REDIS_MULTI) { addReply(c,shared.nullmultibulk); return; }
blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL); }
void blpopCommand(redisClient *c) { blockingPopGenericCommand(c,REDIS_HEAD); }
void brpopCommand(redisClient *c) { blockingPopGenericCommand(c,REDIS_TAIL); }
void brpoplpushCommand(redisClient *c) { time_t timeout;
if (getTimeoutFromObjectOrReply(c,c->argv[3],&timeout) != REDIS_OK) return;
robj *key = lookupKeyWrite(c->db, c->argv[1]); if (key == NULL) { if (c->flags & REDIS_MULTI) {
addReply(c, shared.nullbulk); } else { blockForKeys(c, c->argv + 1, 1, timeout, c->argv[2]); } } else { if (key->type != REDIS_LIST) { addReply(c, shared.wrongtypeerr); } else {
redisAssert(listTypeLength(key) > 0); rpoplpushCommand(c); } } }
|