source: gs3-extensions/solr/trunk/src/src/java/org/greenstone/gsdl3/service/GS2SolrSearch.java@ 32430

Last change on this file since 32430 was 32430, checked in by ak19, 6 years ago

solr should only be accessible locally (from localhost, specifically 127.0.0.1) which means over http. This conflicted with the previous design of the properties file for working with http and/or https. Now we have tomcat.port.https and localhost.port.http, both always set. In place of server.protocol that used to contain the default protocol, we now have server.protocols which can be set to a comma separated list of one or both of http and https. Drastic restructuring followed. I think I've tested all but https certification stuff.

  • Property svn:executable set to *
File size: 20.8 KB
Line 
1/*
2 * GS2SolrSearch.java
3 * Copyright (C) 2006 New Zealand Digital Library, http://www.nzdl.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19package org.greenstone.gsdl3.service;
20
21import java.io.File;
22import java.io.IOException;
23// Greenstone classes
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Map;
29import java.util.Properties;
30import java.util.Set;
31import java.util.Vector;
32
33import org.apache.log4j.Logger;
34import org.apache.solr.client.solrj.SolrServer;
35import org.apache.solr.client.solrj.SolrServerException;
36import org.apache.solr.client.solrj.impl.HttpSolrServer;
37import org.apache.solr.client.solrj.impl.HttpSolrServer.RemoteSolrException;
38import org.apache.solr.client.solrj.request.CoreAdminRequest;
39import org.apache.solr.client.solrj.response.CoreAdminResponse;
40import org.apache.solr.client.solrj.response.FacetField;
41import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction;
42import org.apache.solr.common.util.NamedList;
43import org.greenstone.LuceneWrapper4.SharedSoleneQueryResult;
44import org.greenstone.gsdl3.util.FacetWrapper;
45import org.greenstone.gsdl3.util.GSFile;
46import org.greenstone.gsdl3.util.GSXML;
47import org.greenstone.gsdl3.util.SolrFacetWrapper;
48import org.greenstone.gsdl3.util.SolrQueryResult;
49import org.greenstone.gsdl3.util.SolrQueryWrapper;
50import org.greenstone.util.GlobalProperties;
51import org.greenstone.util.ProtocolPortProperties;
52import org.w3c.dom.Document;
53import org.w3c.dom.Element;
54import org.w3c.dom.NodeList;
55
56import org.apache.solr.client.solrj.impl.HttpSolrServer.RemoteSolrException;
57import org.apache.solr.client.solrj.request.CoreAdminRequest;
58import org.apache.solr.client.solrj.response.CoreAdminResponse;
59import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction;
60import org.apache.solr.common.util.NamedList;
61
62public class GS2SolrSearch extends SharedSoleneGS2FieldSearch
63{
64
65 protected static final String SORT_ORDER_PARAM = "sortOrder";
66 protected static final String SORT_ORDER_DESCENDING = "1";
67 protected static final String SORT_ORDER_ASCENDING = "0";
68
69 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.service.GS2SolrSearch.class.getName());
70
71 protected String solr_servlet_base_url;
72 protected HashMap<String, SolrServer> solr_core_cache;
73 protected SolrQueryWrapper solr_src = null;
74
75 protected ArrayList<String> _facets = new ArrayList<String>();
76 protected HashMap<String, Element> _facet_display_names = new HashMap<String, Element>();
77
78 public GS2SolrSearch()
79 {
80 paramDefaults.put(SORT_ORDER_PARAM, SORT_ORDER_DESCENDING);
81 does_faceting = true;
82 does_highlight_snippets = true;
83 does_full_field_highlighting = true;
84 // Used to store the solr cores that match the required 'level'
85 // of search (e.g. either document-level=>didx, or
86 // section-level=>sidx. The hashmap is filled out on demand
87 // based on 'level' parameter passed in to 'setUpQueryer()'
88
89 solr_core_cache = new HashMap<String, SolrServer>();
90
91 this.solr_src = new SolrQueryWrapper();
92
93 // Create the solr servlet url on GS3's tomcat. By default it's "http://localhost:8383/solr"
94 // Don't do this in configure(), since the tomcat url will remain unchanged while tomcat is running
95 try {
96 Properties globalProperties = new Properties();
97 globalProperties.load(Class.forName("org.greenstone.util.GlobalProperties").getClassLoader().getResourceAsStream("global.properties"));
98
99 /*
100 String host = globalProperties.getProperty("tomcat.server", "localhost");
101 //String port = globalProperties.getProperty("tomcat.port.http", "8383");
102 //String protocol = globalProperties.getProperty("server.protocol", "http");
103 ProtocolPortProperties protocolPortProps = new ProtocolPortProperties(globalProperties); // can throw Exception
104
105 String protocol = protocolPortProps.getProtocol();
106 String port = protocolPortProps.getPort();
107 String solrContext = globalProperties.getProperty("solr.context", "solr");
108
109 String portStr = port.equals("80") ? "" : ":"+port;
110 solr_servlet_base_url = protocol+"://"+host+portStr+"/"+solrContext;
111 */
112
113 // The solr servlet is only accessible locally (from "localhost", specifically 127.0.0.1).
114 // for security reasons, as we don't want non librarian users
115 // to go to the solr servlet and delete solr cores or something.
116 // The security Valve element in the tomcat solr.xml context file restricts
117 // access to 127.0.0.1, but here we ensure that the solr URL is the local http one
118 // and not any https with domain name and https port.
119 // Note that we use 127.0.0.1 instead of literally "localhost" since localhost is unsafe
120 ProtocolPortProperties protocolPortProps = new ProtocolPortProperties(globalProperties); // can throw Exception
121 String solrContext = globalProperties.getProperty("solr.context", "solr");
122 solr_servlet_base_url = protocolPortProps.getLocalHttpBaseAddress()+"/"+solrContext;
123
124 } catch(Exception e) {
125 logger.error("Error reading greenstone's tomcat solr server properties from global.properties", e);
126 }
127 }
128
129 /** configure this service */
130 public boolean configure(Element info, Element extra_info)
131 {
132 boolean success = super.configure(info, extra_info);
133
134 // clear the map of solr cores for this collection added to the map upon querying
135 solr_core_cache.clear();
136
137 if(!success) {
138 return false;
139 }
140
141 if(solr_servlet_base_url == null) {
142 logger.error("Unable to configure GS2SolrSearch - solr_servlet_base_url is null because of issues with port/protocol in global.properties");
143 return false;
144 }
145
146 // Setting up facets
147
148 // the search element from collectionConfig
149 Element searchElem = (Element) GSXML.getChildByTagName(extra_info, GSXML.SEARCH_ELEM);
150
151 Document owner = info.getOwnerDocument();
152 // for each facet in buildConfig
153 NodeList facet_list = info.getElementsByTagName("facet");
154 for (int i=0; i<facet_list.getLength(); i++) {
155 Element facet = (Element)facet_list.item(i);
156 String shortname = facet.getAttribute(GSXML.SHORTNAME_ATT);
157 _facets.add(shortname);
158
159 // now add any displayItems into the facet element
160 // (which is stored as part of info), then we can add to
161 // the result if needed
162 String longname = facet.getAttribute(GSXML.NAME_ATT);
163 Element config_facet = GSXML.getNamedElement(searchElem, "facet", GSXML.NAME_ATT, longname);
164 if (config_facet != null) {
165 NodeList display_items = config_facet.getElementsByTagName(GSXML.DISPLAY_TEXT_ELEM);
166 for (int j=0; j<display_items.getLength(); j++) {
167 Element e = (Element) display_items.item(j);
168 facet.appendChild(owner.importNode(e, true));
169 }
170 _facet_display_names.put(shortname, facet);
171
172 }
173
174 }
175
176 //If use Solr check if cores loaded
177 if (!loadSolrCores()) {
178 logger.error("Collection: couldn't configure collection: " + this.cluster_name + ", "
179 + "Couldn't activate Solr cores");
180 return false;
181 }
182 // NodeList configIndexElems = searchElem.getElementsByTagName(GSXML.INDEX_ELEM);
183
184 // ArrayList<String> chosenFacets = new ArrayList<String>();
185 // for (int i = 0; i < configIndexElems.getLength(); i++)
186 // {
187 // Element current = (Element) configIndexElems.item(i);
188 // if (current.getAttribute(GSXML.FACET_ATT).equals("true"))
189 // {
190 // chosenFacets.add(current.getAttribute(GSXML.NAME_ATT));
191 // }
192 // }
193
194 // Element indexListElem = (Element) GSXML.getChildByTagName(info, GSXML.INDEX_ELEM + GSXML.LIST_MODIFIER);
195 // NodeList buildIndexElems = indexListElem.getElementsByTagName(GSXML.INDEX_ELEM);
196
197 // for (int j = 0; j < buildIndexElems.getLength(); j++)
198 // {
199 // Element current = (Element) buildIndexElems.item(j);
200 // for (int i = 0; i < chosenFacets.size(); i++)
201 // {
202 // if (current.getAttribute(GSXML.NAME_ATT).equals(chosenFacets.get(i)))
203 // {
204 // _facets.add(current.getAttribute(GSXML.SHORTNAME_ATT));
205 // }
206 // }
207 // }
208
209 return true;
210 }
211
212 public void cleanUp()
213 {
214 super.cleanUp();
215 this.solr_src.cleanUp();
216
217 // clear the map keeping track of the SolrServers in this collection
218 solr_core_cache.clear();
219 }
220
221 /** add in the SOLR specific params to TextQuery */
222 protected void addCustomQueryParams(Element param_list, String lang)
223 {
224 super.addCustomQueryParams(param_list, lang);
225 /** Add in the sort order asc/desc param */
226 createParameter(SORT_ORDER_PARAM, param_list, lang);
227 }
228 /** add in SOLR specific params for AdvancedFieldQuery */
229 protected void addCustomQueryParamsAdvField(Element param_list, String lang)
230 {
231 super.addCustomQueryParamsAdvField(param_list, lang);
232 createParameter(SORT_ORDER_PARAM, param_list, lang);
233
234 }
235 /** create a param and add to the list */
236 protected void createParameter(String name, Element param_list, String lang)
237 {
238 Document doc = param_list.getOwnerDocument();
239 Element param = null;
240 String param_default = paramDefaults.get(name);
241 if (name.equals(SORT_ORDER_PARAM)) {
242 String[] vals = { SORT_ORDER_ASCENDING, SORT_ORDER_DESCENDING };
243 String[] vals_texts = { getTextString("param." + SORT_ORDER_PARAM + "." + SORT_ORDER_ASCENDING, lang), getTextString("param." + SORT_ORDER_PARAM + "." + SORT_ORDER_DESCENDING, lang) };
244
245 param = GSXML.createParameterDescription(doc, SORT_ORDER_PARAM, getTextString("param." + SORT_ORDER_PARAM, lang), GSXML.PARAM_TYPE_ENUM_SINGLE, param_default, vals, vals_texts);
246 }
247
248 if (param != null)
249 {
250 param_list.appendChild(param);
251 }
252 else
253 {
254 super.createParameter(name, param_list, lang);
255 }
256
257 }
258
259 /** methods to handle actually doing the query */
260
261 /** do any initialisation of the query object */
262 protected boolean setUpQueryer(HashMap params)
263 {
264 this.solr_src.clearFacets();
265 this.solr_src.clearFacetQueries();
266
267 for (int i = 0; i < _facets.size(); i++)
268 {
269 this.solr_src.addFacet(_facets.get(i));
270 }
271
272 String index = "didx";
273 if (this.default_level.toUpperCase().equals("SEC")) {
274 index = "sidx";
275 }
276 String physical_index_language_name = null;
277 String physical_sub_index_name = null;
278 String docFilter = null;
279 int maxdocs = 100;
280 int hits_per_page = 20;
281 int start_page = 1;
282 // set up the query params
283 Set entries = params.entrySet();
284 Iterator i = entries.iterator();
285 while (i.hasNext())
286 {
287 Map.Entry m = (Map.Entry) i.next();
288 String name = (String) m.getKey();
289 String value = (String) m.getValue();
290
291 ///System.err.println("### GS2SolrSearch.java: name " + name + " - value " + value);
292
293 if (name.equals(MAXDOCS_PARAM) && !value.equals(""))
294 {
295 maxdocs = Integer.parseInt(value);
296 }
297 else if (name.equals(HITS_PER_PAGE_PARAM))
298 {
299 hits_per_page = Integer.parseInt(value);
300 }
301 else if (name.equals(START_PAGE_PARAM))
302 {
303 start_page = Integer.parseInt(value);
304 }
305 else if (name.equals(MATCH_PARAM))
306 {
307 if (value.equals(MATCH_PARAM_ALL))
308 {
309 this.solr_src.setDefaultConjunctionOperator("AND");
310 }
311 else
312 {
313 this.solr_src.setDefaultConjunctionOperator("OR");
314 }
315 }
316 else if (name.equals(RANK_PARAM))
317 {
318 if (value.equals(RANK_PARAM_RANK))
319 {
320 value = SolrQueryWrapper.SORT_BY_RANK;
321 } else if (value.equals(RANK_PARAM_NONE)) {
322 value = SolrQueryWrapper.SORT_BY_INDEX_ORDER;
323 }
324
325 this.solr_src.setSortField(value);
326 }
327 else if (name.equals(SORT_ORDER_PARAM)) {
328 if (value.equals(SORT_ORDER_DESCENDING)) {
329 this.solr_src.setSortOrder(SolrQueryWrapper.SORT_DESCENDING);
330 } else {
331 this.solr_src.setSortOrder(SolrQueryWrapper.SORT_ASCENDING);
332 }
333 }
334 else if (name.equals(LEVEL_PARAM))
335 {
336 if (value.toUpperCase().equals("SEC"))
337 {
338 index = "sidx";
339 }
340 else
341 {
342 index = "didx";
343 }
344 }
345 // Would facets ever come in through params???
346 else if (name.equals("facets") && value.length() > 0)
347 {
348 String[] facets = value.split(",");
349
350 for (String facet : facets)
351 {
352 this.solr_src.addFacet(facet);
353 }
354 }
355 else if (name.equals("facetQueries") && value.length() > 0)
356 {
357 //logger.info("@@@ SOLR FACET VALUE FOUND: " + value);
358 this.solr_src.addFacetQuery(value);
359 }
360 else if (name.equals(INDEX_SUBCOLLECTION_PARAM))
361 {
362 physical_sub_index_name = value;
363 }
364 else if (name.equals(INDEX_LANGUAGE_PARAM))
365 {
366 physical_index_language_name = value;
367 } // ignore any others
368 else if (name.equals("docFilter"))
369 {
370 docFilter = value;
371 docFilter = docFilter.replaceAll("[^A-Za-z0-9.]", "");
372 this.solr_src.setDocFilter(value);
373 }
374 }
375 // set up start and end results if necessary
376 int start_results = 0;
377 if (start_page != 1)
378 {
379 start_results = ((start_page - 1) * hits_per_page) ;
380 }
381 int end_results = hits_per_page * start_page;
382 this.solr_src.setStartResults(start_results);
383 this.solr_src.setEndResults(end_results);
384 this.solr_src.setMaxDocs(maxdocs);
385
386 if (index.equals("sidx") || index.equals("didx"))
387 {
388 if (physical_sub_index_name != null)
389 {
390 index += physical_sub_index_name;
391 }
392 if (physical_index_language_name != null)
393 {
394 index += physical_index_language_name;
395 }
396 }
397
398 // now we know the index level, we can dig out the required
399 // solr-core, (caching the result in 'solr_core_cache')
400 String core_name = getCollectionCoreNamePrefix() + "-" + index;
401
402 SolrServer solr_core = null;
403 //CHECK HERE
404 if (!solr_core_cache.containsKey(core_name))
405 {
406 solr_core = new HttpSolrServer(this.solr_servlet_base_url+"/"+core_name);
407 solr_core_cache.put(core_name, solr_core);
408 }
409 else
410 {
411 solr_core = solr_core_cache.get(core_name);
412 }
413
414 this.solr_src.setSolrCore(solr_core);
415 this.solr_src.setCollectionCoreNamePrefix(getCollectionCoreNamePrefix());
416 this.solr_src.initialise();
417 return true;
418 }
419
420 /** do the query */
421 protected Object runQuery(String query)
422 {
423 try
424 {
425 //if it is a Highlighting Query - execute it
426 this.solr_src.setHighlightField(indexField);
427 if(hldocOID != null)
428 {
429 String rslt = this.solr_src.runHighlightingQuery(query,hldocOID);
430 // Check result
431 if (rslt != null)
432 {
433 return rslt;
434 }
435 //Highlighting request failed. Do standard request.
436 hldocOID = null;
437 }
438 //logger.info("@@@@ Query is now: " + query);
439 SharedSoleneQueryResult sqr = this.solr_src.runQuery(query);
440
441 return sqr;
442 }
443 catch (Exception e)
444 {
445 logger.error("Exception happened in run query: ", e);
446 }
447
448 return null;
449 }
450
451
452 /** get the total number of docs that match */
453 protected long numDocsMatched(Object query_result)
454 {
455 return ((SharedSoleneQueryResult) query_result).getTotalDocs();
456
457 }
458
459 /** get the list of doc ids */
460 protected String[] getDocIDs(Object query_result)
461 {
462 Vector docs = ((SharedSoleneQueryResult) query_result).getDocs();
463 String[] doc_nums = new String[docs.size()];
464 for (int d = 0; d < docs.size(); d++)
465 {
466 String doc_num = ((SharedSoleneQueryResult.DocInfo) docs.elementAt(d)).id_;
467 doc_nums[d] = doc_num;
468 }
469 return doc_nums;
470 }
471
472 /** get the list of doc ranks */
473 protected String[] getDocRanks(Object query_result)
474 {
475 Vector docs = ((SharedSoleneQueryResult) query_result).getDocs();
476 String[] doc_ranks = new String[docs.size()];
477 for (int d = 0; d < docs.size(); d++)
478 {
479 doc_ranks[d] = Float.toString(((SharedSoleneQueryResult.DocInfo) docs.elementAt(d)).rank_);
480 }
481 return doc_ranks;
482 }
483
484 /** add in term info if available */
485 protected boolean addTermInfo(Element term_list, HashMap params, Object query_result)
486 {
487 Document doc = term_list.getOwnerDocument();
488 String query_level = (String) params.get(LEVEL_PARAM); // the current query level
489
490 Vector terms = ((SharedSoleneQueryResult) query_result).getTerms();
491 for (int t = 0; t < terms.size(); t++)
492 {
493 SharedSoleneQueryResult.TermInfo term_info = (SharedSoleneQueryResult.TermInfo) terms.get(t);
494
495 Element term_elem = doc.createElement(GSXML.TERM_ELEM);
496 term_elem.setAttribute(GSXML.NAME_ATT, term_info.term_);
497 term_elem.setAttribute(FREQ_ATT, "" + term_info.term_freq_);
498 term_elem.setAttribute(NUM_DOCS_MATCH_ATT, "" + term_info.match_docs_);
499 term_elem.setAttribute(FIELD_ATT, term_info.field_);
500 term_list.appendChild(term_elem);
501 }
502
503 Vector stopwords = ((SharedSoleneQueryResult) query_result).getStopWords();
504 for (int t = 0; t < stopwords.size(); t++)
505 {
506 String stopword = (String) stopwords.get(t);
507
508 Element stopword_elem = doc.createElement(GSXML.STOPWORD_ELEM);
509 stopword_elem.setAttribute(GSXML.NAME_ATT, stopword);
510 term_list.appendChild(stopword_elem);
511 }
512
513 return true;
514 }
515
516 protected ArrayList<FacetWrapper> getFacets(Object query_result, String lang)
517 {
518 if (!(query_result instanceof SolrQueryResult))
519 {
520 return null;
521 }
522
523 SolrQueryResult result = (SolrQueryResult) query_result;
524 List<FacetField> facets = result.getFacetResults();
525
526 if (facets == null)
527 {
528 return null;
529 }
530
531 ArrayList<FacetWrapper> newFacetList = new ArrayList<FacetWrapper>();
532
533 for (FacetField facet : facets)
534 {
535 SolrFacetWrapper wrap = new SolrFacetWrapper(facet);
536 String fname = wrap.getName();
537 String dname = getDisplayText(_facet_display_names.get(fname), GSXML.DISPLAY_TEXT_NAME, lang, "en", "metadata_names");
538 wrap.setDisplayName(dname);
539 newFacetList.add(wrap);
540 }
541
542 return newFacetList;
543 }
544 @Override
545 protected Map<String, Map<String, List<String>>> getHighlightSnippets(Object query_result)
546 {
547 if (!(query_result instanceof SolrQueryResult))
548 {
549 return null;
550 }
551
552 SolrQueryResult result = (SolrQueryResult) query_result;
553
554 return result.getHighlightResults();
555 }
556
557
558 protected String getCollectionCoreNamePrefix() {
559 String site_name = this.router.getSiteName();
560 String coll_name = this.cluster_name;
561 String collection_core_name_prefix = site_name + "-" + coll_name;
562 return collection_core_name_prefix;
563 }
564
565 private boolean loadSolrCores() {
566
567 HttpSolrServer solrServer = new HttpSolrServer(solr_servlet_base_url);
568 // Max retries
569 solrServer.setMaxRetries(1);
570 // Connection Timeout
571 solrServer.setConnectionTimeout(3000);
572 //Cores
573 String coreSecName = getCollectionCoreNamePrefix() + "-sidx";
574 String coreDocName = getCollectionCoreNamePrefix() + "-didx";
575
576
577 if (!checkSolrCore(coreSecName, solrServer)){
578 if (!activateSolrCore(coreSecName, solrServer)){
579 logger.error("Couldn't activate Solr core " + coreSecName + " for collection " + cluster_name);
580 return false;
581 }
582 }
583 if (!checkSolrCore(coreDocName, solrServer)){
584 if (!activateSolrCore(coreDocName, solrServer)){
585 logger.error("Couldn't activate Solr core " + coreDocName + " for collection " + cluster_name);
586 return false;
587 }
588 }
589 return true;
590 }
591
592 private boolean checkSolrCore(String coreName, HttpSolrServer solrServer) {
593 CoreAdminRequest adminRequest = new CoreAdminRequest();
594 adminRequest.setAction(CoreAdminAction.STATUS);
595 adminRequest.setCoreName(coreName);
596
597 try {
598 CoreAdminResponse adminResponse = adminRequest.process(solrServer);
599 NamedList<NamedList<Object>> coreStatus = adminResponse.getCoreStatus();
600 NamedList<Object> coreList = coreStatus.getVal(0);
601 if (coreList != null) {
602 if (coreList.get("name") == null) {
603 logger.warn("Solr core " + coreName + " for collection " + cluster_name + " not exists.");
604 return false;
605 }
606 }
607
608 } catch (SolrServerException e) {
609 e.printStackTrace();
610 return false;
611 } catch (IOException e) {
612 e.printStackTrace();
613 return false;
614 } catch (RemoteSolrException e1){
615 logger.error("Check solr core " + coreName + " for collection " + cluster_name + " failed.");
616 e1.printStackTrace();
617 return false;
618 }
619 return true;
620 }
621
622 private boolean activateSolrCore(String coreName, HttpSolrServer solrServer) {
623 String dataDir = GSFile.collectionIndexDir(site_home, cluster_name) + File.separator + coreName.substring(coreName.length() - 4);
624 String instanceDir = GSFile.collectionEtcDir(site_home, cluster_name);
625
626 try {
627 CoreAdminRequest.createCore(coreName, instanceDir, solrServer, "", "", dataDir, "");
628 logger.warn("Solr core " + coreName + " for collection " + cluster_name + " activated.");
629 } catch (SolrServerException e1) {
630 e1.printStackTrace();
631 return false;
632 } catch (IOException e1) {
633 e1.printStackTrace();
634 return false;
635 } catch (RemoteSolrException e1){
636 logger.error("Activation solr core " + coreName + " for collection " + cluster_name + " failed.");
637 e1.printStackTrace();
638 return false;
639 }
640
641 return true;
642 }
643
644}
Note: See TracBrowser for help on using the repository browser.