Added piggyback support for filters backend_test + multi.
[metaproxy-moved-to-github.git] / src / filter_multi.cpp
1 /* $Id: filter_multi.cpp,v 1.5 2006-01-17 17:55:40 adam Exp $
2    Copyright (c) 2005, Index Data.
3
4 %LICENSE%
5  */
6
7 #include "config.hpp"
8
9 #include "filter.hpp"
10 #include "package.hpp"
11
12 #include <boost/thread/thread.hpp>
13 #include <boost/thread/mutex.hpp>
14 #include <boost/thread/condition.hpp>
15 #include <boost/shared_ptr.hpp>
16
17 #include "util.hpp"
18 #include "filter_multi.hpp"
19
20 #include <yaz/zgdu.h>
21 #include <yaz/otherinfo.h>
22 #include <yaz/diagbib1.h>
23
24 #include <map>
25 #include <iostream>
26
27 namespace yf = yp2::filter;
28
29 namespace yp2 {
30     namespace filter {
31
32         struct Multi::BackendSet {
33             BackendPtr m_backend;
34             int m_count;
35             bool operator < (const BackendSet &k) const;
36         };
37         struct Multi::FrontendSet {
38             struct PresentJob {
39                 BackendPtr m_backend;
40                 int m_pos;
41                 int m_inside_pos;
42             };
43             FrontendSet(std::string setname);
44             FrontendSet();
45             ~FrontendSet();
46
47             void round_robin(int pos, int number, std::list<PresentJob> &job);
48
49             std::list<BackendSet> m_backend_sets;
50             std::string m_setname;
51         };
52         struct Multi::Backend {
53             PackagePtr m_package;
54             std::string m_backend_database;
55             std::string m_vhost;
56             std::string m_route;
57             void operator() (void);  // thread operation
58         };
59         struct Multi::Frontend {
60             Frontend(Rep *rep);
61             ~Frontend();
62             bool m_is_multi;
63             bool m_in_use;
64             std::list<BackendPtr> m_backend_list;
65             std::map<std::string,Multi::FrontendSet> m_sets;
66
67             void multi_move(std::list<BackendPtr> &blist);
68             void init(Package &package, Z_GDU *gdu);
69             void close(Package &package);
70             void search(Package &package, Z_APDU *apdu);
71             void present(Package &package, Z_APDU *apdu);
72             Rep *m_p;
73         };            
74         struct Multi::Map {
75             Map(std::list<std::string> hosts, std::string route);
76             Map();
77             std::list<std::string> m_hosts;
78             std::string m_route;
79         };
80         class Multi::Rep {
81             friend class Multi;
82             friend class Frontend;
83             
84             FrontendPtr get_frontend(Package &package);
85             void release_frontend(Package &package);
86         private:
87             boost::mutex m_sessions_mutex;
88             std::map<std::string, Multi::Map>m_maps;
89
90             boost::mutex m_mutex;
91             boost::condition m_cond_session_ready;
92             std::map<yp2::Session, FrontendPtr> m_clients;
93         };
94     }
95 }
96
97 using namespace yp2;
98
99 bool yf::Multi::BackendSet::operator < (const BackendSet &k) const
100 {
101     return m_count < k.m_count;
102 }
103
104 yf::Multi::Frontend::Frontend(Rep *rep)
105 {
106     m_p = rep;
107     m_is_multi = false;
108 }
109
110 yf::Multi::Frontend::~Frontend()
111 {
112 }
113
114 yf::Multi::FrontendPtr yf::Multi::Rep::get_frontend(Package &package)
115 {
116     boost::mutex::scoped_lock lock(m_mutex);
117
118     std::map<yp2::Session,yf::Multi::FrontendPtr>::iterator it;
119     
120     while(true)
121     {
122         it = m_clients.find(package.session());
123         if (it == m_clients.end())
124             break;
125         
126         if (!it->second->m_in_use)
127         {
128             it->second->m_in_use = true;
129             return it->second;
130         }
131         m_cond_session_ready.wait(lock);
132     }
133     FrontendPtr f(new Frontend(this));
134     m_clients[package.session()] = f;
135     f->m_in_use = true;
136     return f;
137 }
138
139 void yf::Multi::Rep::release_frontend(Package &package)
140 {
141     boost::mutex::scoped_lock lock(m_mutex);
142     std::map<yp2::Session,yf::Multi::FrontendPtr>::iterator it;
143     
144     it = m_clients.find(package.session());
145     if (it != m_clients.end())
146     {
147         if (package.session().is_closed())
148         {
149             it->second->close(package);
150             m_clients.erase(it);
151         }
152         else
153         {
154             it->second->m_in_use = false;
155         }
156         m_cond_session_ready.notify_all();
157     }
158 }
159
160 yf::Multi::FrontendSet::FrontendSet(std::string setname)
161     :  m_setname(setname)
162 {
163 }
164
165
166 yf::Multi::FrontendSet::FrontendSet()
167 {
168 }
169
170
171 yf::Multi::FrontendSet::~FrontendSet()
172 {
173 }
174
175 yf::Multi::Map::Map(std::list<std::string> hosts, std::string route)
176     : m_hosts(hosts), m_route(route) 
177 {
178 }
179
180 yf::Multi::Map::Map()
181 {
182 }
183
184 yf::Multi::Multi() : m_p(new Multi::Rep)
185 {
186 }
187
188 yf::Multi::~Multi() {
189 }
190
191
192 void yf::Multi::add_map_host2hosts(std::string host,
193                                    std::list<std::string> hosts,
194                                    std::string route)
195 {
196     m_p->m_maps[host] = Multi::Map(hosts, route);
197 }
198
199 void yf::Multi::Backend::operator() (void) 
200 {
201     m_package->move(m_route);
202 }
203
204 void yf::Multi::Frontend::close(Package &package)
205 {
206     std::list<BackendPtr>::const_iterator bit;
207     for (bit = m_backend_list.begin(); bit != m_backend_list.end(); bit++)
208     {
209         BackendPtr b = *bit;
210
211         b->m_package->copy_filter(package);
212         b->m_package->request() = (Z_GDU *) 0;
213         b->m_package->session().close();
214         b->m_package->move(b->m_route);
215     }
216 }
217
218 void yf::Multi::Frontend::multi_move(std::list<BackendPtr> &blist)
219 {
220     std::list<BackendPtr>::const_iterator bit;
221     boost::thread_group g;
222     for (bit = blist.begin(); bit != blist.end(); bit++)
223     {
224         g.add_thread(new boost::thread(**bit));
225     }
226     g.join_all();
227 }
228
229
230 void yf::Multi::FrontendSet::round_robin(int start, int number,
231                                          std::list<PresentJob> &jobs)
232 {
233     int fetched = 0;
234     int p = 1;
235     bool eof = true;
236
237     std::list<int> pos;
238     std::list<int> inside_pos;
239     std::list<BackendSet>::const_iterator bsit;
240     for (bsit = m_backend_sets.begin(); bsit != m_backend_sets.end(); bsit++)
241     {
242         pos.push_back(1);
243         inside_pos.push_back(0);
244     }
245
246     std::list<int>::iterator psit = pos.begin();
247     std::list<int>::iterator esit = inside_pos.begin();
248     bsit = m_backend_sets.begin();
249     while (fetched < number)
250     {
251         if (bsit == m_backend_sets.end())
252         {
253             psit = pos.begin();
254             esit = inside_pos.begin();
255             bsit = m_backend_sets.begin();
256             if (eof)
257                 break;
258             eof = true;
259         }
260         if (*psit <= bsit->m_count)
261         {
262             if (p >= start)
263             {
264                 PresentJob job;
265                 job.m_backend = bsit->m_backend;
266                 job.m_pos = *psit;
267                 job.m_inside_pos = *esit;
268                 jobs.push_back(job);
269                 (*esit)++;
270                 fetched++;
271             }
272             (*psit)++;
273             p++;
274             eof = false;
275         }
276         psit++;
277         esit++;
278         bsit++;
279     }
280 }
281
282 void yf::Multi::Frontend::init(Package &package, Z_GDU *gdu)
283 {
284     Z_InitRequest *req = gdu->u.z3950->u.initRequest;
285
286     std::list<std::string> targets;
287
288     yp2::util::get_vhost_otherinfo(&req->otherInfo, false, targets);
289
290     if (targets.size() < 1)
291     {
292         package.move();
293         return;
294     }
295
296     std::list<std::string>::const_iterator t_it = targets.begin();
297     for (; t_it != targets.end(); t_it++)
298     {
299         Session s;
300         Backend *b = new Backend;
301         b->m_vhost = *t_it;
302
303         // b->m_route unset
304         b->m_package = PackagePtr(new Package(s, package.origin()));
305
306         m_backend_list.push_back(BackendPtr(b));
307     }
308     m_is_multi = true;
309
310     // create init request 
311     std::list<BackendPtr>::const_iterator bit;
312     for (bit = m_backend_list.begin(); bit != m_backend_list.end(); bit++)
313     {
314         yp2::odr odr;
315         BackendPtr b = *bit;
316         Z_APDU *init_apdu = zget_APDU(odr, Z_APDU_initRequest);
317         
318         yaz_oi_set_string_oidval(&init_apdu->u.initRequest->otherInfo, odr,
319                                  VAL_PROXY, 1, b->m_vhost.c_str());
320         
321         Z_InitRequest *req = init_apdu->u.initRequest;
322         
323         ODR_MASK_SET(req->options, Z_Options_search);
324         ODR_MASK_SET(req->options, Z_Options_present);
325         ODR_MASK_SET(req->options, Z_Options_namedResultSets);
326         
327         ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_1);
328         ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_2);
329         ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_3);
330         
331         b->m_package->request() = init_apdu;
332
333         b->m_package->copy_filter(package);
334     }
335     multi_move(m_backend_list);
336
337     // create the frontend init response based on each backend init response
338     yp2::odr odr;
339
340     Z_APDU *f_apdu = odr.create_initResponse(gdu->u.z3950, 0, 0);
341     Z_InitResponse *f_resp = f_apdu->u.initResponse;
342
343     ODR_MASK_SET(f_resp->options, Z_Options_search);
344     ODR_MASK_SET(f_resp->options, Z_Options_present);
345     ODR_MASK_SET(f_resp->options, Z_Options_namedResultSets);
346     
347     ODR_MASK_SET(f_resp->protocolVersion, Z_ProtocolVersion_1);
348     ODR_MASK_SET(f_resp->protocolVersion, Z_ProtocolVersion_2);
349     ODR_MASK_SET(f_resp->protocolVersion, Z_ProtocolVersion_3);
350
351     for (bit = m_backend_list.begin(); bit != m_backend_list.end(); bit++)
352     {
353         PackagePtr p = (*bit)->m_package;
354         
355         if (p->session().is_closed()) // if any backend closes, close frontend
356             package.session().close();
357         Z_GDU *gdu = p->response().get();
358         if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
359             Z_APDU_initResponse)
360         {
361             int i;
362             Z_APDU *b_apdu = gdu->u.z3950;
363             Z_InitResponse *b_resp = b_apdu->u.initResponse;
364
365             // common options for all backends
366             for (i = 0; i <= Z_Options_stringSchema; i++)
367             {
368                 if (!ODR_MASK_GET(b_resp->options, i))
369                     ODR_MASK_CLEAR(f_resp->options, i);
370             }
371             // common protocol version
372             for (i = 0; i <= Z_ProtocolVersion_3; i++)
373                 if (!ODR_MASK_GET(b_resp->protocolVersion, i))
374                     ODR_MASK_CLEAR(f_resp->protocolVersion, i);
375             // reject if any of the backends reject
376             if (!*b_resp->result)
377                 *f_resp->result = 0;
378         }
379         else
380         {
381             // if any target does not return init return that (close or
382             // similar )
383             package.response() = p->response();
384             return;
385         }
386     }
387     package.response() = f_apdu;
388 }
389
390 void yf::Multi::Frontend::search(Package &package, Z_APDU *apdu_req)
391 {
392     // create search request 
393     Z_SearchRequest *req = apdu_req->u.searchRequest;
394
395     // save these for later
396     int smallSetUpperBound = *req->smallSetUpperBound;
397     int largeSetLowerBound = *req->largeSetLowerBound;
398     int mediumSetPresentNumber = *req->mediumSetPresentNumber;
399     
400     // they are altered now - to disable piggyback
401     *req->smallSetUpperBound = 0;
402     *req->largeSetLowerBound = 1;
403     *req->mediumSetPresentNumber = 1;
404
405     int default_num_db = req->num_databaseNames;
406     char **default_db = req->databaseNames;
407
408     std::list<BackendPtr>::const_iterator bit;
409     for (bit = m_backend_list.begin(); bit != m_backend_list.end(); bit++)
410     {
411         PackagePtr p = (*bit)->m_package;
412         yp2::odr odr;
413     
414         if (!yp2::util::set_databases_from_zurl(odr, (*bit)->m_vhost,
415                                                 &req->num_databaseNames,
416                                                 &req->databaseNames))
417         {
418             req->num_databaseNames = default_num_db;
419             req->databaseNames = default_db;
420         }
421         p->request() = apdu_req;
422         p->copy_filter(package);
423     }
424     multi_move(m_backend_list);
425
426     // look at each response
427     FrontendSet resultSet(std::string(req->resultSetName));
428
429     int result_set_size = 0;
430     Z_Records *z_records_diag = 0;  // no diagnostics (yet)
431     for (bit = m_backend_list.begin(); bit != m_backend_list.end(); bit++)
432     {
433         PackagePtr p = (*bit)->m_package;
434         
435         if (p->session().is_closed()) // if any backend closes, close frontend
436             package.session().close();
437         
438         Z_GDU *gdu = p->response().get();
439         if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
440             Z_APDU_searchResponse)
441         {
442             Z_APDU *b_apdu = gdu->u.z3950;
443             Z_SearchResponse *b_resp = b_apdu->u.searchResponse;
444          
445             // see we get any errors (AKA diagnstics)
446             if (b_resp->records)
447             {
448                 if (b_resp->records->which == Z_Records_NSD
449                     || b_resp->records->which == Z_Records_multipleNSD)
450                     z_records_diag = b_resp->records;
451                 // we may set this multiple times (TOO BAD!)
452             }
453             BackendSet backendSet;
454             backendSet.m_backend = *bit;
455             backendSet.m_count = *b_resp->resultCount;
456             result_set_size += *b_resp->resultCount;
457             resultSet.m_backend_sets.push_back(backendSet);
458         }
459         else
460         {
461             // if any target does not return search response - return that 
462             package.response() = p->response();
463             return;
464         }
465     }
466
467     yp2::odr odr;
468     Z_APDU *f_apdu = odr.create_searchResponse(apdu_req, 0, 0);
469     Z_SearchResponse *f_resp = f_apdu->u.searchResponse;
470
471     *f_resp->resultCount = result_set_size;
472     if (z_records_diag)
473     {
474         // search error
475         f_resp->records = z_records_diag;
476         package.response() = f_apdu;
477         return;
478     }
479     // assume OK
480     m_sets[resultSet.m_setname] = resultSet;
481
482     int number;
483     yp2::util::piggyback(smallSetUpperBound,
484                          largeSetLowerBound,
485                          mediumSetPresentNumber,
486                          result_set_size,
487                          number);
488     Package pp(package.session(), package.origin());
489     if (number > 0)
490     {
491         pp.copy_filter(package);
492         Z_APDU *p_apdu = zget_APDU(odr, Z_APDU_presentRequest);
493         Z_PresentRequest *p_req = p_apdu->u.presentRequest;
494         p_req->preferredRecordSyntax = req->preferredRecordSyntax;
495         p_req->resultSetId = req->resultSetName;
496         *p_req->resultSetStartPoint = 1;
497         *p_req->numberOfRecordsRequested = number;
498         pp.request() = p_apdu;
499         present(pp, p_apdu);
500         
501         if (pp.session().is_closed())
502             package.session().close();
503         
504         Z_GDU *gdu = pp.response().get();
505         if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
506             Z_APDU_presentResponse)
507         {
508             Z_PresentResponse *p_res = gdu->u.z3950->u.presentResponse;
509             f_resp->records = p_res->records;
510             *f_resp->numberOfRecordsReturned = 
511                 *p_res->numberOfRecordsReturned;
512             *f_resp->nextResultSetPosition = 
513                 *p_res->nextResultSetPosition;
514         }
515         else 
516         {
517             package.response() = pp.response(); 
518             return;
519         }
520     }
521     package.response() = f_apdu; // in this scope because of p
522 }
523
524 void yf::Multi::Frontend::present(Package &package, Z_APDU *apdu_req)
525 {
526     // create present request 
527     Z_PresentRequest *req = apdu_req->u.presentRequest;
528
529     Sets_it it;
530     it = m_sets.find(std::string(req->resultSetId));
531     if (it == m_sets.end())
532     {
533         yp2::odr odr;
534         Z_APDU *apdu = 
535             odr.create_presentResponse(
536                 apdu_req,
537                 YAZ_BIB1_SPECIFIED_RESULT_SET_DOES_NOT_EXIST,
538                 req->resultSetId);
539         package.response() = apdu;
540         return;
541     }
542     std::list<Multi::FrontendSet::PresentJob> jobs;
543     int start = *req->resultSetStartPoint;
544     int number = *req->numberOfRecordsRequested;
545     it->second.round_robin(start, number, jobs);
546
547     std::list<BackendPtr> present_backend_list;
548
549     std::list<BackendSet>::const_iterator bsit;
550     bsit = it->second.m_backend_sets.begin();
551     for (; bsit != it->second.m_backend_sets.end(); bsit++)
552     {
553         std::list<Multi::FrontendSet::PresentJob>::const_iterator jit;
554         int start = -1;
555         int end = -1;
556         
557         for (jit = jobs.begin(); jit != jobs.end(); jit++)
558         {
559             if (jit->m_backend == bsit->m_backend)
560             {
561                 if (start == -1 || jit->m_pos < start)
562                     start = jit->m_pos;
563                 if (end == -1 || jit->m_pos > end)
564                     end = jit->m_pos;
565             }
566         }
567         if (start != -1)
568         {
569             PackagePtr p = bsit->m_backend->m_package;
570
571             *req->resultSetStartPoint = start;
572             *req->numberOfRecordsRequested = end - start + 1;
573             
574             p->request() = apdu_req;
575             p->copy_filter(package);
576
577             present_backend_list.push_back(bsit->m_backend);
578         }
579     }
580     multi_move(present_backend_list);
581
582     // look at each response
583     Z_Records *z_records_diag = 0;
584
585     std::list<BackendPtr>::const_iterator pbit = present_backend_list.begin();
586     for (; pbit != present_backend_list.end(); pbit++)
587     {
588         PackagePtr p = (*pbit)->m_package;
589         
590         if (p->session().is_closed()) // if any backend closes, close frontend
591             package.session().close();
592         
593         Z_GDU *gdu = p->response().get();
594         if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
595             Z_APDU_presentResponse)
596         {
597             Z_APDU *b_apdu = gdu->u.z3950;
598             Z_PresentResponse *b_resp = b_apdu->u.presentResponse;
599          
600             // see we get any errors (AKA diagnstics)
601             if (b_resp->records)
602             {
603                 if (b_resp->records->which != Z_Records_DBOSD)
604                     z_records_diag = b_resp->records;
605                 // we may set this multiple times (TOO BAD!)
606             }
607         }
608         else
609         {
610             // if any target does not return present response - return that 
611             package.response() = p->response();
612             return;
613         }
614     }
615
616     yp2::odr odr;
617     Z_APDU *f_apdu = odr.create_presentResponse(apdu_req, 0, 0);
618     Z_PresentResponse *f_resp = f_apdu->u.presentResponse;
619
620     if (z_records_diag)
621     {
622         f_resp->records = z_records_diag;
623         *f_resp->presentStatus = Z_PresentStatus_failure;
624     }
625     else
626     {
627         f_resp->records = (Z_Records *) odr_malloc(odr, sizeof(Z_Records));
628         Z_Records * records = f_resp->records;
629         records->which = Z_Records_DBOSD;
630         records->u.databaseOrSurDiagnostics =
631             (Z_NamePlusRecordList *)
632             odr_malloc(odr, sizeof(Z_NamePlusRecordList));
633         Z_NamePlusRecordList *nprl = records->u.databaseOrSurDiagnostics;
634         nprl->num_records = jobs.size();
635         nprl->records = (Z_NamePlusRecord**)
636             odr_malloc(odr, sizeof(Z_NamePlusRecord *) * nprl->num_records);
637         int i = 0;
638         std::list<Multi::FrontendSet::PresentJob>::const_iterator jit;
639         for (jit = jobs.begin(); jit != jobs.end(); jit++)
640         {
641             PackagePtr p = jit->m_backend->m_package;
642             
643             Z_GDU *gdu = p->response().get();
644             Z_APDU *b_apdu = gdu->u.z3950;
645             Z_PresentResponse *b_resp = b_apdu->u.presentResponse;
646
647             nprl->records[i++] =
648                 b_resp->records->u.databaseOrSurDiagnostics->
649                 records[jit->m_inside_pos];
650         }
651         *f_resp->nextResultSetPosition = start + i;
652         *f_resp->numberOfRecordsReturned = i;
653     }
654     package.response() = f_apdu;
655 }
656
657 void yf::Multi::process(Package &package) const
658 {
659     FrontendPtr f = m_p->get_frontend(package);
660
661     Z_GDU *gdu = package.request().get();
662     
663     if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
664         Z_APDU_initRequest && !f->m_is_multi)
665     {
666         f->init(package, gdu);
667     }
668     else if (!f->m_is_multi)
669         package.move();
670     else if (gdu && gdu->which == Z_GDU_Z3950)
671     {
672         Z_APDU *apdu = gdu->u.z3950;
673         if (apdu->which == Z_APDU_initRequest)
674         {
675             yp2::odr odr;
676             
677             package.response() = odr.create_close(
678                 apdu,
679                 Z_Close_protocolError,
680                 "double init");
681             
682             package.session().close();
683         }
684         else if (apdu->which == Z_APDU_searchRequest)
685         {
686             f->search(package, apdu);
687         }
688         else if (apdu->which == Z_APDU_presentRequest)
689         {
690             f->present(package, apdu);
691         }
692         else
693         {
694             yp2::odr odr;
695             
696             package.response() = odr.create_close(
697                 apdu, Z_Close_protocolError,
698                 "unsupported APDU in filter multi");
699             
700             package.session().close();
701         }
702     }
703     m_p->release_frontend(package);
704 }
705
706 void yp2::filter::Multi::configure(const xmlNode * ptr)
707 {
708     for (ptr = ptr->children; ptr; ptr = ptr->next)
709     {
710         if (ptr->type != XML_ELEMENT_NODE)
711             continue;
712         if (!strcmp((const char *) ptr->name, "virtual"))
713         {
714             std::list<std::string> targets;
715             std::string vhost;
716             xmlNode *v_node = ptr->children;
717             for (; v_node; v_node = v_node->next)
718             {
719                 if (v_node->type != XML_ELEMENT_NODE)
720                     continue;
721                 
722                 if (yp2::xml::is_element_yp2(v_node, "vhost"))
723                     vhost = yp2::xml::get_text(v_node);
724                 else if (yp2::xml::is_element_yp2(v_node, "target"))
725                     targets.push_back(yp2::xml::get_text(v_node));
726                 else
727                     throw yp2::filter::FilterException
728                         ("Bad element " 
729                          + std::string((const char *) v_node->name)
730                          + " in virtual section"
731                             );
732             }
733             std::string route = yp2::xml::get_route(ptr);
734             add_map_host2hosts(vhost, targets, route);
735             std::list<std::string>::const_iterator it;
736             for (it = targets.begin(); it != targets.end(); it++)
737             {
738                 std::cout << "Add " << vhost << "->" << *it
739                           << "," << route << "\n";
740             }
741         }
742         else
743         {
744             throw yp2::filter::FilterException
745                 ("Bad element " 
746                  + std::string((const char *) ptr->name)
747                  + " in virt_db filter");
748         }
749     }
750 }
751
752 static yp2::filter::Base* filter_creator()
753 {
754     return new yp2::filter::Multi;
755 }
756
757 extern "C" {
758     struct yp2_filter_struct yp2_filter_multi = {
759         0,
760         "multi",
761         filter_creator
762     };
763 }
764
765
766 /*
767  * Local variables:
768  * c-basic-offset: 4
769  * indent-tabs-mode: nil
770  * c-file-style: "stroustrup"
771  * End:
772  * vim: shiftwidth=4 tabstop=8 expandtab
773  */