Fixed bug #567: Fix up database name in Name-Plus-Record.
[metaproxy-moved-to-github.git] / src / filter_virt_db.cpp
1 /* $Id: filter_virt_db.cpp,v 1.37 2006-04-29 08:09:13 adam Exp $
2    Copyright (c) 2005-2006, 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/mutex.hpp>
13 #include <boost/thread/condition.hpp>
14 #include <boost/shared_ptr.hpp>
15
16 #include "util.hpp"
17 #include "filter_virt_db.hpp"
18
19 #include <yaz/zgdu.h>
20 #include <yaz/otherinfo.h>
21 #include <yaz/diagbib1.h>
22
23 #include <map>
24 #include <iostream>
25
26 namespace mp = metaproxy_1;
27 namespace yf = mp::filter;
28
29 namespace metaproxy_1 {
30     namespace filter {
31
32         struct Virt_db::Set {
33             Set(BackendPtr b, std::string setname);
34             Set();
35             ~Set();
36
37             BackendPtr m_backend;
38             std::string m_setname;
39         };
40         struct Virt_db::Map {
41             Map(std::list<std::string> targets, std::string route);
42             Map();
43             std::list<std::string> m_targets;
44             std::string m_route;
45         };
46         struct Virt_db::Backend {
47             mp::Session m_backend_session;
48             std::list<std::string> m_frontend_databases;
49             std::list<std::string> m_targets;
50             std::string m_route;
51             bool m_named_result_sets;
52             int m_number_of_sets;
53         };
54         struct Virt_db::Frontend {
55             Frontend(Rep *rep);
56             ~Frontend();
57             mp::Session m_session;
58             bool m_is_virtual;
59             bool m_in_use;
60             yazpp_1::GDU m_init_gdu;
61             std::list<BackendPtr> m_backend_list;
62             std::map<std::string,Virt_db::Set> m_sets;
63
64             void search(Package &package, Z_APDU *apdu);
65             void present(Package &package, Z_APDU *apdu);
66             void scan(Package &package, Z_APDU *apdu);
67
68             void close(Package &package);
69             typedef std::map<std::string,Virt_db::Set>::iterator Sets_it;
70
71             void fixup_npr(Package &p, BackendPtr b);
72
73             void fixup_npr(Z_Records *records, std::string database,
74                            ODR odr);
75
76             BackendPtr lookup_backend_from_databases(
77                 std::list<std::string> databases);
78             BackendPtr create_backend_from_databases(
79                 std::list<std::string> databases,
80                 int &error_code,
81                 std::string &failing_database);
82             
83             BackendPtr init_backend(std::list<std::string> database,
84                                     Package &package,
85                                     int &error_code, std::string &addinfo);
86             Rep *m_p;
87         };            
88         class Virt_db::Rep {
89             friend class Virt_db;
90             friend struct Frontend;
91             
92             FrontendPtr get_frontend(Package &package);
93             void release_frontend(Package &package);
94         private:
95             boost::mutex m_sessions_mutex;
96             std::map<std::string, Virt_db::Map>m_maps;
97
98             typedef std::map<std::string,Virt_db::Set>::iterator Sets_it;
99
100             boost::mutex m_mutex;
101             boost::condition m_cond_session_ready;
102             std::map<mp::Session, FrontendPtr> m_clients;
103         };
104     }
105 }
106
107 using namespace mp;
108
109 yf::Virt_db::BackendPtr yf::Virt_db::Frontend::lookup_backend_from_databases(
110     std::list<std::string> databases)
111 {
112     std::list<BackendPtr>::const_iterator map_it;
113     map_it = m_backend_list.begin();
114     for (; map_it != m_backend_list.end(); map_it++)
115         if ((*map_it)->m_frontend_databases == databases)
116             return *map_it;
117     BackendPtr null;
118     return null;
119 }
120
121 yf::Virt_db::BackendPtr yf::Virt_db::Frontend::create_backend_from_databases(
122     std::list<std::string> databases, int &error_code, std::string &addinfo)
123 {
124     BackendPtr b(new Backend);
125     std::list<std::string>::const_iterator db_it = databases.begin();
126
127     b->m_number_of_sets = 0;
128     b->m_frontend_databases = databases;
129     b->m_named_result_sets = false;
130
131     bool first_route = true;
132
133     std::map<std::string,bool> targets_dedup;
134     for (; db_it != databases.end(); db_it++)
135     {
136         std::map<std::string, Virt_db::Map>::iterator map_it;
137         map_it = m_p->m_maps.find(*db_it);
138         if (map_it == m_p->m_maps.end())  // database not found
139         {
140             error_code = YAZ_BIB1_DATABASE_UNAVAILABLE;
141             addinfo = *db_it;
142             BackendPtr ptr;
143             return ptr;
144         }
145         std::list<std::string>::const_iterator t_it =
146             map_it->second.m_targets.begin();
147         for (; t_it != map_it->second.m_targets.end(); t_it++)
148             targets_dedup[*t_it] = true;
149
150         // see if we have a route conflict.
151         if (!first_route && b->m_route != map_it->second.m_route)
152         {
153             // we have a conflict.. 
154             error_code =  YAZ_BIB1_COMBI_OF_SPECIFIED_DATABASES_UNSUPP;
155             BackendPtr ptr;
156             return ptr;
157         }
158         b->m_route = map_it->second.m_route;
159         first_route = false;
160     }
161     std::map<std::string,bool>::const_iterator tm_it = targets_dedup.begin();
162     for (; tm_it != targets_dedup.end(); tm_it++)
163         b->m_targets.push_back(tm_it->first);
164
165     return b;
166 }
167
168 yf::Virt_db::BackendPtr yf::Virt_db::Frontend::init_backend(
169     std::list<std::string> databases, Package &package,
170     int &error_code, std::string &addinfo)
171 {
172     BackendPtr b = create_backend_from_databases(databases, error_code,
173                                                  addinfo);
174     if (!b)
175         return b;
176     Package init_package(b->m_backend_session, package.origin());
177     init_package.copy_filter(package);
178
179     mp::odr odr;
180
181     Z_APDU *init_apdu = zget_APDU(odr, Z_APDU_initRequest);
182
183     mp::util::set_vhost_otherinfo(&init_apdu->u.initRequest->otherInfo, odr,
184                                    b->m_targets);
185     Z_InitRequest *req = init_apdu->u.initRequest;
186
187     // copy stuff from Frontend Init Request
188     Z_GDU *org_gdu = m_init_gdu.get();
189     Z_InitRequest *org_init = org_gdu->u.z3950->u.initRequest;
190
191     req->idAuthentication = org_init->idAuthentication;
192     req->implementationId = org_init->implementationId;
193     req->implementationName = org_init->implementationName;
194     req->implementationVersion = org_init->implementationVersion;
195
196     ODR_MASK_SET(req->options, Z_Options_search);
197     ODR_MASK_SET(req->options, Z_Options_present);
198     ODR_MASK_SET(req->options, Z_Options_namedResultSets);
199     ODR_MASK_SET(req->options, Z_Options_scan);
200
201     ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_1);
202     ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_2);
203     ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_3);
204
205     init_package.request() = init_apdu;
206     
207     init_package.move(b->m_route);  // sending init 
208
209     Z_GDU *gdu = init_package.response().get();
210     // we hope to get an init response
211     if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
212         Z_APDU_initResponse)
213     {
214         Z_InitResponse *res = gdu->u.z3950->u.initResponse;
215         if (ODR_MASK_GET(res->options, Z_Options_namedResultSets))
216         {
217             b->m_named_result_sets = true;
218         }
219         if (!*res->result)
220         {
221             mp::util::get_init_diagnostics(res, error_code, addinfo);
222             BackendPtr null;
223             return null; 
224         }
225     }
226     else
227     {
228         error_code = YAZ_BIB1_DATABASE_UNAVAILABLE;
229         // addinfo = database;
230         BackendPtr null;
231         return null;
232     }        
233     if (init_package.session().is_closed())
234     {
235         error_code = YAZ_BIB1_DATABASE_UNAVAILABLE;
236         // addinfo = database;
237         BackendPtr null;
238         return null;
239     }
240
241     m_backend_list.push_back(b);
242     return b;
243 }
244
245 void yf::Virt_db::Frontend::search(Package &package, Z_APDU *apdu_req)
246 {
247     Z_SearchRequest *req = apdu_req->u.searchRequest;
248     std::string vhost;
249     std::string resultSetId = req->resultSetName;
250     mp::odr odr;
251
252     std::list<std::string> databases;
253     int i;
254     for (i = 0; i<req->num_databaseNames; i++)
255         databases.push_back(req->databaseNames[i]);
256
257     BackendPtr b; // null for now
258     Sets_it sets_it = m_sets.find(req->resultSetName);
259     if (sets_it != m_sets.end())
260     {
261         // result set already exist 
262         // if replace indicator is off: we return diagnostic if
263         // result set already exist.
264         if (*req->replaceIndicator == 0)
265         {
266             Z_APDU *apdu = 
267                 odr.create_searchResponse(
268                     apdu_req,
269                     YAZ_BIB1_RESULT_SET_EXISTS_AND_REPLACE_INDICATOR_OFF,
270                     0);
271             package.response() = apdu;
272             
273             return;
274         } 
275         sets_it->second.m_backend->m_number_of_sets--;
276
277         // pick up any existing backend with a database match
278         std::list<BackendPtr>::const_iterator map_it;
279         map_it = m_backend_list.begin();
280         for (; map_it != m_backend_list.end(); map_it++)
281         {
282             BackendPtr tmp = *map_it;
283             if (tmp->m_frontend_databases == databases)
284                 break;
285         }
286         if (map_it != m_backend_list.end()) 
287             b = *map_it;
288     }
289     else
290     {
291         // new result set.
292
293         // pick up any existing database with named result sets ..
294         // or one which has no result sets.. yet.
295         std::list<BackendPtr>::const_iterator map_it;
296         map_it = m_backend_list.begin();
297         for (; map_it != m_backend_list.end(); map_it++)
298         {
299             BackendPtr tmp = *map_it;
300             if (tmp->m_frontend_databases == databases &&
301                 (tmp->m_named_result_sets ||
302                  tmp->m_number_of_sets == 0))
303                 break;
304         }
305         if (map_it != m_backend_list.end()) 
306             b = *map_it;
307     }
308     if (!b)  // no backend yet. Must create a new one
309     {
310         int error_code;
311         std::string addinfo;
312         b = init_backend(databases, package, error_code, addinfo);
313         if (!b)
314         {
315             // did not get a backend (unavailable somehow?)
316             
317             Z_APDU *apdu = 
318                 odr.create_searchResponse(
319                     apdu_req, error_code, addinfo.c_str());
320             package.response() = apdu;
321             return;
322         }
323     }
324     m_sets.erase(req->resultSetName);
325     // sending search to backend
326     Package search_package(b->m_backend_session, package.origin());
327
328     search_package.copy_filter(package);
329
330     std::string backend_setname;
331     if (b->m_named_result_sets)
332     {
333         backend_setname = std::string(req->resultSetName);
334     }
335     else
336     {
337         backend_setname = "default";
338         req->resultSetName = odr_strdup(odr, backend_setname.c_str());
339     }
340
341     // pick first targets spec and move the databases from it ..
342     std::list<std::string>::const_iterator t_it = b->m_targets.begin();
343     if (t_it != b->m_targets.end())
344     {
345         mp::util::set_databases_from_zurl(odr, *t_it,
346                                                 &req->num_databaseNames,
347                                                 &req->databaseNames);
348     }
349
350     *req->replaceIndicator = 1;
351
352     search_package.request() = yazpp_1::GDU(apdu_req);
353     
354     search_package.move(b->m_route);
355
356     if (search_package.session().is_closed())
357     {
358         package.response() = search_package.response();
359         package.session().close();
360         return;
361     }
362     b->m_number_of_sets++;
363
364     m_sets[resultSetId] = Virt_db::Set(b, backend_setname);
365     fixup_npr(search_package, b);
366     package.response() = search_package.response();
367 }
368
369 yf::Virt_db::Frontend::Frontend(Rep *rep)
370 {
371     m_p = rep;
372     m_is_virtual = false;
373 }
374
375 void yf::Virt_db::Frontend::close(Package &package)
376 {
377     std::list<BackendPtr>::const_iterator b_it;
378     
379     for (b_it = m_backend_list.begin(); b_it != m_backend_list.end(); b_it++)
380     {
381         (*b_it)->m_backend_session.close();
382         Package close_package((*b_it)->m_backend_session, package.origin());
383         close_package.copy_filter(package);
384         close_package.move((*b_it)->m_route);
385     }
386     m_backend_list.clear();
387 }
388
389 yf::Virt_db::Frontend::~Frontend()
390 {
391 }
392
393 yf::Virt_db::FrontendPtr yf::Virt_db::Rep::get_frontend(Package &package)
394 {
395     boost::mutex::scoped_lock lock(m_mutex);
396
397     std::map<mp::Session,yf::Virt_db::FrontendPtr>::iterator it;
398     
399     while(true)
400     {
401         it = m_clients.find(package.session());
402         if (it == m_clients.end())
403             break;
404         
405         if (!it->second->m_in_use)
406         {
407             it->second->m_in_use = true;
408             return it->second;
409         }
410         m_cond_session_ready.wait(lock);
411     }
412     FrontendPtr f(new Frontend(this));
413     m_clients[package.session()] = f;
414     f->m_in_use = true;
415     return f;
416 }
417
418 void yf::Virt_db::Rep::release_frontend(Package &package)
419 {
420     boost::mutex::scoped_lock lock(m_mutex);
421     std::map<mp::Session,yf::Virt_db::FrontendPtr>::iterator it;
422     
423     it = m_clients.find(package.session());
424     if (it != m_clients.end())
425     {
426         if (package.session().is_closed())
427         {
428             it->second->close(package);
429             m_clients.erase(it);
430         }
431         else
432         {
433             it->second->m_in_use = false;
434         }
435         m_cond_session_ready.notify_all();
436     }
437 }
438
439 yf::Virt_db::Set::Set(BackendPtr b, std::string setname)
440     :  m_backend(b), m_setname(setname)
441 {
442 }
443
444
445 yf::Virt_db::Set::Set()
446 {
447 }
448
449
450 yf::Virt_db::Set::~Set()
451 {
452 }
453
454 yf::Virt_db::Map::Map(std::list<std::string> targets, std::string route)
455     : m_targets(targets), m_route(route) 
456 {
457 }
458
459 yf::Virt_db::Map::Map()
460 {
461 }
462
463 yf::Virt_db::Virt_db() : m_p(new Virt_db::Rep)
464 {
465 }
466
467 yf::Virt_db::~Virt_db() {
468 }
469
470
471 void yf::Virt_db::Frontend::fixup_npr(Z_Records *records, std::string database,
472                                       ODR odr)
473 {
474     if (records && records->which == Z_Records_DBOSD)
475     {
476         Z_NamePlusRecordList *nprlist = records->u.databaseOrSurDiagnostics;
477         int i;
478         for (i = 0; i < nprlist->num_records; i++)
479         {
480             Z_NamePlusRecord *npr = nprlist->records[i];
481             npr->databaseName = odr_strdup(odr, database.c_str());
482         }
483     }
484 }
485
486 void yf::Virt_db::Frontend::fixup_npr(Package &p, BackendPtr b)
487 {
488     Z_GDU *gdu = p.response().get();
489     mp::odr odr;
490     std::string database = "dummy";
491     std::list<std::string>::const_iterator db_it =
492         b->m_frontend_databases.begin();
493     if (db_it != b->m_frontend_databases.end())
494         database = *db_it;
495
496     if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
497         Z_APDU_presentResponse)
498     {
499         fixup_npr(gdu->u.z3950->u.presentResponse->records, database, odr);
500         p.response() = gdu;
501     }
502     if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
503         Z_APDU_searchResponse)
504     {
505         fixup_npr(gdu->u.z3950->u.searchResponse->records, database, odr);
506         p.response() = gdu;
507     }
508
509 }
510
511 void yf::Virt_db::Frontend::present(Package &package, Z_APDU *apdu_req)
512 {
513     Z_PresentRequest *req = apdu_req->u.presentRequest;
514     std::string resultSetId = req->resultSetId;
515     mp::odr odr;
516
517     Sets_it sets_it = m_sets.find(resultSetId);
518     if (sets_it == m_sets.end())
519     {
520         Z_APDU *apdu = 
521             odr.create_presentResponse(
522                 apdu_req,
523                 YAZ_BIB1_SPECIFIED_RESULT_SET_DOES_NOT_EXIST,
524                 resultSetId.c_str());
525         package.response() = apdu;
526         return;
527     }
528     Session *id =
529         new mp::Session(sets_it->second.m_backend->m_backend_session);
530     
531     // sending present to backend
532     Package present_package(*id, package.origin());
533     present_package.copy_filter(package);
534
535     req->resultSetId = odr_strdup(odr, sets_it->second.m_setname.c_str());
536     
537     present_package.request() = yazpp_1::GDU(apdu_req);
538
539     present_package.move(sets_it->second.m_backend->m_route);
540
541     fixup_npr(present_package, sets_it->second.m_backend);
542
543     if (present_package.session().is_closed())
544     {
545         package.response() = present_package.response();
546         package.session().close();
547         return;
548     }
549     else
550     {
551         package.response() = present_package.response();
552     }
553     delete id;
554 }
555
556 void yf::Virt_db::Frontend::scan(Package &package, Z_APDU *apdu_req)
557 {
558     Z_ScanRequest *req = apdu_req->u.scanRequest;
559     std::string vhost;
560     mp::odr odr;
561
562     std::list<std::string> databases;
563     int i;
564     for (i = 0; i<req->num_databaseNames; i++)
565         databases.push_back(req->databaseNames[i]);
566
567     BackendPtr b;
568     // pick up any existing backend with a database match
569     std::list<BackendPtr>::const_iterator map_it;
570     map_it = m_backend_list.begin();
571     for (; map_it != m_backend_list.end(); map_it++)
572     {
573         BackendPtr tmp = *map_it;
574         if (tmp->m_frontend_databases == databases)
575             break;
576     }
577     if (map_it != m_backend_list.end()) 
578         b = *map_it;
579     if (!b)  // no backend yet. Must create a new one
580     {
581         int error_code;
582         std::string addinfo;
583         b = init_backend(databases, package, error_code, addinfo);
584         if (!b)
585         {
586             // did not get a backend (unavailable somehow?)
587             Z_APDU *apdu =
588                 odr.create_scanResponse(
589                     apdu_req, error_code, addinfo.c_str());
590             package.response() = apdu;
591             
592             return;
593         }
594     }
595     // sending scan to backend
596     Package scan_package(b->m_backend_session, package.origin());
597
598     scan_package.copy_filter(package);
599
600     // pick first targets spec and move the databases from it ..
601     std::list<std::string>::const_iterator t_it = b->m_targets.begin();
602     if (t_it != b->m_targets.end())
603     {
604         mp::util::set_databases_from_zurl(odr, *t_it,
605                                                 &req->num_databaseNames,
606                                                 &req->databaseNames);
607     }
608     scan_package.request() = yazpp_1::GDU(apdu_req);
609     
610     scan_package.move(b->m_route);
611
612     if (scan_package.session().is_closed())
613     {
614         package.response() = scan_package.response();
615         package.session().close();
616         return;
617     }
618     package.response() = scan_package.response();
619 }
620
621
622 void yf::Virt_db::add_map_db2targets(std::string db, 
623                                      std::list<std::string> targets,
624                                      std::string route)
625 {
626     m_p->m_maps[db] = Virt_db::Map(targets, route);
627 }
628
629
630 void yf::Virt_db::add_map_db2target(std::string db, 
631                                     std::string target,
632                                     std::string route)
633 {
634     std::list<std::string> targets;
635     targets.push_back(target);
636
637     m_p->m_maps[db] = Virt_db::Map(targets, route);
638 }
639
640 void yf::Virt_db::process(Package &package) const
641 {
642     FrontendPtr f = m_p->get_frontend(package);
643
644     Z_GDU *gdu = package.request().get();
645     
646     if (gdu && gdu->which == Z_GDU_Z3950 && gdu->u.z3950->which ==
647         Z_APDU_initRequest && !f->m_is_virtual)
648     {
649         Z_InitRequest *req = gdu->u.z3950->u.initRequest;
650         
651         std::list<std::string> vhosts;
652         mp::util::get_vhost_otherinfo(&req->otherInfo, false, vhosts);
653         if (vhosts.size() == 0)
654         {
655             f->m_init_gdu = gdu;
656             
657             mp::odr odr;
658             Z_APDU *apdu = odr.create_initResponse(gdu->u.z3950, 0, 0);
659             Z_InitResponse *resp = apdu->u.initResponse;
660             
661             int i;
662             static const int masks[] = {
663                 Z_Options_search,
664                 Z_Options_present,
665                 Z_Options_namedResultSets,
666                 Z_Options_scan,
667                 -1 
668             };
669             for (i = 0; masks[i] != -1; i++)
670                 if (ODR_MASK_GET(req->options, masks[i]))
671                     ODR_MASK_SET(resp->options, masks[i]);
672             
673             static const int versions[] = {
674                 Z_ProtocolVersion_1,
675                 Z_ProtocolVersion_2,
676                 Z_ProtocolVersion_3,
677                 -1
678             };
679             for (i = 0; versions[i] != -1; i++)
680                 if (ODR_MASK_GET(req->protocolVersion, versions[i]))
681                     ODR_MASK_SET(resp->protocolVersion, versions[i]);
682                 else
683                     break;
684             
685             package.response() = apdu;
686             f->m_is_virtual = true;
687         }
688         else
689             package.move();
690     }
691     else if (!f->m_is_virtual)
692         package.move();
693     else if (gdu && gdu->which == Z_GDU_Z3950)
694     {
695         Z_APDU *apdu = gdu->u.z3950;
696         if (apdu->which == Z_APDU_initRequest)
697         {
698             mp::odr odr;
699             
700             package.response() = odr.create_close(
701                 apdu,
702                 Z_Close_protocolError,
703                 "double init");
704             
705             package.session().close();
706         }
707         else if (apdu->which == Z_APDU_searchRequest)
708         {
709             f->search(package, apdu);
710         }
711         else if (apdu->which == Z_APDU_presentRequest)
712         {
713             f->present(package, apdu);
714         }
715         else if (apdu->which == Z_APDU_scanRequest)
716         {
717             f->scan(package, apdu);
718         }
719         else
720         {
721             mp::odr odr;
722             
723             package.response() = odr.create_close(
724                 apdu, Z_Close_protocolError,
725                 "unsupported APDU in filter_virt_db");
726             
727             package.session().close();
728         }
729     }
730     m_p->release_frontend(package);
731 }
732
733
734 void mp::filter::Virt_db::configure(const xmlNode * ptr)
735 {
736     for (ptr = ptr->children; ptr; ptr = ptr->next)
737     {
738         if (ptr->type != XML_ELEMENT_NODE)
739             continue;
740         if (!strcmp((const char *) ptr->name, "virtual"))
741         {
742             std::string database;
743             std::list<std::string> targets;
744             xmlNode *v_node = ptr->children;
745             for (; v_node; v_node = v_node->next)
746             {
747                 if (v_node->type != XML_ELEMENT_NODE)
748                     continue;
749                 
750                 if (mp::xml::is_element_yp2(v_node, "database"))
751                     database = mp::xml::get_text(v_node);
752                 else if (mp::xml::is_element_yp2(v_node, "target"))
753                     targets.push_back(mp::xml::get_text(v_node));
754                 else
755                     throw mp::filter::FilterException
756                         ("Bad element " 
757                          + std::string((const char *) v_node->name)
758                          + " in virtual section"
759                             );
760             }
761             std::string route = mp::xml::get_route(ptr);
762             add_map_db2targets(database, targets, route);
763         }
764         else
765         {
766             throw mp::filter::FilterException
767                 ("Bad element " 
768                  + std::string((const char *) ptr->name)
769                  + " in virt_db filter");
770         }
771     }
772 }
773
774 static mp::filter::Base* filter_creator()
775 {
776     return new mp::filter::Virt_db;
777 }
778
779 extern "C" {
780     struct metaproxy_1_filter_struct metaproxy_1_filter_virt_db = {
781         0,
782         "virt_db",
783         filter_creator
784     };
785 }
786
787
788 /*
789  * Local variables:
790  * c-basic-offset: 4
791  * indent-tabs-mode: nil
792  * c-file-style: "stroustrup"
793  * End:
794  * vim: shiftwidth=4 tabstop=8 expandtab
795  */