source: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/collection/Collection.java@ 24393

Last change on this file since 24393 was 24393, checked in by sjm84, 13 years ago

Adding in the server-side code for the Document Maker as well as several other enhancements

  • Property svn:keywords set to Author Date Id Revision
File size: 22.7 KB
Line 
1/*
2 * Collection.java
3 * Copyright (C) 2002 New Zealand Digital Library, http://www.nzdl.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 */
19package org.greenstone.gsdl3.collection;
20
21import org.greenstone.gsdl3.util.*;
22import org.greenstone.gsdl3.core.*;
23import org.greenstone.gsdl3.service.*;
24
25// java XML classes we're using
26import org.w3c.dom.Document;
27import org.w3c.dom.Node;
28import org.w3c.dom.Element;
29import org.w3c.dom.NodeList;
30
31import java.io.*;
32import java.io.File;
33import java.util.HashMap;
34import java.util.*;
35
36import javax.xml.parsers.DocumentBuilder;
37import javax.xml.parsers.DocumentBuilderFactory;
38
39import org.xml.sax.*;
40import javax.xml.parsers.SAXParserFactory;
41import javax.xml.parsers.ParserConfigurationException;
42import javax.xml.parsers.SAXParser;
43
44import org.apache.log4j.*;
45
46// Apache Commons
47import org.apache.commons.lang3.*;
48
49/**
50 * Represents a collection in Greenstone. A collection is an extension of a
51 * ServiceCluster - it has local data that the services use.
52 *
53 * @author <a href="mailto:[email protected]">Katherine Don</a>
54 * @see ModuleInterface
55 */
56public class Collection extends ServiceCluster
57{
58
59 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.collection.Collection.class.getName());
60
61 /** is this collection being tidied */
62 protected boolean useBook = false;
63 /** is this collection public or private */
64 protected boolean is_public = true;
65
66 /** does this collection provide the OAI service */
67 protected boolean has_oai = true;
68 /** time when this collection was built */
69 protected long lastmodified = 0;
70 /** earliestDatestamp of this collection. Necessary for OAI */
71 protected long earliestDatestamp = 0;
72
73 /**
74 * An element containing the serviceRackList element of buildConfig.xml,
75 * used to determine whether it contains the OAIPMH serviceRack
76 */
77 //protected Element service_rack_list = null;
78
79 protected XMLTransformer transformer = null;
80
81 /** same as setClusterName */
82 public void setCollectionName(String name)
83 {
84 setClusterName(name);
85 }
86
87 public Collection()
88 {
89 super();
90 this.description = this.doc.createElement(GSXML.COLLECTION_ELEM);
91
92 }
93
94 /**
95 * Configures the collection.
96 *
97 * gsdlHome and collectionName must be set before configure is called.
98 *
99 * the file buildcfg.xml is located in gsdlHome/collect/collectionName
100 * collection metadata is obtained, and services loaded.
101 *
102 * @return true/false on success/fail
103 */
104 public boolean configure()
105 {
106
107 if (this.site_home == null || this.cluster_name == null)
108 {
109 logger.error("Collection: site_home and collection_name must be set before configure called!");
110 return false;
111 }
112
113 Element coll_config_xml = loadCollConfigFile();
114 Element build_config_xml = loadBuildConfigFile();
115
116 if (coll_config_xml == null || build_config_xml == null)
117 {
118 return false;
119 }
120
121 // get the collection type attribute
122 Element search = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.SEARCH_ELEM);
123 if (search != null)
124 {
125 col_type = search.getAttribute(GSXML.TYPE_ATT);
126 }
127
128 Element browse = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.INFODB_ELEM);
129 if (browse != null)
130 {
131 db_type = browse.getAttribute(GSXML.TYPE_ATT);
132 }
133 else
134 {
135 db_type = "gdbm"; //Default database type
136 }
137
138 // process the metadata and display items
139 findAndLoadInfo(coll_config_xml, build_config_xml);
140
141 // now do the services
142 configureServiceRacks(coll_config_xml, build_config_xml);
143
144 return true;
145
146 }
147
148 public boolean useBook()
149 {
150 return useBook;
151 }
152
153 public boolean isPublic()
154 {
155 return is_public;
156 }
157
158 // Not used anymore by the OAIReceptionist to find out the earliest datestamp
159 // amongst all oai collections in the repository. May be useful generally.
160 public long getLastmodified()
161 {
162 return lastmodified;
163 }
164
165 //used by the OAIReceptionist to find out the earliest datestamp amongst all oai collections in the repository
166 public long getEarliestDatestamp()
167 {
168 return earliestDatestamp;
169 }
170
171 /**
172 * whether the service_map in ServiceCluster.java contains the service
173 * 'OAIPMH' 11/06/2007 xiao
174 */
175 public boolean hasOAI()
176 {
177 return has_oai;
178 }
179
180 /**
181 * load in the collection config file into a DOM Element
182 */
183 protected Element loadCollConfigFile()
184 {
185
186 File coll_config_file = new File(GSFile.collectionConfigFile(this.site_home, this.cluster_name));
187
188 if (!coll_config_file.exists())
189 {
190 logger.error("Collection: couldn't configure collection: " + this.cluster_name + ", " + coll_config_file + " does not exist");
191 return null;
192 }
193 // get the xml for both files
194 Document coll_config_doc = this.converter.getDOM(coll_config_file, CONFIG_ENCODING);
195 Element coll_config_elem = null;
196 if (coll_config_doc != null)
197 {
198 coll_config_elem = coll_config_doc.getDocumentElement();
199 }
200 return coll_config_elem;
201
202 }
203
204 /**
205 * load in the collection build config file into a DOM Element
206 */
207 protected Element loadBuildConfigFile()
208 {
209
210 File build_config_file = new File(GSFile.collectionBuildConfigFile(this.site_home, this.cluster_name));
211 if (!build_config_file.exists())
212 {
213 logger.error("Collection: couldn't configure collection: " + this.cluster_name + ", " + build_config_file + " does not exist");
214 return null;
215 }
216 Document build_config_doc = this.converter.getDOM(build_config_file, CONFIG_ENCODING);
217 Element build_config_elem = null;
218 if (build_config_doc != null)
219 {
220 build_config_elem = build_config_doc.getDocumentElement();
221 }
222
223 lastmodified = build_config_file.lastModified();
224
225 return build_config_elem;
226 }
227
228 /**
229 * find the metadata and display elems from the two config files and add it
230 * to the appropriate lists
231 */
232 protected boolean findAndLoadInfo(Element coll_config_xml, Element build_config_xml)
233 {
234
235 // metadata
236 Element meta_list = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER);
237 addMetadata(meta_list);
238 meta_list = (Element) GSXML.getChildByTagName(build_config_xml, GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER);
239 addMetadata(meta_list);
240
241 meta_list = this.doc.createElement(GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER);
242 GSXML.addMetadata(this.doc, meta_list, "httpPath", this.site_http_address + "/collect/" + this.cluster_name);
243 addMetadata(meta_list);
244
245 // display stuff
246 Element display_list = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.DISPLAY_TEXT_ELEM + GSXML.LIST_MODIFIER);
247 if (display_list != null)
248 {
249 resolveMacros(display_list);
250 addDisplayItems(display_list);
251 }
252
253 //check whether the html are tidy or not
254 Element import_list = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.IMPORT_ELEM);
255 if (import_list != null)
256 {
257 Element plugin_list = (Element) GSXML.getChildByTagName(import_list, GSXML.PLUGIN_ELEM + GSXML.LIST_MODIFIER);
258 addPlugins(plugin_list);
259 if (plugin_list != null)
260 {
261 Element plugin_elem = (Element) GSXML.getNamedElement(plugin_list, GSXML.PLUGIN_ELEM, GSXML.NAME_ATT, "HTMLPlugin");
262 if (plugin_elem != null)
263 {
264 //get the option
265 Element option_elem = (Element) GSXML.getNamedElement(plugin_elem, GSXML.PARAM_OPTION_ELEM, GSXML.NAME_ATT, "-use_realistic_book");
266 if (option_elem != null)
267 {
268 useBook = true;
269 }
270 }
271 }
272 }
273 meta_list = this.doc.createElement(GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER);
274 if (useBook == true)
275 GSXML.addMetadata(this.doc, meta_list, "tidyoption", "tidy");
276 else
277 GSXML.addMetadata(this.doc, meta_list, "tidyoption", "untidy");
278 addMetadata(meta_list);
279
280 // check whether we are public or not
281 if (meta_list != null)
282 {
283 Element meta_elem = (Element) GSXML.getNamedElement(metadata_list, GSXML.METADATA_ELEM, GSXML.NAME_ATT, "public");
284 if (meta_elem != null)
285 {
286 String value = GSXML.getValue(meta_elem).toLowerCase().trim();
287 if (value.equals("false"))
288 {
289 is_public = false;
290 }
291 }
292 }
293 return true;
294
295 }
296
297 protected boolean configureServiceRacks(Element coll_config_xml, Element build_config_xml)
298 {
299 clearServices();
300 Element service_list = (Element) GSXML.getChildByTagName(build_config_xml, GSXML.SERVICE_CLASS_ELEM + GSXML.LIST_MODIFIER);
301 configureServiceRackList(service_list, coll_config_xml);
302
303 // collection Config may also contain manually added service racks
304 service_list = (Element) GSXML.getChildByTagName(coll_config_xml, GSXML.SERVICE_CLASS_ELEM + GSXML.LIST_MODIFIER);
305 if (service_list != null)
306 {
307 configureServiceRackList(service_list, build_config_xml);
308
309 // Check for oai
310 Element oai_service_rack = GSXML.getNamedElement(service_list, GSXML.SERVICE_CLASS_ELEM, OAIXML.NAME, OAIXML.OAIPMH);
311 if (oai_service_rack == null)
312 {
313 has_oai = false;
314 logger.info("No oai for collection: " + this.cluster_name);
315
316 }
317 else
318 {
319 has_oai = true;
320
321 // extract earliestDatestamp from the buildconfig.xml for OAI
322 Element metadata_list = (Element) GSXML.getChildByTagName(build_config_xml, GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER);
323
324 if (metadata_list != null)
325 {
326 NodeList children = metadata_list.getElementsByTagName(GSXML.METADATA_ELEM);
327 // can't do getChildNodes(), because whitespace, such as newlines, creates Text nodes
328 for (int i = 0; i < children.getLength(); i++)
329 {
330 Element metadata = (Element) children.item(i);
331 if (metadata.getAttribute(GSXML.NAME_ATT).equals(OAIXML.EARLIEST_DATESTAMP))
332 {
333 String earliestDatestampStr = GSXML.getValue(metadata);
334 if (!earliestDatestampStr.equals(""))
335 {
336 earliestDatestamp = Long.parseLong(earliestDatestampStr);
337 }
338 break; // found a metadata element with name=earliestDatestamp in buildconfig
339 }
340 }
341 }
342
343 // If at the end of this, there is no value for earliestDatestamp, print out a warning
344 logger.warn("No earliestDatestamp in buildConfig.xml for collection: " + this.cluster_name + ". Defaulting to 0.");
345
346 }
347 }
348 else
349 { // no list of services (no ServiceRackList), so no oai_service_rack either
350 // explicitly set has_oai to false here, since it's initialised to true by default
351 has_oai = false;
352 }
353 return true;
354 }
355
356 protected boolean resolveMacros(Element display_list)
357 {
358 if (display_list == null)
359 return false;
360 NodeList displaynodes = display_list.getElementsByTagName(GSXML.DISPLAY_TEXT_ELEM);
361 if (displaynodes.getLength() > 0)
362 {
363 String http_site = this.site_http_address;
364 String http_collection = this.site_http_address + "/collect/" + this.cluster_name;
365 for (int k = 0; k < displaynodes.getLength(); k++)
366 {
367 Element d = (Element) displaynodes.item(k);
368 String text = GSXML.getNodeText(d);
369 text = StringUtils.replace(text, "_httpsite_", http_site);
370 text = StringUtils.replace(text, "_httpcollection_", http_collection);
371 GSXML.setNodeText(d, text);
372 }
373 }
374 return true;
375 }
376
377 /**
378 * do a configure on only part of the collection
379 */
380 protected boolean configureSubset(String subset)
381 {
382
383 // need the coll config files
384 Element coll_config_elem = loadCollConfigFile();
385 Element build_config_elem = loadBuildConfigFile();
386 if (coll_config_elem == null || build_config_elem == null)
387 {
388 // wont be able to do any of the requests
389 return false;
390 }
391
392 if (subset.equals(GSXML.SERVICE_ELEM + GSXML.LIST_MODIFIER))
393 {
394 return configureServiceRacks(coll_config_elem, build_config_elem);
395 }
396
397 if (subset.equals(GSXML.METADATA_ELEM + GSXML.LIST_MODIFIER) || subset.equals(GSXML.DISPLAY_TEXT_ELEM + GSXML.LIST_MODIFIER) || subset.equals(GSXML.PLUGIN_ELEM + GSXML.LIST_MODIFIER))
398 {
399 return findAndLoadInfo(coll_config_elem, build_config_elem);
400
401 }
402
403 logger.error("Collection: cant process system request, configure " + subset);
404 return false;
405 }
406
407 /**
408 * handles requests made to the ServiceCluster itself
409 *
410 * @param req
411 * - the request Element- <request>
412 * @return the result Element - should be <response>
413 */
414 protected Element processMessage(Element request)
415 {
416
417 Element response = this.doc.createElement(GSXML.RESPONSE_ELEM);
418 response.setAttribute(GSXML.FROM_ATT, this.cluster_name);
419 String type = request.getAttribute(GSXML.TYPE_ATT);
420 String lang = request.getAttribute(GSXML.LANG_ATT);
421 response.setAttribute(GSXML.TYPE_ATT, type);
422
423 logger.error("Collection received a message, attempting to process");
424
425 if (type.equals(GSXML.REQUEST_TYPE_FORMAT_STRING))
426 {
427 logger.error("Received format string request");
428
429 String subaction = request.getAttribute("subaction");
430 logger.error("Subaction is " + subaction);
431
432 String service = request.getAttribute("service");
433 logger.error("Service is " + service);
434
435 String classifier = null;
436 if (service.equals("ClassifierBrowse"))
437 {
438 classifier = request.getAttribute("classifier");
439 logger.error("Classifier is " + classifier);
440 }
441
442 //logger.error("Format string: " + format_string);
443 logger.error("Config file location = " + GSFile.collectionConfigFile(this.site_home, this.cluster_name));
444
445 // check for version file
446
447 String directory = new File(GSFile.collectionConfigFile(this.site_home, this.cluster_name)).getParent() + File.separator;
448 logger.error("Directory is " + directory);
449
450 String version_filename = "";
451 if (service.equals("ClassifierBrowse"))
452 version_filename = directory + "browse_" + classifier + "_format_statement_version.txt";
453 else
454 version_filename = directory + "query_format_statement_version.txt";
455
456 File version_file = new File(version_filename);
457 logger.error("Version filename is " + version_filename);
458
459 if (subaction.equals("update"))
460 {
461 Element format_element = (Element) GSXML.getChildByTagName(request, GSXML.FORMAT_STRING_ELEM);
462 //String format_string = GSXML.getNodeText(format_element);
463 Element format_statement = (Element) format_element.getFirstChild();
464
465 String version_number = "1";
466 BufferedWriter writer;
467
468 try
469 {
470
471 if (version_file.exists())
472 {
473 // Read version
474 BufferedReader reader = new BufferedReader(new FileReader(version_filename));
475 version_number = reader.readLine();
476 int aInt = Integer.parseInt(version_number) + 1;
477 version_number = Integer.toString(aInt);
478 reader.close();
479 }
480 else
481 {
482 // Create
483 version_file.createNewFile();
484 writer = new BufferedWriter(new FileWriter(version_filename));
485 writer.write(version_number);
486 writer.close();
487 }
488
489 // Write version file
490 String format_statement_filename = "";
491
492 if (service.equals("ClassifierBrowse"))
493 format_statement_filename = directory + "browse_" + classifier + "_format_statement_v" + version_number + ".txt";
494 else
495 format_statement_filename = directory + "query_format_statement_v" + version_number + ".txt";
496
497 logger.error("Format statement filename is " + format_statement_filename);
498
499 // Write format statement
500 String format_string = this.converter.getString(format_statement); //GSXML.xmlNodeToString(format_statement);
501 writer = new BufferedWriter(new FileWriter(format_statement_filename));
502 writer.write(format_string);
503 writer.close();
504
505 // Update version number
506 writer = new BufferedWriter(new FileWriter(version_filename));
507 writer.write(version_number);
508 writer.close();
509
510 }
511 catch (IOException e)
512 {
513 logger.error("IO Exception " + e);
514 }
515 }
516
517 if (subaction.equals("saveDocument"))
518 {
519 int k;
520 Element format_element = (Element) GSXML.getChildByTagName(request, GSXML.FORMAT_STRING_ELEM);
521 //String format_string = GSXML.getNodeText(format_element);
522 // Get display tag
523 Element display_format = (Element) format_element.getFirstChild();
524
525 logger.error("I have received a save document request");
526 String format_string = GSXML.xmlNodeToString(display_format);
527 logger.error("Param=" + format_string);
528 String collection_config = directory + "collectionConfig.xml";
529 Document config = this.converter.getDOM(new File(collection_config), "UTF-8");
530
531 Node current_node = GSXML.getChildByTagName(config, "CollectionConfig");
532
533 // Get display child
534 if (GSXML.getChildByTagName(current_node, "display") == null)
535 {
536 logger.error("ERROR: does not have a display child");
537 // well then create a format tag
538 Element display_tag = config.createElement("display");
539 current_node = (Node) current_node.appendChild(display_tag);
540 //current_node = (Node) format_tag;
541 }
542
543 else
544 {
545 current_node = GSXML.getChildByTagName(current_node, "display");
546 }
547
548 if (GSXML.getChildByTagName(current_node, "format") == null)
549 {
550 logger.error("ERROR: does not have a format child");
551 // well then create a format tag
552 Element format_tag = config.createElement("format");
553 current_node.appendChild(format_tag);
554 //current_node = (Node) format_tag;
555 }
556
557 current_node.replaceChild(config.importNode(display_format, true), GSXML.getChildByTagName(current_node, "format"));
558
559 logger.error(GSXML.xmlNodeToString(current_node));
560
561 logger.error("Convert config to string");
562 String new_config = this.converter.getString(config);
563
564 new_config = StringUtils.replace(new_config, "&lt;", "<");
565 new_config = StringUtils.replace(new_config, "&gt;", ">");
566 new_config = StringUtils.replace(new_config, "&quot;", "\"");
567
568 try
569 {
570 // Write to file (not original! for now)
571 BufferedWriter writer = new BufferedWriter(new FileWriter(collection_config + ".new"));
572 writer.write(new_config);
573 writer.close();
574 logger.error("All is happy with collection saveDocument");
575 }
576 catch (IOException e)
577 {
578 logger.error("IO Exception " + e);
579 }
580 }
581
582 if (subaction.equals("save"))
583 {
584 logger.error("SAVE format statement");
585
586 Element format_element = (Element) GSXML.getChildByTagName(request, GSXML.FORMAT_STRING_ELEM);
587 //String format_string = GSXML.getNodeText(format_element);
588 Element format_statement = (Element) format_element.getFirstChild();
589
590 try
591 {
592
593 // open collectionConfig.xml and read in to w3 Document
594 String collection_config = directory + "collectionConfig.xml";
595 Document config = this.converter.getDOM(new File(collection_config), "UTF-8");
596
597 //String tag_name = "";
598 int k;
599 int index;
600 Element elem;
601 // Try importing entire tree to this.doc so we can add and remove children at ease
602 //Node current_node = this.doc.importNode(GSXML.getChildByTagName(config, "CollectionConfig"),true);
603 Node current_node = GSXML.getChildByTagName(config, "CollectionConfig");
604 NodeList current_node_list;
605
606 logger.error("Service is " + service);
607
608 if (service.equals("ClassifierBrowse"))
609 {
610 //tag_name = "browse";
611 // if CLX then need to look in <classifier> X then <format>
612 // default is <browse><format>
613
614 logger.error("Looking for browse");
615 current_node = GSXML.getChildByTagName(current_node, "browse");
616
617 // find CLX
618 if (classifier != null)
619 {
620 logger.error("Classifier is not null");
621 logger.error("Classifier is " + classifier);
622 current_node_list = GSXML.getChildrenByTagName(current_node, "classifier");
623 index = Integer.parseInt(classifier.substring(2)) - 1;
624 logger.error("classifier index is " + index);
625 // index should be given by X-1
626 current_node = current_node_list.item(index);
627 // what if classifier does not have a format tag?
628 if (GSXML.getChildByTagName(current_node, "format") == null)
629 {
630 logger.error("ERROR: valid classifier but does not have a format child");
631 // well then create a format tag
632 Element format_tag = config.createElement("format");
633 current_node.appendChild(format_tag);
634 //current_node = (Node) format_tag;
635 }
636 }
637 else
638 {
639 logger.error("Classifier is null");
640 // To support all classifiers, set classifier to null? There is the chance here that the format tag does not exist
641 if (GSXML.getChildByTagName(current_node, "format") == null)
642 {
643 logger.error("ERROR: classifier does not have a format child");
644 // well then create a format tag
645 Element format_tag = config.createElement("format");
646 current_node.appendChild(format_tag);
647 //current_node = (Node) format_tag;
648 }
649 }
650 }
651 else if (service.equals("AllClassifierBrowse"))
652 {
653 logger.error("Looking for browse");
654 current_node = GSXML.getChildByTagName(current_node, "browse");
655 if (GSXML.getChildByTagName(current_node, "format") == null)
656 {
657 logger.error("ERROR AllClassifierBrowse: all classifiers do not have a format child");
658 // well then create a format tag
659 Element format_tag = config.createElement("format");
660 current_node.appendChild(format_tag);
661 //current_node = (Node) format_tag;
662 }
663 }
664 else
665 {
666 // look in <format> with no attributes
667 logger.error("I presume this is search");
668
669 current_node_list = GSXML.getChildrenByTagName(current_node, "search");
670 for (k = 0; k < current_node_list.getLength(); k++)
671 {
672 current_node = current_node_list.item(k);
673 // if current_node has no attributes then break
674 elem = (Element) current_node;
675 if (elem.hasAttribute("name") == false)
676 break;
677 }
678 }
679
680 current_node.replaceChild(config.importNode(format_statement, true), GSXML.getChildByTagName(current_node, "format"));
681
682 // Now convert config document to string for writing to file
683 logger.error("Convert config to string");
684 String new_config = this.converter.getString(config);
685
686 new_config = StringUtils.replace(new_config, "&lt;", "<");
687 new_config = StringUtils.replace(new_config, "&gt;", ">");
688 new_config = StringUtils.replace(new_config, "&quot;", "\"");
689
690 // Write to file (not original! for now)
691 BufferedWriter writer = new BufferedWriter(new FileWriter(collection_config + ".new"));
692 writer.write(new_config);
693 writer.close();
694 logger.error("All is happy with collection");
695
696 }
697 catch (Exception ex)
698 {
699 logger.error("There was an exception " + ex);
700
701 StringWriter sw = new StringWriter();
702 PrintWriter pw = new PrintWriter(sw, true);
703 ex.printStackTrace(pw);
704 pw.flush();
705 sw.flush();
706 logger.error(sw.toString());
707 }
708
709 }
710 }
711 else
712 { // unknown type
713 return super.processMessage(request);
714
715 }
716 return response;
717 }
718
719}
Note: See TracBrowser for help on using the repository browser.