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