source: main/trunk/greenstone2/runtime-src/src/colservr/collectserver.cpp@ 31903

Last change on this file since 31903 was 31903, checked in by ak19, 7 years ago

I hope these are all the changes necessary on the runtime side of GS2 to get the OAI server validation working for GS2: instead of working out the earliest datetime stamp of the OAI repository by comparing the builddate in index/build.cfg of each OAI collection and selecting the earliest, the oai-inf.db is now storing the special earliesttimestamp record. The timestamp of this record represents its collection's earliest timestamp. And the earliest of these among all OAI collections is now the earliest datetime of the OAI repository.

  • Property svn:keywords set to Author Date Id Revision
File size: 22.3 KB
Line 
1
2/**********************************************************************
3 *
4 * collectserver.cpp --
5 * Copyright (C) 1999 The New Zealand Digital Library Project
6 *
7 * A component of the Greenstone digital library software
8 * from the New Zealand Digital Library Project at the
9 * University of Waikato, New Zealand.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24 *
25 *********************************************************************/
26
27#include "collectserver.h"
28#include "OIDtools.h"
29#include <assert.h>
30#include "display.h"
31
32void check_if_valid_buildtype(const text_t& buildtype)
33{
34 if (buildtype=="mg") {
35#ifndef ENABLE_MG
36 cerr << "Warning: Greenstone installation has not been compiled to support buildtype 'mg'." << endl;
37#endif
38 }
39
40 else if (buildtype=="mgpp") {
41#ifndef ENABLE_MGPP
42 cerr << "Warning: Greenstone installation has not been compiled to support buildtype 'mgpp'." << endl;
43#endif
44 }
45
46 else if (buildtype=="lucene") {
47#ifndef ENABLE_LUCENE
48 cerr << "Warning: Greenstone installation has not been compiled to support buildtype 'lucene'." << endl;
49#endif
50 }
51
52 else {
53 cerr << "Error: buildtype '" << buildtype << "' is not a recognized indexer for Greenstone." << endl;
54 }
55
56}
57
58
59void check_if_valid_infodbtype(const text_t& infodbtype)
60{
61 if (infodbtype=="gdbm") {
62#ifndef USE_GDBM
63 cerr << "Warning: Greenstone installation has not been compiled to support infodbtype 'gdbm'." << endl;
64#endif
65 }
66 else if (infodbtype=="gdbm-txtgz") {
67#ifndef USE_GDBM
68 cerr << "Warning: Greenstone installation has not been compiled to support infodbtype 'gdbm-txtgz'." << endl;
69#endif
70 }
71 else if (infodbtype=="jdbm") {
72#ifndef USE_JDBM
73 cerr << "Warning: Greenstone installation has not been compiled to support infodbtype 'jdbm'." << endl;
74#endif
75 }
76 else if (infodbtype=="sqlite") {
77#ifndef USE_SQLITE
78 cerr << "Warning: Greenstone installation has not been compiled to support infodbtype 'sqlite'." << endl;
79#endif
80 }
81 else if (infodbtype=="mssql") {
82#ifndef USE_MSSQL
83 cerr << "Warning: Greenstone installation has not been compiled to support infodbtype 'mssql'." << endl;
84#endif
85 }
86
87 else {
88 cerr << "Error: infodbtype '" << infodbtype << "' is not a recognized database type for Greenstone." << endl;
89 }
90
91}
92
93
94
95collectserver::collectserver ()
96 : collectinfo()
97{
98 configinfo.collection = "null";
99}
100
101collectserver::~collectserver () {
102
103 // clean up the sources
104 sourcelistclass::iterator source_here = sources.begin();
105 sourcelistclass::iterator source_end = sources.end();
106 while (source_here != source_end) {
107 if ((*source_here).s != NULL)
108 delete (*source_here).s;
109 ++source_here;
110 }
111 sources.clear();
112
113 // clean up the filters
114 filtermapclass::iterator filter_here = filters.begin();
115 filtermapclass::iterator filter_end = filters.end();
116 while (filter_here != filter_end) {
117 if ((*filter_here).second.f != NULL)
118 delete (*filter_here).second.f;
119 ++filter_here;
120 }
121 filters.clear();
122}
123
124// configure should be called for each line in the
125// configuration files to configure the collection server and everything
126// it contains. The configuration should take place just before initialisation
127void collectserver::configure (const text_t &key, const text_tarray &cfgline) {
128 if (cfgline.size() >= 1) {
129 const text_t &value = cfgline[0];
130 if (key == "plugin")
131 {
132 //get the plugin name
133 const text_t &name = cfgline[0];
134
135 if (name == "HTMLPlugin" || name== "PDFPlugin")
136 {
137 for (int hI = 1; hI < cfgline.size(); hI++)
138 {
139 const text_t &plugOption = cfgline[hI];
140
141 if (plugOption == "-use_realistic_book")
142 {
143 collectinfo.useBook = true;
144 break;
145 }
146 }
147 }
148 }
149 else if (key == "gsdlhome") configinfo.gsdlhome = value;
150 else if (key == "gdbmhome") configinfo.dbhome = value;
151 else if (key == "collecthome") configinfo.collecthome = value;
152 else if (key == "collection") {
153 configinfo.collection = value;
154 collectinfo.shortInfo.name = value;
155 }
156 else if (key == "collectdir") configinfo.collectdir = value;
157 else if (key == "host") collectinfo.shortInfo.host = value;
158 else if (key == "port") collectinfo.shortInfo.port = value.getint();
159 else if (key == "public") {
160 if (value == "true") collectinfo.isPublic = true;
161 else collectinfo.isPublic = false;
162 } else if (key == "beta") {
163 if (value == "true") collectinfo.isBeta = true;
164 else collectinfo.isBeta = false;
165 } else if (key == "collectgroup") {
166 if (value == "true") collectinfo.isCollectGroup = true;
167 else collectinfo.isCollectGroup = false;
168 } else if ((key == "ccscols") || (key == "supercollection")) collectinfo.ccsCols = cfgline;
169 else if (key == "supercollectionoptions") {
170 text_tarray::const_iterator begin = cfgline.begin();
171 text_tarray::const_iterator end = cfgline.end();
172 while(begin != end) {
173
174 if (*begin == "uniform_search_results_formatting") {
175 collectinfo.ccsOptions |= CCSUniformSearchResultsFormatting;
176 }
177 begin++;
178 }
179 }
180 else if (key == "builddate") collectinfo.buildDate = value.getint();
181 else if (key == "languages") collectinfo.languages = cfgline;
182 else if (key == "numdocs") collectinfo.numDocs = value.getint();
183 else if (key == "numsections") collectinfo.numSections = value.getint();
184 else if (key == "numwords") collectinfo.numWords = value.getint();
185 else if (key == "numbytes") collectinfo.numBytes = value.getint();
186 else if (key == "stemindexes") collectinfo.stemIndexes = value.getint();
187 else if (key == "collectionmeta") {
188 // genuine collmeta get added as collectionmeta and collection_macros
189 // .collmeta just get added as collection_macros
190 text_t params;
191 if (cfgline.size() == 3) {
192 // get the params for later
193 text_t::const_iterator first=cfgline[1].begin()+1;
194 text_t::const_iterator last=cfgline[1].end()-1;
195 params=substr(first, last);
196 }
197
198 text_t meta_name = cfgline[0];
199 if (*(meta_name.begin())=='.') {
200 // a .xxx collectionmeta. strip off the . and
201 // look it up in the indexmap to get the actual value
202
203 text_t name = substr(cfgline[0].begin()+1,cfgline[0].end());
204 text_t new_name;
205
206 // Now that GLI has been fixed to deal with ex. prefixes, and modelcol's collect.cfg does not contain
207 // Greenstone ex.* meta in the "collectionmeta" section, we won't encounter ex.* in collectionmeta here.
208 // So we should not remove any "ex." prefixes here, since collectionmeta does not contain ex.* but it can
209 // contain ex.dc.* type metadata, which will need to have their ex. prefix preserved for matching below.
210
211 if (indexmap.from2to(name, new_name)) {
212 meta_name = new_name;
213 }
214 } else {
215 // add them to collectionmeta
216 text_tmap lang_map = collectinfo.collectionmeta[cfgline[0]];
217 if (cfgline.size() == 2) {
218 lang_map[g_EmptyText] = cfgline[1];
219 } else if (cfgline.size() == 3 ) {
220 // get the lang out of params
221 paramhashtype params_hash;
222 splitparams(params, params_hash);
223
224 text_t lang = params_hash["l"];
225 lang_map[lang] = cfgline[2];
226 if (lang_map[g_EmptyText].empty()) {
227 // want the first one as the default if no default specified
228 lang_map[g_EmptyText] = cfgline[2];
229 }
230 }
231 collectinfo.collectionmeta[cfgline[0]] = lang_map;
232
233 }
234
235 // add all collectionmeta to macro list
236 text_tmap params_map = collectinfo.collection_macros[meta_name];
237
238 if (cfgline.size() == 2) {// no params for this macro
239 params_map[g_EmptyText] = cfgline[1];
240 }
241 else if (cfgline.size() == 3) {// has params
242 params_map[params] = cfgline[2];
243 if (params_map[g_EmptyText].empty()) {
244 params_map[g_EmptyText] = cfgline[2];
245 }
246 }
247 collectinfo.collection_macros[meta_name] = params_map;
248 }
249 else if (key == "collectionmacro") {
250 text_t nobrackets;
251 text_tmap params_map = collectinfo.collection_macros[cfgline[0]];
252 // add all to macro list
253 if (cfgline.size() == 2) { // no params for this macro
254 params_map[g_EmptyText] = cfgline[1];
255 }
256 else if (cfgline.size() == 3) {// has params
257 // strip [ ] brackets from params
258 text_t::const_iterator first=cfgline[1].begin()+1;
259 text_t::const_iterator last=cfgline[1].end()-1;
260 nobrackets=substr(first, last);
261 params_map[nobrackets] = cfgline[2];
262 }
263 collectinfo.collection_macros[cfgline[0]] = params_map;
264
265 } else if (key == "format" && cfgline.size() == 2)
266 collectinfo.format[cfgline[0]] = cfgline[1];
267 else if (key == "building" && cfgline.size() == 2)
268 collectinfo.building[cfgline[0]] = cfgline[1];
269 else if (key == "httpdomain") collectinfo.httpdomain = value;
270 else if (key == "httpprefix") collectinfo.httpprefix = value;
271 else if (key == "receptionist") collectinfo.receptionist = value;
272 else if (key == "buildtype") {
273 check_if_valid_buildtype(value); // prints warning if value (indexer) is invalid
274 collectinfo.buildType = value;
275 }
276 // backwards compatibility - searchytpes is now a format statement
277 else if (key == "searchtype") { // means buildtype is mgpp
278 if (collectinfo.buildType.empty()) {
279 check_if_valid_buildtype("mgpp"); // prints warning if value (indexer) is invalid
280 collectinfo.buildType = "mgpp";
281 }
282 joinchar(cfgline, ',', collectinfo.format["SearchTypes"]);
283 //collectinfo.searchTypes = cfgline;
284 }
285 else if (key == "infodbtype") {
286 check_if_valid_infodbtype(value); // prints warning if value (database type) is invalid
287 collectinfo.infodbType = value;
288 }
289 else if (key == "separate_cjk") {
290 if (value == "true") collectinfo.isSegmented = true;
291 else collectinfo.isSegmented = false;
292 }
293 // What have we set in our collect.cfg file : document or collection ?
294 else if (key == "authenticate") collectinfo.authenticate = value;
295
296 // What have we set for our group list
297 else if ((key == "auth_group") || (key == "auth_groups")) joinchar(cfgline,',',collectinfo.auth_group);
298
299 // build.cfg, earliestDatestamp of this collection needed for
300 // OAIServer to work out earliestDatestamp of this repository
301 else if (key == "earliestdatestamp") {
302 collectinfo.earliestDatestamp = cfgline[0]; // get it from build.cfg
303 }
304
305 // store all the mappings for use when collection meta is read later
306 // (build.cfg read before collect.cfg)
307 else if (key == "indexmap" || key == "indexfieldmap" || key == "subcollectionmap" || key == "languagemap" || key == "levelmap") {
308 indexmap.importmap (cfgline, true);
309
310 }
311 // In the map the key-value pair contain the same
312 // data i.e key == data, if key is 2 then data is 2
313
314 // What have we set for our public_documents ACL
315 else if (key == "public_documents")
316 {
317 text_tarray::const_iterator begin = cfgline.begin();
318 text_tarray::const_iterator end = cfgline.end();
319 while(begin != end)
320 {
321 // key = data i.e if key is 2 then data is 2
322 // collectinfo.public_documents[*begin] is the key
323 // *begin is the data value
324
325 collectinfo.public_documents[*begin] = *begin;
326 ++begin;
327 }
328 }
329
330 // What have we set for our private_documents ACL
331 else if (key == "private_documents")
332 {
333 text_tarray::const_iterator begin = cfgline.begin();
334 text_tarray::const_iterator end = cfgline.end();
335 while(begin != end)
336 {
337 // key = data i.e if key is 2 then data is 2
338 // collectinfo.public_documents[*begin] is the key
339 // *begin is the data value
340
341 collectinfo.private_documents[*begin] = *begin;
342 ++begin;
343 }
344 }
345
346 // dynamic_classifier <UniqueID> "<Options>"
347 else if (key == "dynamic_classifier")
348 {
349 collectinfo.dynamic_classifiers[cfgline[0]] = cfgline[1];
350 }
351 }
352
353 // configure the filters
354 filtermapclass::iterator filter_here = filters.begin();
355 filtermapclass::iterator filter_end = filters.end();
356 while (filter_here != filter_end) {
357 assert ((*filter_here).second.f != NULL);
358 if ((*filter_here).second.f != NULL)
359 (*filter_here).second.f->configure(key, cfgline);
360
361 ++filter_here;
362 }
363
364 // configure the sources
365 sourcelistclass::iterator source_here = sources.begin();
366 sourcelistclass::iterator source_end = sources.end();
367 while (source_here != source_end) {
368 assert ((*source_here).s != NULL);
369 if ((*source_here).s != NULL)
370 (*source_here).s->configure(key, cfgline);
371
372 ++source_here;
373 }
374}
375
376
377void collectserver::configure (const text_t &key, const text_t &value) {
378 text_tarray cfgline;
379 cfgline.push_back (value);
380 configure(key, cfgline);
381}
382
383void collectserver::ping (bool &wasSuccess, comerror_t &error, ostream &logout) {
384 // if we've not been properly configured, then it is a foregone
385 // conclusion that we cannot be active
386 if (this->configinfo.collection == "null")
387 {
388 wasSuccess = false;
389 }
390 // if no build date exists, then the collection was probably not built;
391 // ditto if the number of documents is zero, then something is pretty
392 // wrong
393 else if (this->collectinfo.buildDate == 0 ||
394 this->collectinfo.numDocs == 0)
395 {
396 wasSuccess = false;
397 }
398 // it is probably okay
399 else
400 wasSuccess = true;
401}
402
403
404bool collectserver::init (ostream &logout) {
405 // delete the indexmap
406 indexmap.clear();
407
408 // init the filters
409 filtermapclass::iterator filter_here = filters.begin();
410 filtermapclass::iterator filter_end = filters.end();
411 while (filter_here != filter_end) {
412 assert ((*filter_here).second.f != NULL);
413 if (((*filter_here).second.f != NULL) &&
414 !(*filter_here).second.f->init(logout)) return false;
415
416 ++filter_here;
417 }
418
419 // init the sources
420 sourcelistclass::iterator source_here = sources.begin();
421 sourcelistclass::iterator source_end = sources.end();
422 while (source_here != source_end) {
423 assert ((*source_here).s != NULL);
424 if (((*source_here).s != NULL) &&
425 !(*source_here).s->init(logout)) return false;
426
427 ++source_here;
428 }
429
430 return true;
431}
432
433
434void collectserver::get_collectinfo (ColInfoResponse_t &reponse,
435 comerror_t &err, ostream &/*logout*/) {
436 reponse = collectinfo;
437 err = noError;
438}
439
440void collectserver::get_filterinfo (InfoFiltersResponse_t &response,
441 comerror_t &err, ostream &/*logout*/) {
442 response.clear ();
443
444 // get a list of filter names
445 filtermapclass::iterator filter_here = filters.begin();
446 filtermapclass::iterator filter_end = filters.end();
447 while (filter_here != filter_end) {
448 response.filterNames.insert ((*filter_here).first);
449 ++filter_here;
450 }
451
452 err = noError;
453}
454
455void collectserver::get_filteroptions (const InfoFilterOptionsRequest_t &request,
456 InfoFilterOptionsResponse_t &response,
457 comerror_t &err, ostream &logout) {
458 outconvertclass text_t2ascii;
459
460 filterclass *thisfilter = filters.getfilter(request.filterName);
461 if (thisfilter != NULL) {
462 thisfilter->get_filteroptions (response, err, logout);
463 } else {
464 response.clear ();
465 err = protocolError;
466 text_t& infodbtype = collectinfo.infodbType;
467
468 // Don't print out the warning if were's asking about SQLQueryFilter
469 // when we know the infodbtype is something other than .*sql.*
470
471 if ((request.filterName != "SQLQueryFilter")
472 || (findword(infodbtype.begin(),infodbtype.end(),"sql") != infodbtype.end())) {
473 logout << text_t2ascii << "Protocol Error: filter options requested for non-existent\n"
474 << "filter \"" << request.filterName << "\".\n\n";
475 }
476 }
477}
478
479void collectserver::filter (FilterRequest_t &request,
480 FilterResponse_t &response,
481 comerror_t &err, ostream &logout) {
482 outconvertclass text_t2ascii;
483
484 // translate any ".fc", ".pr" etc. stuff in the docSet
485 text_t translatedOID;
486 text_tarray translatedOIDs;
487 text_tarray::iterator doc_here = request.docSet.begin();
488 text_tarray::iterator doc_end = request.docSet.end();
489 while (doc_here != doc_end) {
490 if (needs_translating (*doc_here)) {
491 sourcelistclass::iterator source_here = sources.begin();
492 sourcelistclass::iterator source_end = sources.end();
493 while (source_here != source_end) {
494 assert ((*source_here).s != NULL);
495 if (((*source_here).s != NULL) &&
496 ((*source_here).s->translate_OID (*doc_here, translatedOID, err, logout))) {
497 if (err != noError) return;
498 break;
499 }
500 ++source_here;
501 }
502 translatedOIDs.push_back (translatedOID);
503 } else {
504 translatedOIDs.push_back (*doc_here);
505 }
506 ++doc_here;
507 }
508 request.docSet = translatedOIDs;
509
510 response.clear();
511
512 filterclass *thisfilter = filters.getfilter(request.filterName);
513 if (thisfilter != NULL) {
514 // filter the data
515 thisfilter->filter (request, response, err, logout);
516 if (err != noError) return;
517
518 // fill in the metadata for each of the OIDs (if it is requested)
519 if (request.filterResultOptions & FRmetadata) {
520
521 bool processed = false;
522 ResultDocInfo_tarray::iterator resultdoc_here = response.docInfo.begin();
523 ResultDocInfo_tarray::iterator resultdoc_end = response.docInfo.end();
524 while (resultdoc_here != resultdoc_end) {
525
526 text_t deleted_status = "";
527 bool append_metadata = (request.filterResultOptions & FROAI) ? true : false;
528
529 // try each of the sources in turn
530 sourcelistclass::iterator source_here = sources.begin();
531 sourcelistclass::iterator source_end = sources.end();
532 while (source_here != source_end) {
533 assert ((*source_here).s != NULL);
534
535 // first check for oai metadata from the oai_db, if asked for it (if FROAI is set)
536 if(((*source_here).s != NULL) &&
537 (request.filterResultOptions & FROAI) &&
538 ((*source_here).s->get_oai_metadata(request.requestParams, request.refParams,
539 request.getParents, request.fields,
540 (*resultdoc_here).OID, deleted_status, (*resultdoc_here).metadata,
541 err, logout))) {
542
543 if (err != noError) return;
544
545 processed = true;
546 }
547
548 // if OID is the special OAI specific OAI_EARLIESTTIMESTAMP_OID,
549 // then we'd have got its OAI meta above if we were requested to do so.
550 // Either way, we won't be additionally getting regular meta for this OID,
551 // as it's not a real doc OID, so we stop processing this OID here.
552 if((*resultdoc_here).OID == OAI_EARLIESTTIMESTAMP_OID) {
553 ++source_here;
554 if(processed == true) break;
555 else continue;
556 }
557
558 // We may or may not have got oai_meta (depends on if FROAI was set).
559 // If we didn't get oai_meta, then deleted_status would still be "".
560 // If we did get oai_meta, and if the deleted_status for the OID was D for deleted entry,
561 // don't bother getting any other metadata, as there will be no entry for that OID in index db.
562
563 // Note that if we did get oai_meta and OID marked as existing, we're in append_mode:
564 // don't let get_metadata() clear the metadata list, as there's already stuff in there
565 //if(deleted_status == "E") append_metadata = true;
566
567 if (((*source_here).s != NULL) &&
568 deleted_status != "D" &&
569 ((*source_here).s->get_metadata(request.requestParams, request.refParams,
570 request.getParents, request.fields,
571 (*resultdoc_here).OID, (*resultdoc_here).metadata,
572 err, logout, append_metadata))) {
573 if (err != noError) return; // check for errors again
574
575 processed = processed || true; // processed would not have been set yet if not doing FROAI. Set now.
576 // OR-ing isn't necessary, but indicates some consideration of both get oai meta & get meta success
577 }
578
579 if(processed) break;
580
581 ++source_here;
582 }
583 if (!processed) {
584
585 logout << text_t2ascii << "Protocol Error: nothing processed for "
586 << "filter \"" << request.filterName << "\".\n\n";
587
588 err = protocolError;
589 return;
590 }
591 ++resultdoc_here;
592 }
593 }
594
595 err = noError;
596 }
597 else
598 {
599 response.clear ();
600 err = protocolError;
601 logout << text_t2ascii << "Protocol Error: filter options requested for non-existent\n"
602 << "filter \"" << request.filterName << "\".\n\n";
603 }
604}
605
606void collectserver::get_document (const DocumentRequest_t &request,
607 DocumentResponse_t &response,
608 comerror_t &err, ostream &logout) {
609
610 sourcelistclass::iterator source_here = sources.begin();
611 sourcelistclass::iterator source_end = sources.end();
612 while (source_here != source_end) {
613 assert ((*source_here).s != NULL);
614 if (((*source_here).s != NULL) &&
615 ((*source_here).s->get_document (request.OID, response.doc, err, logout))) {
616 if (err != noError) return;
617 break;
618 }
619 ++source_here;
620 }
621}
622
623void collectserver::is_searchable (bool &issearchable, comerror_t &err,
624 ostream &logout) {
625
626 sourcelistclass::iterator source_here = sources.begin();
627 sourcelistclass::iterator source_end = sources.end();
628 while (source_here != source_end) {
629 assert ((*source_here).s != NULL);
630 if (((*source_here).s != NULL) &&
631 ((*source_here).s->is_searchable (issearchable, err, logout))) {
632 if (err != noError) return;
633 break;
634 }
635 ++source_here;
636 }
637}
638
639
640bool operator==(const collectserverptr &x, const collectserverptr &y) {
641 return (x.c == y.c);
642}
643
644bool operator<(const collectserverptr &x, const collectserverptr &y) {
645 return (x.c < y.c);
646}
647
648
649// thecollectserver remains the property of the calling code but
650// should not be deleted until it is removed from this list.
651void collectservermapclass::addcollectserver (collectserver *thecollectserver) {
652 // can't add a null collection server
653 assert (thecollectserver != NULL);
654 if (thecollectserver == NULL) return;
655
656 // can't add an collection server with no collection name
657 assert (!(thecollectserver->get_collection_name()).empty());
658 if ((thecollectserver->get_collection_name()).empty()) return;
659
660 collectserverptr cptr;
661 cptr.c = thecollectserver;
662 collectserverptrs[thecollectserver->get_collection_name()] = cptr;
663}
664
665// getcollectserver will return NULL if the collectserver could not be found
666collectserver *collectservermapclass::getcollectserver (const text_t &collection) {
667 // can't find a collection with no name
668 if (collection.empty()) return NULL;
669
670 iterator here = collectserverptrs.find (collection);
671 if (here == collectserverptrs.end()) return NULL;
672
673 return (*here).second.c;
674}
Note: See TracBrowser for help on using the repository browser.