TLA Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * wparser.c
4 : * Standard interface to word parser
5 : *
6 : * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
7 : *
8 : *
9 : * IDENTIFICATION
10 : * src/backend/tsearch/wparser.c
11 : *
12 : *-------------------------------------------------------------------------
13 : */
14 : #include "postgres.h"
15 :
16 : #include "catalog/namespace.h"
17 : #include "catalog/pg_type.h"
18 : #include "commands/defrem.h"
19 : #include "common/jsonapi.h"
20 : #include "funcapi.h"
21 : #include "tsearch/ts_cache.h"
22 : #include "tsearch/ts_utils.h"
23 : #include "utils/builtins.h"
24 : #include "utils/jsonfuncs.h"
25 : #include "utils/varlena.h"
26 :
27 : /******sql-level interface******/
28 :
29 : typedef struct
30 : {
31 : int cur;
32 : LexDescr *list;
33 : } TSTokenTypeStorage;
34 :
35 : /* state for ts_headline_json_* */
36 : typedef struct HeadlineJsonState
37 : {
38 : HeadlineParsedText *prs;
39 : TSConfigCacheEntry *cfg;
40 : TSParserCacheEntry *prsobj;
41 : TSQuery query;
42 : List *prsoptions;
43 : bool transformed;
44 : } HeadlineJsonState;
45 :
46 : static text *headline_json_value(void *_state, char *elem_value, int elem_len);
47 :
48 : static void
49 GNC 165 : tt_setup_firstcall(FuncCallContext *funcctx, FunctionCallInfo fcinfo,
50 : Oid prsid)
51 : {
52 : TupleDesc tupdesc;
53 : MemoryContext oldcontext;
54 : TSTokenTypeStorage *st;
55 GIC 165 : TSParserCacheEntry *prs = lookup_ts_parser_cache(prsid);
56 ECB :
57 GIC 165 : if (!OidIsValid(prs->lextypeOid))
58 LBC 0 : elog(ERROR, "method lextype isn't defined for text search parser %u",
59 EUB : prsid);
60 :
61 GIC 165 : oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
62 ECB :
63 GIC 165 : st = (TSTokenTypeStorage *) palloc(sizeof(TSTokenTypeStorage));
64 CBC 165 : st->cur = 0;
65 ECB : /* lextype takes one dummy argument */
66 GIC 165 : st->list = (LexDescr *) DatumGetPointer(OidFunctionCall1(prs->lextypeOid,
67 ECB : (Datum) 0));
68 GIC 165 : funcctx->user_fctx = (void *) st;
69 ECB :
70 GNC 165 : if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
71 UNC 0 : elog(ERROR, "return type must be a row type");
72 GNC 165 : funcctx->tuple_desc = tupdesc;
73 GIC 165 : funcctx->attinmeta = TupleDescGetAttInMetadata(tupdesc);
74 :
75 165 : MemoryContextSwitchTo(oldcontext);
76 CBC 165 : }
77 :
78 : static Datum
79 GIC 3960 : tt_process_call(FuncCallContext *funcctx)
80 ECB : {
81 : TSTokenTypeStorage *st;
82 :
83 GIC 3960 : st = (TSTokenTypeStorage *) funcctx->user_fctx;
84 3960 : if (st->list && st->list[st->cur].lexid)
85 : {
86 : Datum result;
87 : char *values[3];
88 ECB : char txtid[16];
89 : HeapTuple tuple;
90 :
91 CBC 3795 : sprintf(txtid, "%d", st->list[st->cur].lexid);
92 GIC 3795 : values[0] = txtid;
93 CBC 3795 : values[1] = st->list[st->cur].alias;
94 3795 : values[2] = st->list[st->cur].descr;
95 :
96 3795 : tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);
97 3795 : result = HeapTupleGetDatum(tuple);
98 ECB :
99 CBC 3795 : pfree(values[1]);
100 GIC 3795 : pfree(values[2]);
101 CBC 3795 : st->cur++;
102 GIC 3795 : return result;
103 : }
104 165 : return (Datum) 0;
105 ECB : }
106 :
107 : Datum
108 GIC 3888 : ts_token_type_byid(PG_FUNCTION_ARGS)
109 : {
110 ECB : FuncCallContext *funcctx;
111 : Datum result;
112 :
113 CBC 3888 : if (SRF_IS_FIRSTCALL())
114 : {
115 GIC 162 : funcctx = SRF_FIRSTCALL_INIT();
116 GNC 162 : tt_setup_firstcall(funcctx, fcinfo, PG_GETARG_OID(0));
117 : }
118 ECB :
119 CBC 3888 : funcctx = SRF_PERCALL_SETUP();
120 ECB :
121 GIC 3888 : if ((result = tt_process_call(funcctx)) != (Datum) 0)
122 3726 : SRF_RETURN_NEXT(funcctx, result);
123 162 : SRF_RETURN_DONE(funcctx);
124 ECB : }
125 :
126 : Datum
127 GIC 72 : ts_token_type_byname(PG_FUNCTION_ARGS)
128 : {
129 ECB : FuncCallContext *funcctx;
130 : Datum result;
131 :
132 GIC 72 : if (SRF_IS_FIRSTCALL())
133 : {
134 CBC 3 : text *prsname = PG_GETARG_TEXT_PP(0);
135 ECB : Oid prsId;
136 :
137 GIC 3 : funcctx = SRF_FIRSTCALL_INIT();
138 3 : prsId = get_ts_parser_oid(textToQualifiedNameList(prsname), false);
139 GNC 3 : tt_setup_firstcall(funcctx, fcinfo, prsId);
140 : }
141 ECB :
142 CBC 72 : funcctx = SRF_PERCALL_SETUP();
143 ECB :
144 GIC 72 : if ((result = tt_process_call(funcctx)) != (Datum) 0)
145 69 : SRF_RETURN_NEXT(funcctx, result);
146 3 : SRF_RETURN_DONE(funcctx);
147 : }
148 :
149 : typedef struct
150 : {
151 : int type;
152 : char *lexeme;
153 : } LexemeEntry;
154 :
155 : typedef struct
156 : {
157 : int cur;
158 : int len;
159 : LexemeEntry *list;
160 : } PrsStorage;
161 ECB :
162 :
163 : static void
164 GNC 22 : prs_setup_firstcall(FuncCallContext *funcctx, FunctionCallInfo fcinfo,
165 : Oid prsid, text *txt)
166 : {
167 : TupleDesc tupdesc;
168 ECB : MemoryContext oldcontext;
169 : PrsStorage *st;
170 CBC 22 : TSParserCacheEntry *prs = lookup_ts_parser_cache(prsid);
171 22 : char *lex = NULL;
172 GIC 22 : int llen = 0,
173 22 : type = 0;
174 ECB : void *prsdata;
175 :
176 CBC 22 : oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
177 ECB :
178 CBC 22 : st = (PrsStorage *) palloc(sizeof(PrsStorage));
179 22 : st->cur = 0;
180 GIC 22 : st->len = 16;
181 CBC 22 : st->list = (LexemeEntry *) palloc(sizeof(LexemeEntry) * st->len);
182 :
183 GIC 22 : prsdata = (void *) DatumGetPointer(FunctionCall2(&prs->prsstart,
184 : PointerGetDatum(VARDATA_ANY(txt)),
185 ECB : Int32GetDatum(VARSIZE_ANY_EXHDR(txt))));
186 :
187 GIC 541 : while ((type = DatumGetInt32(FunctionCall3(&prs->prstoken,
188 ECB : PointerGetDatum(prsdata),
189 : PointerGetDatum(&lex),
190 CBC 541 : PointerGetDatum(&llen)))) != 0)
191 : {
192 519 : if (st->cur >= st->len)
193 ECB : {
194 GIC 12 : st->len = 2 * st->len;
195 CBC 12 : st->list = (LexemeEntry *) repalloc(st->list, sizeof(LexemeEntry) * st->len);
196 ECB : }
197 CBC 519 : st->list[st->cur].lexeme = palloc(llen + 1);
198 519 : memcpy(st->list[st->cur].lexeme, lex, llen);
199 519 : st->list[st->cur].lexeme[llen] = '\0';
200 GIC 519 : st->list[st->cur].type = type;
201 519 : st->cur++;
202 ECB : }
203 :
204 CBC 22 : FunctionCall1(&prs->prsend, PointerGetDatum(prsdata));
205 ECB :
206 GIC 22 : st->len = st->cur;
207 CBC 22 : st->cur = 0;
208 ECB :
209 GBC 22 : funcctx->user_fctx = (void *) st;
210 GNC 22 : if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
211 UNC 0 : elog(ERROR, "return type must be a row type");
212 GNC 22 : funcctx->tuple_desc = tupdesc;
213 CBC 22 : funcctx->attinmeta = TupleDescGetAttInMetadata(tupdesc);
214 GIC 22 : MemoryContextSwitchTo(oldcontext);
215 22 : }
216 :
217 ECB : static Datum
218 CBC 541 : prs_process_call(FuncCallContext *funcctx)
219 : {
220 : PrsStorage *st;
221 :
222 GIC 541 : st = (PrsStorage *) funcctx->user_fctx;
223 541 : if (st->cur < st->len)
224 : {
225 ECB : Datum result;
226 : char *values[2];
227 : char tid[16];
228 : HeapTuple tuple;
229 :
230 GIC 519 : values[0] = tid;
231 CBC 519 : sprintf(tid, "%d", st->list[st->cur].type);
232 519 : values[1] = st->list[st->cur].lexeme;
233 519 : tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);
234 GIC 519 : result = HeapTupleGetDatum(tuple);
235 ECB :
236 GIC 519 : pfree(values[1]);
237 519 : st->cur++;
238 519 : return result;
239 ECB : }
240 GIC 22 : return (Datum) 0;
241 : }
242 :
243 : Datum
244 CBC 105 : ts_parse_byid(PG_FUNCTION_ARGS)
245 : {
246 ECB : FuncCallContext *funcctx;
247 : Datum result;
248 :
249 CBC 105 : if (SRF_IS_FIRSTCALL())
250 ECB : {
251 GIC 18 : text *txt = PG_GETARG_TEXT_PP(1);
252 :
253 CBC 18 : funcctx = SRF_FIRSTCALL_INIT();
254 GNC 18 : prs_setup_firstcall(funcctx, fcinfo, PG_GETARG_OID(0), txt);
255 CBC 18 : PG_FREE_IF_COPY(txt, 1);
256 ECB : }
257 :
258 GIC 105 : funcctx = SRF_PERCALL_SETUP();
259 :
260 105 : if ((result = prs_process_call(funcctx)) != (Datum) 0)
261 CBC 87 : SRF_RETURN_NEXT(funcctx, result);
262 GIC 18 : SRF_RETURN_DONE(funcctx);
263 : }
264 :
265 : Datum
266 CBC 436 : ts_parse_byname(PG_FUNCTION_ARGS)
267 : {
268 ECB : FuncCallContext *funcctx;
269 : Datum result;
270 :
271 GIC 436 : if (SRF_IS_FIRSTCALL())
272 ECB : {
273 CBC 4 : text *prsname = PG_GETARG_TEXT_PP(0);
274 4 : text *txt = PG_GETARG_TEXT_PP(1);
275 : Oid prsId;
276 :
277 4 : funcctx = SRF_FIRSTCALL_INIT();
278 GIC 4 : prsId = get_ts_parser_oid(textToQualifiedNameList(prsname), false);
279 GNC 4 : prs_setup_firstcall(funcctx, fcinfo, prsId, txt);
280 ECB : }
281 :
282 GIC 436 : funcctx = SRF_PERCALL_SETUP();
283 :
284 436 : if ((result = prs_process_call(funcctx)) != (Datum) 0)
285 CBC 432 : SRF_RETURN_NEXT(funcctx, result);
286 GIC 4 : SRF_RETURN_DONE(funcctx);
287 ECB : }
288 :
289 : Datum
290 CBC 73 : ts_headline_byid_opt(PG_FUNCTION_ARGS)
291 : {
292 GIC 73 : Oid tsconfig = PG_GETARG_OID(0);
293 73 : text *in = PG_GETARG_TEXT_PP(1);
294 73 : TSQuery query = PG_GETARG_TSQUERY(2);
295 73 : text *opt = (PG_NARGS() > 3 && PG_GETARG_POINTER(3)) ? PG_GETARG_TEXT_PP(3) : NULL;
296 : HeadlineParsedText prs;
297 ECB : List *prsoptions;
298 : text *out;
299 : TSConfigCacheEntry *cfg;
300 : TSParserCacheEntry *prsobj;
301 EUB :
302 GIC 73 : cfg = lookup_ts_config_cache(tsconfig);
303 73 : prsobj = lookup_ts_parser_cache(cfg->prsId);
304 :
305 CBC 73 : if (!OidIsValid(prsobj->headlineOid))
306 LBC 0 : ereport(ERROR,
307 ECB : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
308 : errmsg("text search parser does not support headline creation")));
309 :
310 CBC 73 : memset(&prs, 0, sizeof(HeadlineParsedText));
311 GIC 73 : prs.lenwords = 32;
312 CBC 73 : prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords);
313 ECB :
314 GIC 146 : hlparsetext(cfg->cfgId, &prs, query,
315 CBC 146 : VARDATA_ANY(in), VARSIZE_ANY_EXHDR(in));
316 :
317 73 : if (opt)
318 GIC 33 : prsoptions = deserialize_deflist(PointerGetDatum(opt));
319 : else
320 40 : prsoptions = NIL;
321 :
322 CBC 73 : FunctionCall3(&(prsobj->prsheadline),
323 : PointerGetDatum(&prs),
324 ECB : PointerGetDatum(prsoptions),
325 : PointerGetDatum(query));
326 :
327 CBC 73 : out = generateHeadline(&prs);
328 ECB :
329 CBC 73 : PG_FREE_IF_COPY(in, 1);
330 73 : PG_FREE_IF_COPY(query, 2);
331 GIC 73 : if (opt)
332 CBC 33 : PG_FREE_IF_COPY(opt, 3);
333 GIC 73 : pfree(prs.words);
334 73 : pfree(prs.startsel);
335 73 : pfree(prs.stopsel);
336 ECB :
337 GIC 73 : PG_RETURN_POINTER(out);
338 ECB : }
339 :
340 : Datum
341 GIC 40 : ts_headline_byid(PG_FUNCTION_ARGS)
342 : {
343 40 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_byid_opt,
344 : PG_GETARG_DATUM(0),
345 EUB : PG_GETARG_DATUM(1),
346 : PG_GETARG_DATUM(2)));
347 : }
348 :
349 : Datum
350 UIC 0 : ts_headline(PG_FUNCTION_ARGS)
351 : {
352 0 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_byid_opt,
353 : ObjectIdGetDatum(getTSCurrentConfig(true)),
354 EUB : PG_GETARG_DATUM(0),
355 : PG_GETARG_DATUM(1)));
356 : }
357 :
358 : Datum
359 UIC 0 : ts_headline_opt(PG_FUNCTION_ARGS)
360 : {
361 0 : PG_RETURN_DATUM(DirectFunctionCall4(ts_headline_byid_opt,
362 : ObjectIdGetDatum(getTSCurrentConfig(true)),
363 : PG_GETARG_DATUM(0),
364 ECB : PG_GETARG_DATUM(1),
365 : PG_GETARG_DATUM(2)));
366 : }
367 :
368 : Datum
369 CBC 21 : ts_headline_jsonb_byid_opt(PG_FUNCTION_ARGS)
370 : {
371 21 : Oid tsconfig = PG_GETARG_OID(0);
372 GIC 21 : Jsonb *jb = PG_GETARG_JSONB_P(1);
373 CBC 21 : TSQuery query = PG_GETARG_TSQUERY(2);
374 GIC 21 : text *opt = (PG_NARGS() > 3 && PG_GETARG_POINTER(3)) ? PG_GETARG_TEXT_P(3) : NULL;
375 ECB : Jsonb *out;
376 CBC 21 : JsonTransformStringValuesAction action = (JsonTransformStringValuesAction) headline_json_value;
377 ECB : HeadlineParsedText prs;
378 GIC 21 : HeadlineJsonState *state = palloc0(sizeof(HeadlineJsonState));
379 ECB :
380 CBC 21 : memset(&prs, 0, sizeof(HeadlineParsedText));
381 21 : prs.lenwords = 32;
382 21 : prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords);
383 ECB :
384 CBC 21 : state->prs = &prs;
385 GIC 21 : state->cfg = lookup_ts_config_cache(tsconfig);
386 CBC 21 : state->prsobj = lookup_ts_parser_cache(state->cfg->prsId);
387 GIC 21 : state->query = query;
388 CBC 21 : if (opt)
389 GBC 6 : state->prsoptions = deserialize_deflist(PointerGetDatum(opt));
390 : else
391 GIC 15 : state->prsoptions = NIL;
392 :
393 CBC 21 : if (!OidIsValid(state->prsobj->headlineOid))
394 UIC 0 : ereport(ERROR,
395 ECB : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
396 : errmsg("text search parser does not support headline creation")));
397 :
398 CBC 21 : out = transform_jsonb_string_values(jb, state, action);
399 :
400 21 : PG_FREE_IF_COPY(jb, 1);
401 GIC 21 : PG_FREE_IF_COPY(query, 2);
402 CBC 21 : if (opt)
403 GIC 6 : PG_FREE_IF_COPY(opt, 3);
404 ECB :
405 CBC 21 : pfree(prs.words);
406 :
407 GIC 21 : if (state->transformed)
408 ECB : {
409 GIC 12 : pfree(prs.startsel);
410 12 : pfree(prs.stopsel);
411 : }
412 ECB :
413 GIC 21 : PG_RETURN_JSONB_P(out);
414 ECB : }
415 :
416 : Datum
417 GIC 12 : ts_headline_jsonb(PG_FUNCTION_ARGS)
418 : {
419 12 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_jsonb_byid_opt,
420 : ObjectIdGetDatum(getTSCurrentConfig(true)),
421 ECB : PG_GETARG_DATUM(0),
422 : PG_GETARG_DATUM(1)));
423 : }
424 :
425 : Datum
426 GIC 3 : ts_headline_jsonb_byid(PG_FUNCTION_ARGS)
427 : {
428 3 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_jsonb_byid_opt,
429 : PG_GETARG_DATUM(0),
430 ECB : PG_GETARG_DATUM(1),
431 : PG_GETARG_DATUM(2)));
432 : }
433 :
434 : Datum
435 GIC 3 : ts_headline_jsonb_opt(PG_FUNCTION_ARGS)
436 : {
437 3 : PG_RETURN_DATUM(DirectFunctionCall4(ts_headline_jsonb_byid_opt,
438 : ObjectIdGetDatum(getTSCurrentConfig(true)),
439 : PG_GETARG_DATUM(0),
440 ECB : PG_GETARG_DATUM(1),
441 : PG_GETARG_DATUM(2)));
442 : }
443 :
444 : Datum
445 CBC 21 : ts_headline_json_byid_opt(PG_FUNCTION_ARGS)
446 : {
447 21 : Oid tsconfig = PG_GETARG_OID(0);
448 GIC 21 : text *json = PG_GETARG_TEXT_P(1);
449 21 : TSQuery query = PG_GETARG_TSQUERY(2);
450 CBC 21 : text *opt = (PG_NARGS() > 3 && PG_GETARG_POINTER(3)) ? PG_GETARG_TEXT_P(3) : NULL;
451 : text *out;
452 21 : JsonTransformStringValuesAction action = (JsonTransformStringValuesAction) headline_json_value;
453 ECB :
454 : HeadlineParsedText prs;
455 GIC 21 : HeadlineJsonState *state = palloc0(sizeof(HeadlineJsonState));
456 ECB :
457 CBC 21 : memset(&prs, 0, sizeof(HeadlineParsedText));
458 21 : prs.lenwords = 32;
459 21 : prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords);
460 ECB :
461 CBC 21 : state->prs = &prs;
462 GIC 21 : state->cfg = lookup_ts_config_cache(tsconfig);
463 CBC 21 : state->prsobj = lookup_ts_parser_cache(state->cfg->prsId);
464 GIC 21 : state->query = query;
465 CBC 21 : if (opt)
466 GBC 6 : state->prsoptions = deserialize_deflist(PointerGetDatum(opt));
467 : else
468 GIC 15 : state->prsoptions = NIL;
469 :
470 CBC 21 : if (!OidIsValid(state->prsobj->headlineOid))
471 UIC 0 : ereport(ERROR,
472 ECB : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
473 : errmsg("text search parser does not support headline creation")));
474 :
475 CBC 21 : out = transform_json_string_values(json, state, action);
476 ECB :
477 GIC 21 : PG_FREE_IF_COPY(json, 1);
478 CBC 21 : PG_FREE_IF_COPY(query, 2);
479 GIC 21 : if (opt)
480 CBC 6 : PG_FREE_IF_COPY(opt, 3);
481 21 : pfree(prs.words);
482 :
483 GIC 21 : if (state->transformed)
484 ECB : {
485 GIC 12 : pfree(prs.startsel);
486 12 : pfree(prs.stopsel);
487 : }
488 ECB :
489 GIC 21 : PG_RETURN_TEXT_P(out);
490 ECB : }
491 :
492 : Datum
493 GIC 12 : ts_headline_json(PG_FUNCTION_ARGS)
494 : {
495 12 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_json_byid_opt,
496 : ObjectIdGetDatum(getTSCurrentConfig(true)),
497 ECB : PG_GETARG_DATUM(0),
498 : PG_GETARG_DATUM(1)));
499 : }
500 :
501 : Datum
502 GIC 3 : ts_headline_json_byid(PG_FUNCTION_ARGS)
503 : {
504 3 : PG_RETURN_DATUM(DirectFunctionCall3(ts_headline_json_byid_opt,
505 : PG_GETARG_DATUM(0),
506 ECB : PG_GETARG_DATUM(1),
507 : PG_GETARG_DATUM(2)));
508 : }
509 :
510 : Datum
511 GIC 3 : ts_headline_json_opt(PG_FUNCTION_ARGS)
512 : {
513 3 : PG_RETURN_DATUM(DirectFunctionCall4(ts_headline_json_byid_opt,
514 : ObjectIdGetDatum(getTSCurrentConfig(true)),
515 : PG_GETARG_DATUM(0),
516 : PG_GETARG_DATUM(1),
517 : PG_GETARG_DATUM(2)));
518 : }
519 :
520 ECB :
521 : /*
522 : * Return headline in text from, generated from a json(b) element
523 : */
524 : static text *
525 CBC 114 : headline_json_value(void *_state, char *elem_value, int elem_len)
526 ECB : {
527 CBC 114 : HeadlineJsonState *state = (HeadlineJsonState *) _state;
528 ECB :
529 GIC 114 : HeadlineParsedText *prs = state->prs;
530 CBC 114 : TSConfigCacheEntry *cfg = state->cfg;
531 114 : TSParserCacheEntry *prsobj = state->prsobj;
532 114 : TSQuery query = state->query;
533 GIC 114 : List *prsoptions = state->prsoptions;
534 :
535 114 : prs->curwords = 0;
536 114 : hlparsetext(cfg->cfgId, prs, query, elem_value, elem_len);
537 CBC 114 : FunctionCall3(&(prsobj->prsheadline),
538 ECB : PointerGetDatum(prs),
539 : PointerGetDatum(prsoptions),
540 : PointerGetDatum(query));
541 :
542 GIC 114 : state->transformed = true;
543 114 : return generateHeadline(prs);
544 : }
|