source: greenstone3/trunk/src/java/org/greenstone/gsdl3/core/TransformingReceptionist.java@ 20149

Last change on this file since 20149 was 20149, checked in by kjdon, 15 years ago

indented the file consistently

  • Property svn:keywords set to Author Date Id Revision
File size: 23.4 KB
Line 
1package org.greenstone.gsdl3.core;
2
3import org.greenstone.gsdl3.util.*;
4import org.greenstone.gsdl3.action.*;
5// XML classes
6import org.w3c.dom.Node;
7import org.w3c.dom.NodeList;
8import org.w3c.dom.Document;
9import org.w3c.dom.Element;
10import org.xml.sax.InputSource;
11
12// other java classes
13import java.io.File;
14import java.io.StringWriter;
15import java.io.FileReader;
16import java.io.FileNotFoundException;
17import java.util.HashMap;
18import java.util.Enumeration;
19
20import javax.xml.parsers.*;
21import javax.xml.transform.*;
22import javax.xml.transform.dom.*;
23import javax.xml.transform.stream.*;
24import org.apache.log4j.*;
25import org.apache.xerces.dom.*;
26import org.apache.xerces.parsers.DOMParser;
27
28/** A receptionist that uses xslt to transform the page_data before returning it. . Receives requests consisting
29 * of an xml representation of cgi args, and returns the page of data - in
30 * html by default. The requests are processed by the appropriate action class
31 *
32 * @see Action
33 */
34public class TransformingReceptionist extends Receptionist{
35
36 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.core.TransformingReceptionist.class.getName());
37
38 /** The preprocess.xsl file is in a fixed location */
39 static final String preprocess_xsl_filename = GlobalProperties.getGSDL3Home() + File.separatorChar
40 + "ui" + File.separatorChar + "xslt" + File.separatorChar + "preProcess.xsl";
41
42 /** the list of xslt to use for actions */
43 protected HashMap xslt_map = null;
44
45 /** a transformer class to transform xml using xslt */
46 protected XMLTransformer transformer=null;
47
48 protected TransformerFactory transformerFactory=null;
49 protected DOMParser parser = null;
50 public TransformingReceptionist() {
51 super();
52 this.xslt_map = new HashMap();
53 this.transformer = new XMLTransformer();
54 try {
55 transformerFactory = org.apache.xalan.processor.TransformerFactoryImpl.newInstance();
56 this.converter = new XMLConverter();
57 //transformerFactory.setURIResolver(new MyUriResolver()) ;
58
59 parser = new DOMParser();
60 parser.setFeature("http://xml.org/sax/features/validation", false);
61 // don't try and load external DTD - no need if we are not validating, and may cause connection errors if a proxy is not set up.
62 parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
63 // a performance test showed that having this on lead to increased
64 // memory use for small-medium docs, and not much gain for large
65 // docs.
66 // http://www.sosnoski.com/opensrc/xmlbench/conclusions.html
67 parser.setFeature("http://apache.org/xml/features/dom/defer-node-expansion", false);
68 parser.setFeature("http://apache.org/xml/features/continue-after-fatal-error", true);
69 // setting a handler for when fatal errors, errors or warnings happen during xml parsing
70 // call XMLConverter's getParseErrorMessage() to get the errorstring that can be rendered as web page
71 this.parser.setErrorHandler(new XMLConverter.ParseErrorHandler());
72 }
73 catch (Exception e) {
74 e.printStackTrace();
75 }
76
77 }
78
79 /** configures the receptionist - overwrite this to set up the xslt map*/
80 public boolean configure() {
81
82 if (this.config_params==null) {
83 logger.error(" config variables must be set before calling configure");
84 return false;
85 }
86 if (this.mr==null) {
87 logger.error(" message router must be set before calling configure");
88 return false;
89 }
90
91 // find the config file containing a list of actions
92 File interface_config_file = new File(GSFile.interfaceConfigFile(GSFile.interfaceHome(GlobalProperties.getGSDL3Home(), (String)this.config_params.get(GSConstants.INTERFACE_NAME))));
93 if (!interface_config_file.exists()) {
94 logger.error(" interface config file: "+interface_config_file.getPath()+" not found!");
95 return false;
96 }
97 Document config_doc = this.converter.getDOM(interface_config_file, "utf-8");
98 if (config_doc == null) {
99 logger.error(" could not parse interface config file: "+interface_config_file.getPath());
100 return false;
101 }
102 Element config_elem = config_doc.getDocumentElement();
103 String base_interface = config_elem.getAttribute("baseInterface");
104 setUpBaseInterface(base_interface);
105 setUpInterfaceOptions(config_elem);
106
107 Element action_list = (Element)GSXML.getChildByTagName(config_elem, GSXML.ACTION_ELEM+GSXML.LIST_MODIFIER);
108 NodeList actions = action_list.getElementsByTagName(GSXML.ACTION_ELEM);
109
110 for (int i=0; i<actions.getLength(); i++) {
111 Element action = (Element) actions.item(i);
112 String class_name = action.getAttribute("class");
113 String action_name = action.getAttribute("name");
114 Action ac = null;
115 try {
116 ac = (Action)Class.forName("org.greenstone.gsdl3.action."+class_name).newInstance();
117 } catch (Exception e) {
118 logger.error(" couldn't load in action "+class_name);
119 e.printStackTrace();
120 continue;
121 }
122 ac.setConfigParams(this.config_params);
123 ac.setMessageRouter(this.mr);
124 ac.configure();
125 ac.getActionParameters(this.params);
126 this.action_map.put(action_name, ac);
127
128 // now do the xslt map
129 String xslt = action.getAttribute("xslt");
130 if (!xslt.equals("")) {
131 this.xslt_map.put(action_name, xslt);
132 }
133 NodeList subactions = action.getElementsByTagName(GSXML.SUBACTION_ELEM);
134 for (int j=0; j<subactions.getLength(); j++) {
135 Element subaction = (Element)subactions.item(j);
136 String subname = subaction.getAttribute(GSXML.NAME_ATT);
137 String subxslt = subaction.getAttribute("xslt");
138
139 String map_key = action_name+":"+subname;
140 logger.debug("adding in to xslt map, "+map_key+"->"+subxslt);
141 this.xslt_map.put(map_key, subxslt);
142 }
143 }
144 Element lang_list = (Element)GSXML.getChildByTagName(config_elem, "languageList");
145 if (lang_list == null) {
146 logger.error(" didn't find a language list in the config file!!");
147 } else {
148 this.language_list = (Element) this.doc.importNode(lang_list, true);
149 }
150
151 return true;
152 }
153
154
155 protected Node postProcessPage(Element page) {
156 // might need to add some data to the page
157 addExtraInfo(page);
158 // transform the page using xslt
159 Node transformed_page = transformPage(page);
160
161 return transformed_page;
162 }
163
164 /** overwrite this to add any extra info that might be needed in the page before transformation */
165 protected void addExtraInfo(Element page) {}
166
167 /** transform the page using xslt
168 * we need to get any format element out of the page and add it to the xslt
169 * before transforming */
170 protected Node transformPage(Element page) {
171
172 logger.debug("page before transfomring:");
173 logger.debug(this.converter.getPrettyString(page));
174
175 Element request = (Element)GSXML.getChildByTagName(page, GSXML.PAGE_REQUEST_ELEM);
176 String action = request.getAttribute(GSXML.ACTION_ATT);
177 String subaction = request.getAttribute(GSXML.SUBACTION_ATT);
178
179 String output = request.getAttribute(GSXML.OUTPUT_ATT);
180 // we should choose how to transform the data based on output, eg diff
181 // choice for html, and wml??
182 // for now, if output=xml, we don't transform the page, we just return
183 // the page xml
184 if (output.equals("xml")) {
185 return page;
186 }
187
188
189 Element cgi_param_list = (Element)GSXML.getChildByTagName(request, GSXML.PARAM_ELEM+GSXML.LIST_MODIFIER);
190 String collection = "";
191 if (cgi_param_list != null) {
192 HashMap params = GSXML.extractParams(cgi_param_list, false);
193 collection = (String)params.get(GSParams.COLLECTION);
194 if (collection == null) collection = "";
195 }
196
197 String xslt_file = getXSLTFileName(action, subaction, collection);
198 if (xslt_file==null) {
199 // returning file not found error page to indicate which file is missing
200 return fileNotFoundErrorPage(xslt_file);
201 }
202
203 Document style_doc = this.converter.getDOM(new File(xslt_file), "UTF-8");
204 String errorPage = this.converter.getParseErrorMessage();
205 if(errorPage != null) {
206 return XMLTransformer.constructErrorXHTMLPage(
207 "Cannot parse the xslt file: " + xslt_file + "\n" + errorPage);
208 }
209 if (style_doc == null) {
210 logger.error(" cant parse the xslt file needed, so returning the original page!");
211 return page;
212 }
213
214
215 // put the page into a document - this is necessary for xslt to get
216 // the paths right if you have paths relative to the document root
217 // eg /page.
218 Document doc = this.converter.newDOM();
219 doc.appendChild(doc.importNode(page, true));
220 Element page_response = (Element)GSXML.getChildByTagName(page, GSXML.PAGE_RESPONSE_ELEM);
221 Element format_elem = (Element)GSXML.getChildByTagName(page_response, GSXML.FORMAT_ELEM);
222 if (output.equals("formatelem")) {
223 return format_elem;
224 }
225 if (format_elem != null) {
226 //page_response.removeChild(format_elem);
227 logger.debug("format elem="+this.converter.getPrettyString(format_elem));
228 // need to transform the format info
229 String configStylesheet_file = GSFile.stylesheetFile(GlobalProperties.getGSDL3Home(), (String)this.config_params.get(GSConstants.SITE_NAME), collection, (String)this.config_params.get(GSConstants.INTERFACE_NAME), base_interfaces, "config_format.xsl");
230 Document configStylesheet_doc = this.converter.getDOM(new File(configStylesheet_file));
231 if (configStylesheet_doc != null) {
232 Document format_doc = this.converter.newDOM();
233 format_doc.appendChild(format_doc.importNode(format_elem, true));
234 Node result = this.transformer.transform(configStylesheet_doc, format_doc);
235
236 // Since we started creating documents with DocTypes, we can end up with
237 // Document objects here. But we will be working with an Element instead,
238 // so we grab the DocumentElement() of the Document object in such a case.
239 Element new_format;
240 if(result.getNodeType() == Node.DOCUMENT_NODE) {
241 new_format = ((Document)result).getDocumentElement();
242 } else {
243 new_format = (Element)result;
244 }
245 logger.debug("new format elem="+this.converter.getPrettyString(new_format));
246 if (output.equals("newformat")) {
247 return new_format;
248 }
249
250 // add extracted GSF statements in to the main stylesheet
251 GSXSLT.mergeStylesheets(style_doc, new_format);
252 //System.out.println("added extracted GSF statements into the main stylesheet") ;
253
254 // add extracted GSF statements in to the debug test stylesheet
255 //GSXSLT.mergeStylesheets(oldStyle_doc, new_format);
256 } else {
257 logger.error(" couldn't parse the config_format stylesheet, adding the format info as is");
258 GSXSLT.mergeStylesheets(style_doc, format_elem);
259 //GSXSLT.mergeStylesheets(oldStyle_doc, format_elem);
260 }
261 logger.debug("the converted stylesheet is:");
262 logger.debug(this.converter.getPrettyString(style_doc.getDocumentElement()));
263 }
264
265 //for debug purposes only
266 Document oldStyle_doc = style_doc;
267
268
269 Document preprocessingXsl ;
270 try {
271 preprocessingXsl = getPreprocessDoc();
272 String errMsg = ((XMLConverter.ParseErrorHandler)parser.getErrorHandler()).getErrorMessage();
273 if(errMsg != null) {
274 return XMLTransformer.constructErrorXHTMLPage("error loading preprocess xslt file: "
275 + preprocess_xsl_filename + "\n" + errMsg);
276 }
277 } catch (java.io.FileNotFoundException e) {
278 return fileNotFoundErrorPage(e.getMessage());
279 } catch (Exception e) {
280 e.printStackTrace() ;
281 System.out.println("error loading preprocess xslt") ;
282 return XMLTransformer.constructErrorXHTMLPage("error loading preprocess xslt\n" + e.getMessage());
283 }
284
285 Document libraryXsl = null;
286 try {
287 libraryXsl = getLibraryDoc() ;
288 String errMsg = ((XMLConverter.ParseErrorHandler)parser.getErrorHandler()).getErrorMessage();
289 if(errMsg != null) {
290 return XMLTransformer.constructErrorXHTMLPage("Error loading xslt file: "
291 + this.getLibraryXSLFilename() + "\n" + errMsg);
292 }
293 } catch (java.io.FileNotFoundException e) {
294 return fileNotFoundErrorPage(e.getMessage());
295 } catch (Exception e) {
296 e.printStackTrace() ;
297 System.out.println("error loading library xslt") ;
298 return XMLTransformer.constructErrorXHTMLPage("error loading library xslt\n" + e.getMessage()) ;
299 }
300
301 // Combine the skin file and library variables/templates into one document.
302 // Please note: We dont just use xsl:import because the preprocessing stage
303 // needs to know what's available in the library.
304
305 Document skinAndLibraryXsl = null ;
306 Document skinAndLibraryDoc = converter.newDOM();
307 try {
308
309 skinAndLibraryXsl = converter.newDOM();
310 Element root = skinAndLibraryXsl.createElement("skinAndLibraryXsl") ;
311 skinAndLibraryXsl.appendChild(root) ;
312
313 Element s = skinAndLibraryXsl.createElement("skinXsl") ;
314 s.appendChild(skinAndLibraryXsl.importNode(style_doc.getDocumentElement(), true)) ;
315 root.appendChild(s) ;
316
317 Element l = skinAndLibraryXsl.createElement("libraryXsl") ;
318 Element libraryXsl_el = libraryXsl.getDocumentElement();
319 l.appendChild(skinAndLibraryXsl.importNode(libraryXsl_el, true)) ;
320 root.appendChild(l) ;
321 //System.out.println("Skin and Library XSL are now together") ;
322
323
324 //System.out.println("Pre-processing the skin file...") ;
325
326 //pre-process the skin style sheet
327 //In other words, apply the preProcess.xsl to 'skinAndLibraryXsl' in order to
328 //expand all GS-Lib statements into complete XSL statements and also to create
329 //a valid xsl style sheet document.
330
331 Transformer preProcessor = transformerFactory.newTransformer(new DOMSource(preprocessingXsl));
332 preProcessor.setErrorListener(new XMLTransformer.TransformErrorListener());
333 DOMResult result = new DOMResult();
334 result.setNode(skinAndLibraryDoc);
335 preProcessor.transform(new DOMSource(skinAndLibraryXsl), result);
336 //System.out.println("GS-Lib statements are now expanded") ;
337
338 }
339 catch (TransformerException e) {
340 e.printStackTrace() ;
341 System.out.println("TransformerException while preprocessing the skin xslt") ;
342 return XMLTransformer.constructErrorXHTMLPage(e.getMessage()) ;
343 }
344 catch (Exception e) {
345 e.printStackTrace() ;
346 System.out.println("Error while preprocessing the skin xslt") ;
347 return XMLTransformer.constructErrorXHTMLPage(e.getMessage()) ;
348 }
349
350 //The following code is to be uncommented if we need to append the extracted GSF statements
351 //after having extracted the GSLib elements. In case of a problem during postprocessing.
352 /*
353 // put the page into a document - this is necessary for xslt to get
354 // the paths right if you have paths relative to the document root
355 // eg /page.
356 Document doc = this.converter.newDOM();
357 doc.appendChild(doc.importNode(page, true));
358 Element page_response = (Element)GSXML.getChildByTagName(page, GSXML.PAGE_RESPONSE_ELEM);
359 Element format_elem = (Element)GSXML.getChildByTagName(page_response, GSXML.FORMAT_ELEM);
360 if (output.equals("formatelem")) {
361 return format_elem;
362 }
363 if (format_elem != null) {
364 //page_response.removeChild(format_elem);
365 logger.debug("format elem="+this.converter.getPrettyString(format_elem));
366 // need to transform the format info
367 String configStylesheet_file = GSFile.stylesheetFile(GlobalProperties.getGSDL3Home(), (String)this.config_params.get(GSConstants.SITE_NAME), collection, (String)this.config_params.get(GSConstants.INTERFACE_NAME), base_interfaces, "config_format.xsl");
368 Document configStylesheet_doc = this.converter.getDOM(new File(configStylesheet_file));
369 if (configStylesheet_doc != null) {
370 Document format_doc = this.converter.newDOM();
371 format_doc.appendChild(format_doc.importNode(format_elem, true));
372 Node result = this.transformer.transform(configStylesheet_doc, format_doc);
373
374 // Since we started creating documents with DocTypes, we can end up with
375 // Document objects here. But we will be working with an Element instead,
376 // so we grab the DocumentElement() of the Document object in such a case.
377 Element new_format;
378 if(result.getNodeType() == Node.DOCUMENT_NODE) {
379 new_format = ((Document)result).getDocumentElement();
380 } else {
381 new_format = (Element)result;
382 }
383 logger.debug("new format elem="+this.converter.getPrettyString(new_format));
384 if (output.equals("newformat")) {
385 return new_format;
386 }
387
388 // add extracted GSF statements in to the main stylesheet
389 GSXSLT.mergeStylesheets(skinAndLibraryDoc, new_format);
390 //System.out.println("added extracted GSF statements into the main stylesheet") ;
391
392 // add extracted GSF statements in to the debug test stylesheet
393 //GSXSLT.mergeStylesheets(oldStyle_doc, new_format);
394 } else {
395 logger.error(" couldn't parse the config_format stylesheet, adding the format info as is");
396 GSXSLT.mergeStylesheets(skinAndLibraryDoc, format_elem);
397 // GSXSLT.mergeStylesheets(oldStyle_doc, format_elem);
398 }
399 logger.debug("the converted stylesheet is:");
400 logger.debug(this.converter.getPrettyString(skinAndLibraryDoc.getDocumentElement()));
401 }
402 */
403
404 // there is a thing called a URIResolver which you can set for a
405 // transformer or transformer factory. may be able to use this
406 // instead of this absoluteIncludepaths hack
407
408 GSXSLT.absoluteIncludePaths(skinAndLibraryDoc, GlobalProperties.getGSDL3Home(),
409 (String)this.config_params.get(GSConstants.SITE_NAME),
410 collection, (String)this.config_params.get(GSConstants.INTERFACE_NAME),
411 base_interfaces);
412
413
414 //Same but for the debug version when we want the do the transformation like we use to do
415 //without any gslib elements.
416 GSXSLT.absoluteIncludePaths(oldStyle_doc, GlobalProperties.getGSDL3Home(),
417 (String)this.config_params.get(GSConstants.SITE_NAME),
418 collection, (String)this.config_params.get(GSConstants.INTERFACE_NAME),
419 base_interfaces);
420
421 //Send different stages of the skin xslt to the browser for debug purposes only
422 //using &o=skindoc or &o=skinandlib etc...
423 if (output.equals("skindoc")) {
424 return converter.getDOM(getStringFromDocument(style_doc));
425 }
426 if (output.equals("skinandlib")) {
427 return converter.getDOM(getStringFromDocument(skinAndLibraryXsl));
428 }
429 if (output.equals("skinandlibdoc")) {
430 return converter.getDOM(getStringFromDocument(skinAndLibraryDoc));
431 }
432 if (output.equals("oldskindoc")) {
433 return converter.getDOM(getStringFromDocument(oldStyle_doc));
434 }
435
436 // DocType defaults in case the skin doesn't have an "xsl:output" element
437 String qualifiedName = "html";
438 String publicID = "-//W3C//DTD HTML 4.01 Transitional//EN";
439 String systemID = "http://www.w3.org/TR/html4/loose.dtd";
440
441 // Try to get the system and public ID from the current skin xsl document
442 // otherwise keep the default values.
443 Element root = skinAndLibraryDoc.getDocumentElement();
444 NodeList nodes = root.getElementsByTagName("xsl:output");
445 // If there is at least one "xsl:output" command in the final xsl then...
446 if(nodes.getLength() != 0) {
447 // There should be only one element called xsl:output,
448 // but if this is not the case get the last one
449 Element xsl_output = (Element)nodes.item(nodes.getLength()-1);
450 if (xsl_output != null) {
451 // Qualified name will always be html even for xhtml pages
452 //String attrValue = xsl_output.getAttribute("method");
453 //qualifiedName = attrValue.equals("") ? qualifiedName : attrValue;
454
455 String attrValue = xsl_output.getAttribute("doctype-system");
456 systemID = attrValue.equals("") ? systemID : attrValue;
457
458 attrValue = xsl_output.getAttribute("doctype-public");
459 publicID = attrValue.equals("") ? publicID : attrValue;
460 }
461 }
462
463 // We need to create an empty document with a predefined DocType,
464 // that will then be used for the transformation by the DOMResult
465 Document docWithDoctype = converter.newDOM(qualifiedName, publicID, systemID);
466
467 //System.out.println(converter.getPrettyString(docWithDoctype));
468 //System.out.println("Doctype vals: " + qualifiedName + " " + publicID + " " + systemID) ;
469
470
471 //System.out.println("Generate final HTML from current skin") ;
472 //Transformation of the XML message from the receptionist to HTML with doctype
473 return this.transformer.transform(skinAndLibraryDoc, doc, config_params, docWithDoctype);
474
475
476 // The line below will do the transformation like we use to do before having Skin++ implemented,
477 // it will not contain any GS-Lib statements expanded, and the result will not contain any doctype.
478
479 //return (Element)this.transformer.transform(style_doc, doc, config_params);
480
481 }
482
483
484 // method to convert Document to a proper XML string for debug purposes only
485 protected String getStringFromDocument(Document doc)
486 {
487 String content = "";
488 try
489 {
490 DOMSource domSource = new DOMSource(doc);
491 StringWriter writer = new StringWriter();
492 StreamResult result = new StreamResult(writer);
493 TransformerFactory tf = TransformerFactory.newInstance();
494 Transformer transformer = tf.newTransformer();
495 transformer.transform(domSource, result);
496 content = writer.toString();
497 System.out.println("Change the & to &Amp; for proper debug dispay") ;
498 content = content.replaceAll("&", "&amp;");
499 writer.flush();
500 }
501 catch(TransformerException ex)
502 {
503 ex.printStackTrace();
504 return null;
505 }
506 return content;
507 }
508
509
510 protected Document getPreprocessDoc() throws Exception {
511
512 File xslt_file = new File(preprocess_xsl_filename) ;
513
514 FileReader reader = new FileReader(xslt_file);
515 InputSource xml_source = new InputSource(reader);
516 this.parser.parse(xml_source);
517 Document doc = this.parser.getDocument();
518
519 return doc ;
520 }
521
522 protected Document getLibraryDoc() throws Exception {
523 Document doc = null;
524 File xslt_file = new File(this.getLibraryXSLFilename()) ;
525
526 FileReader reader = new FileReader(xslt_file);
527 InputSource xml_source = new InputSource(reader);
528 this.parser.parse(xml_source);
529
530 doc = this.parser.getDocument();
531 return doc ;
532 }
533
534 protected String getXSLTFileName(String action, String subaction,
535 String collection) {
536
537 String name = null;
538 if (!subaction.equals("")) {
539 String key = action+":"+subaction;
540 name = (String) this.xslt_map.get(key);
541 }
542 // try the action by itself
543 if (name==null) {
544 name = (String) this.xslt_map.get(action);
545 }
546 // now find the absolute path
547 String stylesheet = GSFile.stylesheetFile(GlobalProperties.getGSDL3Home(), (String)this.config_params.get(GSConstants.SITE_NAME), collection, (String)this.config_params.get(GSConstants.INTERFACE_NAME), base_interfaces, name);
548 if (stylesheet==null) {
549 logger.info(" cant find stylesheet for "+name);
550 }
551 return stylesheet;
552 }
553
554 // returns the library.xsl path of the library file that is applicable for the current interface
555 protected String getLibraryXSLFilename() {
556 return GSFile.xmlTransformDir(GSFile.interfaceHome(
557 GlobalProperties.getGSDL3Home(), (String)this.config_params.get(GSConstants.INTERFACE_NAME)))
558 + File.separatorChar + "library.xsl";
559 }
560
561 // Call this when a FileNotFoundException could be thrown when loading an xsl (xml) file.
562 // Returns an error xhtml page indicating which xsl (or other xml) file is missing.
563 protected Document fileNotFoundErrorPage(String filenameMessage) {
564 String errorMessage = "ERROR missing file: " + filenameMessage;
565 Element errPage = XMLTransformer.constructErrorXHTMLPage(errorMessage);
566 logger.error(errorMessage);
567 System.err.println("****" + errorMessage);
568 return errPage.getOwnerDocument();
569 }
570}
Note: See TracBrowser for help on using the repository browser.