Fixed record offset in handling of raw records
[pazpar2-moved-to-github.git] / src / client.c
1 /* This file is part of Pazpar2.
2    Copyright (C) 2006-2008 Index Data
3
4 Pazpar2 is free software; you can redistribute it and/or modify it under
5 the terms of the GNU General Public License as published by the Free
6 Software Foundation; either version 2, or (at your option) any later
7 version.
8
9 Pazpar2 is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18 */
19
20 /** \file client.c
21     \brief Z39.50 client 
22 */
23
24 #if HAVE_CONFIG_H
25 #include <config.h>
26 #endif
27
28 #include <stdlib.h>
29 #include <stdio.h>
30 #include <string.h>
31 #if HAVE_SYS_TIME_H
32 #include <sys/time.h>
33 #endif
34 #if HAVE_UNISTD_H
35 #include <unistd.h>
36 #endif
37 #if HAVE_SYS_SOCKET_H
38 #include <sys/socket.h>
39 #endif
40 #if HAVE_NETDB_H
41 #include <netdb.h>
42 #endif
43 #include <signal.h>
44 #include <ctype.h>
45 #include <assert.h>
46
47 #include <yaz/marcdisp.h>
48 #include <yaz/comstack.h>
49 #include <yaz/tcpip.h>
50 #include <yaz/proto.h>
51 #include <yaz/readconf.h>
52 #include <yaz/pquery.h>
53 #include <yaz/otherinfo.h>
54 #include <yaz/yaz-util.h>
55 #include <yaz/nmem.h>
56 #include <yaz/query-charset.h>
57 #include <yaz/querytowrbuf.h>
58 #include <yaz/oid_db.h>
59 #include <yaz/diagbib1.h>
60 #include <yaz/snprintf.h>
61
62 #define USE_TIMING 0
63 #if USE_TIMING
64 #include <yaz/timing.h>
65 #endif
66
67 #if HAVE_NETINET_IN_H
68 #include <netinet/in.h>
69 #endif
70
71 #include "pazpar2.h"
72
73 #include "client.h"
74 #include "connection.h"
75 #include "settings.h"
76
77 /** \brief Represents client state for a connection to one search target */
78 struct client {
79     struct session_database *database;
80     struct connection *connection;
81     struct session *session;
82     char *pquery; // Current search
83     int hits;
84     int records;
85     int setno;
86     int requestid;            // ID of current outstanding request
87     int diagnostic;
88     enum client_state state;
89     struct show_raw *show_raw;
90     struct client *next;     // next client in session or next in free list
91 };
92
93 struct show_raw {
94     int active; // whether this request has been sent to the server
95     int position;
96     int binary;
97     char *syntax;
98     char *esn;
99     void (*error_handler)(void *data, const char *addinfo);
100     void (*record_handler)(void *data, const char *buf, size_t sz);
101     void *data;
102     struct show_raw *next;
103 };
104
105 static const char *client_states[] = {
106     "Client_Connecting",
107     "Client_Connected",
108     "Client_Idle",
109     "Client_Initializing",
110     "Client_Searching",
111     "Client_Presenting",
112     "Client_Error",
113     "Client_Failed",
114     "Client_Disconnected",
115     "Client_Stopped",
116     "Client_Continue"
117 };
118
119 static struct client *client_freelist = 0;
120
121 const char *client_get_state_str(struct client *cl)
122 {
123     return client_states[cl->state];
124 }
125
126 enum client_state client_get_state(struct client *cl)
127 {
128     return cl->state;
129 }
130
131 void client_set_state(struct client *cl, enum client_state st)
132 {
133     cl->state = st;
134     if (cl->session)
135     {
136         int no_active = session_active_clients(cl->session);
137         if (no_active == 0)
138             session_alert_watch(cl->session, SESSION_WATCH_SHOW);
139     }
140 }
141
142 static void client_show_raw_error(struct client *cl, const char *addinfo);
143
144 // Close connection and set state to error
145 void client_fatal(struct client *cl)
146 {
147     //client_show_raw_error(cl, "client connection failure");
148     yaz_log(YLOG_WARN, "Fatal error from %s", client_get_url(cl));
149     connection_destroy(cl->connection);
150     client_set_state(cl, Client_Error);
151 }
152
153 struct connection *client_get_connection(struct client *cl)
154 {
155     return cl->connection;
156 }
157
158 struct session_database *client_get_database(struct client *cl)
159 {
160     return cl->database;
161 }
162
163 struct session *client_get_session(struct client *cl)
164 {
165     return cl->session;
166 }
167
168 const char *client_get_pquery(struct client *cl)
169 {
170     return cl->pquery;
171 }
172
173 void client_set_requestid(struct client *cl, int id)
174 {
175     cl->requestid = id;
176 }
177
178
179 static void client_send_raw_present(struct client *cl);
180
181 int client_show_raw_begin(struct client *cl, int position,
182                           const char *syntax, const char *esn,
183                           void *data,
184                           void (*error_handler)(void *data, const char *addinfo),
185                           void (*record_handler)(void *data, const char *buf,
186                                                  size_t sz),
187                           void **data2,
188                           int binary)
189 {
190     struct show_raw *rr, **rrp;
191     if (!cl->connection)
192     {   /* the client has no connection */
193         return -1;
194     }
195     rr = xmalloc(sizeof(*rr));
196     *data2 = rr;
197     rr->position = position;
198     rr->active = 0;
199     rr->data = data;
200     rr->error_handler = error_handler;
201     rr->record_handler = record_handler;
202     rr->binary = binary;
203     if (syntax)
204         rr->syntax = xstrdup(syntax);
205     else
206         rr->syntax = 0;
207     if (esn)
208         rr->esn = xstrdup(esn);
209     else
210         rr->esn = 0;
211     rr->next = 0;
212     
213     for (rrp = &cl->show_raw; *rrp; rrp = &(*rrp)->next)
214         ;
215     *rrp = rr;
216     
217     if (cl->state == Client_Failed)
218     {
219         client_show_raw_error(cl, "client failed");
220     }
221     else if (cl->state == Client_Disconnected)
222     {
223         client_show_raw_error(cl, "client disconnected");
224     }
225     else
226     {
227         client_send_raw_present(cl);
228     }
229     return 0;
230 }
231
232 void client_show_raw_remove(struct client *cl, void *data)
233 {
234     struct show_raw *rr = data;
235     struct show_raw **rrp = &cl->show_raw;
236     while (*rrp != rr)
237         rrp = &(*rrp)->next;
238     if (*rrp)
239     {
240         *rrp = rr->next;
241         xfree(rr);
242     }
243 }
244
245 void client_show_raw_dequeue(struct client *cl)
246 {
247     struct show_raw *rr = cl->show_raw;
248
249     cl->show_raw = rr->next;
250     xfree(rr);
251 }
252
253 static void client_show_raw_error(struct client *cl, const char *addinfo)
254 {
255     while (cl->show_raw)
256     {
257         cl->show_raw->error_handler(cl->show_raw->data, addinfo);
258         client_show_raw_dequeue(cl);
259     }
260 }
261
262 static void client_show_raw_cancel(struct client *cl)
263 {
264     while (cl->show_raw)
265     {
266         cl->show_raw->error_handler(cl->show_raw->data, "cancel");
267         client_show_raw_dequeue(cl);
268     }
269 }
270
271 static void client_send_raw_present(struct client *cl)
272 {
273     struct session_database *sdb = client_get_database(cl);
274     struct connection *co = client_get_connection(cl);
275     ZOOM_resultset set = connection_get_resultset(co);
276
277     int offset = cl->show_raw->position;
278     const char *syntax = 0;
279     const char *elements = 0;
280
281     assert(cl->show_raw);
282     assert(set);
283
284     yaz_log(YLOG_DEBUG, "%s: trying to present %d record(s) from %d",
285             client_get_url(cl), 1, offset);
286
287     if (cl->show_raw->syntax)
288         syntax = cl->show_raw->syntax;
289     else
290         syntax = session_setting_oneval(sdb, PZ_REQUESTSYNTAX);
291     ZOOM_resultset_option_set(set, "preferredRecordSyntax", syntax);
292
293     if (cl->show_raw->esn)
294         elements = cl->show_raw->esn;
295     else
296         elements = session_setting_oneval(sdb, PZ_ELEMENTS);
297     if (elements && *elements)
298         ZOOM_resultset_option_set(set, "elementSetName", elements);
299
300     ZOOM_resultset_records(set, 0, offset-1, 1);
301     cl->show_raw->active = 1;
302
303     connection_continue(co);
304 }
305
306 static void ingest_raw_record(struct client *cl, ZOOM_record rec)
307 {
308     const char *buf;
309     int len;
310     char type[50];
311
312     if (cl->show_raw->binary)
313         strcpy(type, "raw");
314     else
315     {
316         struct session_database *sdb = client_get_database(cl);
317         const char *cset;
318
319         const char *nativesyntax = session_setting_oneval(sdb, PZ_NATIVESYNTAX);
320         if (*nativesyntax && (cset = strchr(nativesyntax, ';')))
321             yaz_snprintf(type, sizeof(type)-1, "xml; charset=%s", cset);
322         else
323             strcpy(type, "xml");
324     }
325
326     buf = ZOOM_record_get(rec, type, &len);
327     cl->show_raw->record_handler(cl->show_raw->data,  buf, len);
328     client_show_raw_dequeue(cl);
329 }
330
331 #ifdef RETIRED
332
333 static void ingest_raw_records(struct client *cl, Z_Records *r)
334 {
335     Z_NamePlusRecordList *rlist;
336     Z_NamePlusRecord *npr;
337     xmlDoc *doc;
338     xmlChar *buf_out;
339     int len_out;
340     if (r->which != Z_Records_DBOSD)
341     {
342         client_show_raw_error(cl, "non-surrogate diagnostics");
343         return;
344     }
345
346     rlist = r->u.databaseOrSurDiagnostics;
347     if (rlist->num_records != 1 || !rlist->records || !rlist->records[0])
348     {
349         client_show_raw_error(cl, "no records");
350         return;
351     }
352     npr = rlist->records[0];
353     if (npr->which != Z_NamePlusRecord_databaseRecord)
354     {
355         client_show_raw_error(cl, "surrogate diagnostic");
356         return;
357     }
358
359     if (cl->show_raw && cl->show_raw->binary)
360     {
361         Z_External *rec = npr->u.databaseRecord;
362         if (rec->which == Z_External_octet)
363         {
364             cl->show_raw->record_handler(cl->show_raw->data,
365                                          (const char *)
366                                          rec->u.octet_aligned->buf,
367                                          rec->u.octet_aligned->len);
368             client_show_raw_dequeue(cl);
369         }
370         else
371             client_show_raw_error(cl, "no records");
372     }
373
374     doc = record_to_xml(client_get_database(cl), npr->u.databaseRecord);
375     if (!doc)
376     {
377         client_show_raw_error(cl, "unable to convert record to xml");
378         return;
379     }
380
381     xmlDocDumpMemory(doc, &buf_out, &len_out);
382     xmlFreeDoc(doc);
383
384     if (cl->show_raw)
385     {
386         cl->show_raw->record_handler(cl->show_raw->data,
387                                      (const char *) buf_out, len_out);
388         client_show_raw_dequeue(cl);
389     }
390     xmlFree(buf_out);
391 }
392
393 #endif // RETIRED show raw
394
395 void client_search_response(struct client *cl)
396 {
397     struct connection *co = cl->connection;
398     struct session *se = cl->session;
399     ZOOM_connection link = connection_get_link(co);
400     ZOOM_resultset resultset = connection_get_resultset(co);
401     const char *error, *addinfo;
402
403     if (ZOOM_connection_error(link, &error, &addinfo))
404     {
405         cl->hits = 0;
406         cl->state = Client_Error;
407         yaz_log(YLOG_WARN, "Search error %s (%s): %s",
408             error, addinfo, client_get_url(cl));
409     }
410     else
411     {
412         cl->hits = ZOOM_resultset_size(resultset);
413         se->total_hits += cl->hits;
414     }
415 }
416
417 void client_record_response(struct client *cl)
418 {
419     struct connection *co = cl->connection;
420     ZOOM_connection link = connection_get_link(co);
421     ZOOM_resultset resultset = connection_get_resultset(co);
422     const char *error, *addinfo;
423
424     yaz_log(YLOG_LOG, "client_record_response");
425     if (ZOOM_connection_error(link, &error, &addinfo))
426     {
427         cl->state = Client_Error;
428         yaz_log(YLOG_WARN, "Search error %s (%s): %s",
429             error, addinfo, client_get_url(cl));
430     }
431     else
432     {
433         ZOOM_record rec = 0;
434         const char *msg, *addinfo;
435         
436         yaz_log(YLOG_LOG, "show_raw=%p show_raw->active=%d",
437                 cl->show_raw, cl->show_raw ? cl->show_raw->active : 0);
438         if (cl->show_raw && cl->show_raw->active)
439         {
440             if ((rec = ZOOM_resultset_record(resultset,
441                                              cl->show_raw->position-1)))
442             {
443                 cl->show_raw->active = 0;
444                 ingest_raw_record(cl, rec);
445             }
446         }
447         else
448         {
449             int offset = cl->records;
450             if ((rec = ZOOM_resultset_record(resultset, offset)))
451             {
452                 yaz_log(YLOG_LOG, "Record with offset %d", offset);
453                 
454                 cl->records++;
455                 if (ZOOM_record_error(rec, &msg, &addinfo, 0))
456                     yaz_log(YLOG_WARN, "Record error %s (%s): %s (rec #%d)",
457                             error, addinfo, client_get_url(cl), cl->records);
458                 else
459                 {
460                     struct session_database *sdb = client_get_database(cl);
461                     const char *xmlrec;
462                     char type[128] = "xml";
463                     const char *nativesyntax =
464                         session_setting_oneval(sdb, PZ_NATIVESYNTAX);
465                     char *cset;
466                     
467                     if (*nativesyntax && (cset = strchr(nativesyntax, ';')))
468                         sprintf(type, "xml; charset=%s", cset + 1);
469                     
470                     if ((xmlrec = ZOOM_record_get(rec, type, NULL)))
471                     {
472                         if (ingest_record(cl, xmlrec, cl->records))
473                         {
474                             session_alert_watch(cl->session, SESSION_WATCH_SHOW);
475                             session_alert_watch(cl->session, SESSION_WATCH_RECORD);
476                         }
477                         else
478                             yaz_log(YLOG_WARN, "Failed to ingest");
479                     }
480                     else
481                         yaz_log(YLOG_WARN, "Failed to extract ZOOM record");
482                 }
483
484             }
485         }
486         if (!rec)
487             yaz_log(YLOG_WARN, "Expected record, but got NULL");
488     }
489 }
490
491 #ifdef RETIRED
492
493 void client_present_response(struct client *cl, Z_APDU *a)
494 {
495     Z_PresentResponse *r = a->u.presentResponse;
496     Z_Records *recs = r->records;
497         
498     if (recs && recs->which == Z_Records_NSD)
499     {
500         WRBUF w = wrbuf_alloc();
501         
502         Z_DiagRec dr, *dr_p = &dr;
503         dr.which = Z_DiagRec_defaultFormat;
504         dr.u.defaultFormat = recs->u.nonSurrogateDiagnostic;
505         
506         wrbuf_printf(w, "Present response NSD %s: ",
507                      cl->database->database->url);
508         
509         cl->diagnostic = diag_to_wrbuf(&dr_p, 1, w);
510         
511         yaz_log(YLOG_WARN, "%s", wrbuf_cstr(w));
512         
513         cl->state = Client_Error;
514         wrbuf_destroy(w);
515
516         client_show_raw_error(cl, "non surrogate diagnostics");
517     }
518     else if (recs && recs->which == Z_Records_multipleNSD)
519     {
520         WRBUF w = wrbuf_alloc();
521         
522         wrbuf_printf(w, "Present response multipleNSD %s: ",
523                      cl->database->database->url);
524         cl->diagnostic = 
525             diag_to_wrbuf(recs->u.multipleNonSurDiagnostics->diagRecs,
526                           recs->u.multipleNonSurDiagnostics->num_diagRecs,
527                           w);
528         yaz_log(YLOG_WARN, "%s", wrbuf_cstr(w));
529         cl->state = Client_Error;
530         wrbuf_destroy(w);
531     }
532     else if (recs && !*r->presentStatus && cl->state != Client_Error)
533     {
534         yaz_log(YLOG_DEBUG, "Good Present response %s",
535                 cl->database->database->url);
536
537         // we can mix show raw and normal show ..
538         if (cl->show_raw && cl->show_raw->active)
539         {
540             cl->show_raw->active = 0; // no longer active
541             ingest_raw_records(cl, recs);
542         }
543         else
544             ingest_records(cl, recs);
545         cl->state = Client_Continue;
546     }
547     else if (*r->presentStatus) 
548     {
549         yaz_log(YLOG_WARN, "Bad Present response %s",
550                 cl->database->database->url);
551         cl->state = Client_Error;
552         client_show_raw_error(cl, "bad present response");
553     }
554 }
555
556 void client_close_response(struct client *cl, Z_APDU *a)
557 {
558     struct connection *co = cl->connection;
559     /* Z_Close *r = a->u.close; */
560
561     yaz_log(YLOG_WARN, "Close response %s", cl->database->database->url);
562
563     cl->state = Client_Failed;
564     connection_destroy(co);
565 }
566
567 #endif // RETIRED show raw
568
569 #ifdef RETIRED
570 int client_is_our_response(struct client *cl)
571 {
572     struct session *se = client_get_session(cl);
573
574     if (cl && (cl->requestid == se->requestid || 
575                cl->state == Client_Initializing))
576         return 1;
577     return 0;
578 }
579 #endif
580
581 void client_start_search(struct client *cl)
582 {
583     struct session_database *sdb = client_get_database(cl);
584     struct connection *co = client_get_connection(cl);
585     ZOOM_connection link = connection_get_link(co);
586     ZOOM_resultset rs;
587     char *databaseName = sdb->database->databases[0];
588     const char *opt_piggyback = session_setting_oneval(sdb, PZ_PIGGYBACK);
589     const char *opt_queryenc = session_setting_oneval(sdb, PZ_QUERYENCODING);
590     const char *opt_elements = session_setting_oneval(sdb, PZ_ELEMENTS);
591     const char *opt_requestsyn = session_setting_oneval(sdb, PZ_REQUESTSYNTAX);
592     const char *opt_maxrecs = session_setting_oneval(sdb, PZ_MAXRECS);
593
594     assert(link);
595
596     cl->hits = -1;
597     cl->records = 0;
598     cl->diagnostic = 0;
599
600     if (*opt_piggyback)
601         ZOOM_connection_option_set(link, "piggyback", opt_piggyback);
602     else
603         ZOOM_connection_option_set(link, "piggyback", "1");
604     if (*opt_queryenc)
605         ZOOM_connection_option_set(link, "rpnCharset", opt_queryenc);
606     if (*opt_elements)
607         ZOOM_connection_option_set(link, "elementSetName", opt_elements);
608     if (*opt_requestsyn)
609         ZOOM_connection_option_set(link, "preferredRecordSyntax", opt_requestsyn);
610     if (*opt_maxrecs)
611         ZOOM_connection_option_set(link, "count", opt_maxrecs);
612     else
613     {
614         char n[128];
615         sprintf(n, "%d", global_parameters.toget);
616         ZOOM_connection_option_set(link, "count", n);
617     }
618     if (!databaseName || !*databaseName)
619         databaseName = "Default";
620     ZOOM_connection_option_set(link, "databaseName", databaseName);
621
622     ZOOM_connection_option_set(link, "presentChunk", "20");
623
624     rs = ZOOM_connection_search_pqf(link, cl->pquery);
625     connection_set_resultset(co, rs);
626     connection_continue(co);
627 }
628
629 struct client *client_create(void)
630 {
631     struct client *r;
632     if (client_freelist)
633     {
634         r = client_freelist;
635         client_freelist = client_freelist->next;
636     }
637     else
638         r = xmalloc(sizeof(struct client));
639     r->pquery = 0;
640     r->database = 0;
641     r->connection = 0;
642     r->session = 0;
643     r->hits = 0;
644     r->records = 0;
645     r->setno = 0;
646     r->requestid = -1;
647     r->diagnostic = 0;
648     r->state = Client_Disconnected;
649     r->show_raw = 0;
650     r->next = 0;
651     return r;
652 }
653
654 void client_destroy(struct client *c)
655 {
656     struct session *se = c->session;
657     if (c == se->clients)
658         se->clients = c->next;
659     else
660     {
661         struct client *cc;
662         for (cc = se->clients; cc && cc->next != c; cc = cc->next)
663             ;
664         if (cc)
665             cc->next = c->next;
666     }
667     xfree(c->pquery);
668
669     if (c->connection)
670         connection_release(c->connection);
671     c->next = client_freelist;
672     client_freelist = c;
673 }
674
675 void client_set_connection(struct client *cl, struct connection *con)
676 {
677     cl->connection = con;
678 }
679
680 void client_disconnect(struct client *cl)
681 {
682     if (cl->state != Client_Idle)
683         client_set_state(cl, Client_Disconnected);
684     client_set_connection(cl, 0);
685 }
686
687 // Extract terms from query into null-terminated termlist
688 static void extract_terms(NMEM nmem, struct ccl_rpn_node *query, char **termlist)
689 {
690     int num = 0;
691
692     pull_terms(nmem, query, termlist, &num);
693     termlist[num] = 0;
694 }
695
696 // Initialize CCL map for a target
697 static CCL_bibset prepare_cclmap(struct client *cl)
698 {
699     struct session_database *sdb = client_get_database(cl);
700     struct setting *s;
701     CCL_bibset res;
702
703     if (!sdb->settings)
704         return 0;
705     res = ccl_qual_mk();
706     for (s = sdb->settings[PZ_CCLMAP]; s; s = s->next)
707     {
708         char *p = strchr(s->name + 3, ':');
709         if (!p)
710         {
711             yaz_log(YLOG_WARN, "Malformed cclmap name: %s", s->name);
712             ccl_qual_rm(&res);
713             return 0;
714         }
715         p++;
716         ccl_qual_fitem(res, s->value, p);
717     }
718     return res;
719 }
720
721 // Parse the query given the settings specific to this client
722 int client_parse_query(struct client *cl, const char *query)
723 {
724     struct session *se = client_get_session(cl);
725     struct ccl_rpn_node *cn;
726     int cerror, cpos;
727     CCL_bibset ccl_map = prepare_cclmap(cl);
728
729     if (!ccl_map)
730         return -1;
731
732     cn = ccl_find_str(ccl_map, query, &cerror, &cpos);
733     ccl_qual_rm(&ccl_map);
734     if (!cn)
735     {
736         cl->state = Client_Error;
737         yaz_log(YLOG_WARN, "Failed to parse query for %s",
738                          client_get_database(cl)->database->url);
739         return -1;
740     }
741     wrbuf_rewind(se->wrbuf);
742     ccl_pquery(se->wrbuf, cn);
743     xfree(cl->pquery);
744     cl->pquery = xstrdup(wrbuf_cstr(se->wrbuf));
745
746     if (!se->relevance)
747     {
748         // Initialize relevance structure with query terms
749         char *p[512];
750         extract_terms(se->nmem, cn, p);
751         se->relevance = relevance_create(
752             global_parameters.server->relevance_pct,
753             se->nmem, (const char **) p,
754             se->expected_maxrecs);
755     }
756
757     ccl_rpn_delete(cn);
758     return 0;
759 }
760
761 void client_set_session(struct client *cl, struct session *se)
762 {
763     cl->session = se;
764     cl->next = se->clients;
765     se->clients = cl;
766 }
767
768 int client_is_active(struct client *cl)
769 {
770     if (cl->connection && (cl->state == Client_Continue ||
771                            cl->state == Client_Connecting ||
772                            cl->state == Client_Working))
773         return 1;
774     return 0;
775 }
776
777 struct client *client_next_in_session(struct client *cl)
778 {
779     if (cl)
780         return cl->next;
781     return 0;
782
783 }
784
785 int client_get_hits(struct client *cl)
786 {
787     return cl->hits;
788 }
789
790 int client_get_num_records(struct client *cl)
791 {
792     return cl->records;
793 }
794
795 int client_get_diagnostic(struct client *cl)
796 {
797     return cl->diagnostic;
798 }
799
800 void client_set_database(struct client *cl, struct session_database *db)
801 {
802     cl->database = db;
803 }
804
805 struct host *client_get_host(struct client *cl)
806 {
807     return client_get_database(cl)->database->host;
808 }
809
810 const char *client_get_url(struct client *cl)
811 {
812     return client_get_database(cl)->database->url;
813 }
814
815 /*
816  * Local variables:
817  * c-basic-offset: 4
818  * indent-tabs-mode: nil
819  * End:
820  * vim: shiftwidth=4 tabstop=8 expandtab
821  */