source: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/util/OAIXML.java@ 28851

Last change on this file since 28851 was 28851, checked in by kjdon, 10 years ago

tidied up OAIXML. Moved out any generic methods. Split off resumption token code to OAIResumptionToken.java, apart from the bit where we generate the XML for the OAI response. resumption token todo: still have to handle expiring old tokens.

File size: 22.2 KB
Line 
1/*
2 * OAIXML.java
3 * Copyright (C) 2008 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.util;
20
21import org.greenstone.util.GlobalProperties;
22
23import org.w3c.dom.*;
24
25import java.io.*;
26import java.net.*;
27import java.util.*;
28import java.text.DateFormat;
29import java.text.SimpleDateFormat;
30
31// import file Logger.java
32import org.apache.log4j.*;
33
34/** these constants are used for the OAI service */
35public class OAIXML {
36
37 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.util.GSXML.class.getName());
38
39 // the leading keyword of oai protocol
40 public static final String VERB = "verb";
41
42 // six valid oai verbs
43 public static final String GET_RECORD = "GetRecord";
44 public static final String LIST_RECORDS = "ListRecords";
45 public static final String LIST_IDENTIFIERS = "ListIdentifiers";
46 public static final String LIST_SETS = "ListSets";
47 public static final String LIST_METADATA_FORMATS = "ListMetadataFormats";
48 public static final String IDENTIFY = "Identify";
49
50 // oai request parameters
51 public static final String METADATA_PREFIX = "metadataPrefix";
52 public static final String FROM = "from";
53 public static final String UNTIL = "until";
54 public static final String SET = "set";
55 public static final String RESUMPTION_TOKEN = "resumptionToken";
56 public static final String IDENTIFIER = "identifier";
57
58 // Error element and code att
59 public static final String ERROR = "error";
60 public static final String CODE = "code";
61
62 // OAI error codes
63 public static final String BAD_ARGUMENT = "badArgument";
64 public static final String BAD_RESUMPTION_TOKEN = "badResumptionToken";
65 public static final String BAD_VERB = "badVerb";
66 public static final String CANNOT_DISSEMINATE_FORMAT = "cannotDisseminateFormat";
67 public static final String ID_DOES_NOT_EXIST = "idDoesNotExist";
68 public static final String NO_METADATA_FORMATS = "noMetadataFormats";
69 public static final String NO_RECORDS_MATCH = "noRecordsMatch";
70 public static final String NO_SET_HIERARCHY = "noSetHierarchy";
71
72
73 // words used to compose oai responses
74 // many of these used in OAIConfig too
75
76 // General
77 public static final String OAI_PMH = "OAI-PMH";
78 public static final String RESPONSE_DATE = "responseDate";
79 public static final String REQUEST = "request";
80
81 // Identify data
82 public static final String ADMIN_EMAIL = "adminEmail";
83 public static final String BASE_URL = "baseURL";
84 public static final String COMPRESSION = "compression";
85 public static final String DELETED_RECORD = "deletedRecord";
86 public static final String DESCRIPTION = "description";
87 public static final String EARLIEST_DATESTAMP = "earliestDatestamp";
88 public static final String GRANULARITY = "granularity";
89 public static final String PROTOCOL_VERSION = "protocolVersion";
90 public static final String REPOSITORY_NAME = "repositoryName";
91 public static final String OAI_IDENTIFIER = "oai-identifier";
92 public static final String SCHEME = "scheme";
93 public static final String REPOSITORY_IDENTIFIER = "repositoryIdentifier";
94 public static final String DELIMITER = "delimiter";
95 public static final String SAMPLE_IDENTIFIER = "sampleIdentifier";
96
97 // metadata formats
98 public static final String METADATA_FORMAT = "metadataFormat";
99 public static final String SCHEMA = "schema";
100 public static final String METADATA_NAMESPACE = "metadataNamespace";
101 public static final String OAI_DC = "oai_dc";
102 public static final String DC = "dc";
103
104 // record response data
105 // SET_SPEC
106 public static final String RECORD = "record";
107 public static final String HEADER = "header";
108 public static final String DATESTAMP = "datestamp";
109 public static final String METADATA = "metadata";
110
111 // list sets
112 // SET,
113 public static final String SET_NAME = "setName";
114 public static final String SET_SPEC = "setSpec";
115 public static final String SET_DESCRIPTION = "setDescription";
116
117 // resumption token element
118 public static final String RESUMPTION_TOKEN_ELEM = "resumptionToken";
119 public static final String EXPIRATION_DATE = "expirationDate";
120 public static final String COMPLETE_LIST_SIZE = "completeListSize";
121 public static final String CURSOR = "cursor";
122
123 // extra elements/attributes from OAIConfig
124 public static final String OAI_INFO = "oaiInfo";
125 public static final String USE_STYLESHEET = "useOAIStylesheet";
126 public static final String STYLESHEET = "OAIStylesheet";
127 public static final String RESUME_AFTER = "resumeAfter";
128 public static final String RESUMPTION_TOKEN_EXPIRATION = "resumptionTokenExpiration";
129 public static final String OAI_SUPER_SET = "oaiSuperSet";
130 public static final String MAPPING = "mapping";
131 public static final String MAPPING_LIST = "mappingList";
132
133 // code constants
134 public static final String GS_OAI_RESOURCE_URL = "gs.OAIResourceURL";
135 public static final String ILLEGAL_OAI_VERB = "Illegal OAI verb";
136 public static final String LASTMODIFIED = "lastmodified";
137 // // The node id in the collection database, which contains all the OIDs in the database
138 public static final String BROWSELIST = "browselist";
139 public static final String OAI_LASTMODIFIED = "oailastmodified";
140 public static final String OAIPMH = "OAIPMH";
141 public static final String OAI_SET_LIST = "oaiSetList";
142 public static final String OAI_SERVICE_UNAVAILABLE = "OAI service unavailable";
143 public static final String OID = "OID";
144
145 //system-dependent file separator, maybe '/' or '\'
146 public static final String FILE_SEPARATOR = File.separator;
147 public static final String OAI_VERSION1 = "1.0";
148 public static final String OAI_VERSION2 = "2.0";
149 /*************************above are final values****************************/
150
151
152 //initialized in getOAIConfigXML()
153 public static Element oai_config_elem = null;
154
155 //stores the date format "yyyy-MM-ddTHH:mm:ssZ"
156 public static String granularity = "";
157
158 // http://www.openarchives.org/OAI/openarchivesprotocol.html#DatestampsRequests
159 // specifies that all repositories must support YYYY-MM-DD (yyyy-MM-dd in Java)
160 // this would be in addition to the other (optional) granularity of above that
161 // a repository may additionally choose to support.
162 public static final String default_granularity = "yyyy-MM-dd";
163
164 public static long token_expiration = 7200;
165 /** which version of oai that this oaiserver supports; default is 2.0
166 * initialized in getOAIConfigXML()
167 */
168 public static String oai_version = "2.0";
169 public static String baseURL = "";
170
171 /** Converter for parsing files and creating Elements */
172 public static XMLConverter converter = new XMLConverter();
173
174 public static String[] special_char = {"/", "?", "#", "=", "&", ":", ";", " ", "%", "+"};
175 public static String[] escape_sequence = {"%2F", "%3F", "%23", "%3D", "%26", "%3A", "%3B", "%20", "%25", "%2B"};
176
177 public static String getOAIVersion() {
178 return oai_version;
179 }
180
181 public static String getBaseURL() {
182 return baseURL;
183 }
184
185 /** Read in OAIConfig.xml (residing web/WEB-INF/classes/) and use it to configure the receptionist etc.
186 * the oai_version and baseURL variables are also set in here.
187 * The init() method is also called in here. */
188 public static Element getOAIConfigXML() {
189
190 File oai_config_file = null;
191
192 try {
193 URL oai_config_url = Class.forName("org.greenstone.gsdl3.OAIServer").getClassLoader().getResource("OAIConfig.xml");
194 if (oai_config_url == null) {
195 logger.error("couldn't find OAIConfig.xml via class loader");
196 return null;
197 }
198 oai_config_file = new File(oai_config_url.toURI());
199 if (!oai_config_file.exists()) {
200 logger.error(" oai config file: "+oai_config_file.getPath()+" not found!");
201 return null;
202 }
203 } catch(Exception e) {
204 logger.error("couldn't find OAIConfig.xml "+e.getMessage());
205 return null;
206 }
207
208 Document oai_config_doc = converter.getDOM(oai_config_file, "utf-8");
209 if (oai_config_doc != null) {
210 oai_config_elem = oai_config_doc.getDocumentElement();
211 } else {
212 logger.error("Failed to parse oai config file OAIConfig.xml.");
213 return null;
214 }
215
216 //initialize oai_version
217 Element protocol_version = (Element)GSXML.getChildByTagName(oai_config_elem, PROTOCOL_VERSION);
218 oai_version = GSXML.getNodeText(protocol_version).trim();
219
220 // initialize baseURL
221 Element base_url_elem = (Element)GSXML.getChildByTagName(oai_config_elem, BASE_URL);
222 baseURL = GSXML.getNodeText(base_url_elem);
223
224 //initialize token_expiration
225 Element expiration = (Element)GSXML.getChildByTagName(oai_config_elem, RESUMPTION_TOKEN_EXPIRATION);
226 String expire_str = GSXML.getNodeText(expiration).trim();
227 if (expiration != null && !expire_str.equals("")) {
228 token_expiration = Long.parseLong(expire_str);
229 }
230
231 // read granularity from the config file
232 Element granu_elem = (Element)GSXML.getChildByTagName(oai_config_elem, GRANULARITY);
233 //initialize the granu_str which might be used by other methods (eg, getDate())
234 granularity = GSXML.getNodeText(granu_elem).trim();
235
236 //change "yyyy-MM-ddTHH:mm:ssZ" to "yyyy-MM-dd'T'HH:mm:ss'Z'"
237 granularity = granularity.replaceAll("T", "'T'");
238 granularity = granularity.replaceAll("Z", "'Z'");
239 granularity = granularity.replaceAll("YYYY", "yyyy").replaceAll("DD", "dd").replaceAll("hh", "HH");
240 return oai_config_elem;
241 }
242
243 public static String[] getMetadataMapping(Element metadata_format) {
244
245 if (metadata_format == null) {
246 return null;
247 }
248 NodeList mappings = metadata_format.getElementsByTagName(MAPPING);
249 int size = mappings.getLength();
250 if (size == 0) {
251 logger.info("No metadata mappings are provided in OAIConfig.xml.");
252 return null;
253 }
254 String[] names = new String[size];
255 for (int i=0; i<size; i++) {
256 names[i] = GSXML.getNodeText((Element)mappings.item(i)).trim();
257 }
258 return names;
259
260 }
261
262 public static String[] getGlobalMetadataMapping(String prefix) {
263 Element list_meta_formats = (Element)GSXML.getChildByTagName(oai_config_elem, LIST_METADATA_FORMATS);
264 if(list_meta_formats == null) {
265 return null;
266 }
267 Element metadata_format = GSXML.getNamedElement(list_meta_formats, METADATA_FORMAT, METADATA_PREFIX, prefix);
268 if(metadata_format == null) {
269 return null;
270 }
271 return getMetadataMapping(metadata_format);
272 }
273
274
275 public static long getTokenExpiration() {
276 return token_expiration*1000; // in milliseconds
277 }
278
279 /** TODO: returns a basic response for appropriate oai version
280 *
281 */
282 public static Element createBasicResponse(Document doc, String verb, String[] pairs) {
283
284 Element response = createResponseHeader(doc, verb);
285
286 //set the responseDate and request elements accordingly
287 Element request_elem = (Element)GSXML.getChildByTagName(response, REQUEST);
288 if (verb.equals("")) {
289 request_elem.setAttribute(VERB, verb);
290 }
291 int num_pairs = (pairs==null)? 0 : pairs.length;
292 for (int i=num_pairs - 1; i>=0; i--) {
293 int index = pairs[i].indexOf("=");
294 if (index != -1) {
295 String[] strs = pairs[i].split("=");
296 if(strs != null && strs.length == 2) {
297 request_elem.setAttribute(strs[0], oaiDecode(strs[1]));
298 }
299 }
300 }//end of for()
301
302 GSXML.setNodeText(request_elem, baseURL);
303
304 Node resp_date = GSXML.getChildByTagName(response, RESPONSE_DATE);
305 if (resp_date != null) {
306 GSXML.setNodeText((Element)resp_date, getCurrentUTCTime());
307 }
308
309 return response;
310 }
311 /** @param error_code the value of the code attribute
312 * @param error_text the node text of the error element
313 * @return an oai error <message><response><error>
314 */
315 public static Element createErrorMessage(String error_code, String error_text) {
316 Document doc = converter.newDOM();
317 Element message = doc.createElement(GSXML.MESSAGE_ELEM);
318 Element resp = doc.createElement(GSXML.RESPONSE_ELEM);
319 message.appendChild(resp);
320 Element error = createErrorElement(doc, error_code, error_text);
321 resp.appendChild(error);
322 return message;
323 }
324
325 /** @param error_code the value of the code attribute
326 * @param error_text the node text of the error element
327 * @return an oai error <response><error>
328 */
329 public static Element createErrorResponse(String error_code, String error_text) {
330 Document doc = converter.newDOM();
331 Element resp = doc.createElement(GSXML.RESPONSE_ELEM);
332 Element error = createErrorElement(doc, error_code, error_text);
333 resp.appendChild(error);
334 return resp;
335 }
336
337 /** @param error_code the value of the code attribute
338 * @param error_text the node text of the error element
339 * @return an oai error <error>
340 */
341 public static Element createErrorElement(Document doc, String error_code, String error_text) {
342 Element error = doc.createElement(ERROR);
343 error.setAttribute(CODE, error_code);
344 GSXML.setNodeText(error, error_text);
345 return error;
346 }
347
348 /** convert the escaped sequences (eg, '%3A') of those special characters back to their
349 * original form (eg, ':').
350 */
351 public static String oaiDecode(String escaped_str) {
352 logger.info("oaiDecode() " +escaped_str);
353 for (int i=0; i<special_char.length; i++) {
354 if (escaped_str.indexOf(escape_sequence[i]) != -1) {
355 escaped_str = escaped_str.replaceAll(escape_sequence[i], special_char[i]);
356 }
357 }
358 return escaped_str;
359 }
360 /** convert those special characters (eg, ':') to their
361 * escaped sequences (eg, '%3A').
362 */
363 public static String oaiEncode(String original_str) {
364 logger.info("oaiEncode() " + original_str);
365 for (int i=0; i<special_char.length; i++) {
366 if (original_str.indexOf(special_char[i]) != -1) {
367 original_str = original_str.replaceAll(special_char[i], escape_sequence[i]);
368 }
369 }
370 return original_str;
371 }
372 /** convert YYYY-MM_DDThh:mm:ssZ to yyyy-MM-ddTHH:mm:ssZ
373 */
374 public static String convertToJava(String oai_format) {
375 oai_format = oai_format.replaceAll("YYYY", "yyyy").replaceAll("DD", "dd").replaceAll("hh", "HH");
376 return oai_format;
377 }
378 /** convert yyyy-MM-ddTHH:mm:ssZ to YYYY-MM_DDThh:mm:ssZ
379 */
380 public static String convertToOAI(String java_format) {
381 java_format = java_format.replaceAll("yyyy", "YYYY").replaceAll("dd", "DD").replaceAll("HH", "hh");
382 return java_format;
383 }
384 public static String getCurrentUTCTime() {
385 Date current_utc = new Date(System.currentTimeMillis());
386 //granularity is in the form: yyyy-MM-dd'T'HH:mm:ss'Z '
387 DateFormat formatter = new SimpleDateFormat(granularity);
388 return formatter.format(current_utc);
389 }
390 /** get a Date object from a Date format pattern string
391 *
392 * @param pattern - in the form: 2007-06-14T16:48:25Z, for example.
393 * @return a Date object - null if the pattern is not in the specified form
394 */
395
396 public static Date getDate(String pattern) {
397 if (pattern == null || pattern.equals("")) {
398 return null;
399 }
400 Date date = null;
401 // String str = pattern.replaceAll("T", " ");
402 // str = str.replaceAll("Z", "");
403 SimpleDateFormat sdf = null;
404 try {
405 sdf = new SimpleDateFormat(granularity);
406 date = sdf.parse(pattern);
407 } catch(Exception e) {
408 if(!default_granularity.equals(granularity)) { // try validating against default granularity
409 try {
410 date = null;
411 sdf = null;
412 sdf = new SimpleDateFormat(default_granularity);
413 date = sdf.parse(pattern);
414 } catch(Exception ex) {
415 logger.error("invalid date format: " + pattern);
416 return null;
417 }
418 } else {
419 logger.error("invalid date format: " + pattern);
420 return null;
421 }
422 }
423 return date;
424 }
425 /** get the million second value from a string representing time in a pattern
426 * (eg, 2007-06-14T16:48:25Z)
427 */
428 public static long getTime(String pattern) {
429 if (pattern == null || pattern.equals("")) {
430 return -1;
431 }
432 Date date = null;
433 SimpleDateFormat sdf = null;
434 try {
435 //granularity is a global variable in the form: yyyy-MM-ddTHH:mm:ssZ
436 sdf = new SimpleDateFormat(granularity);
437 date = sdf.parse(pattern);
438 } catch(Exception e) {
439 if(!default_granularity.equals(granularity)) { // try validating against default granularity
440 try {
441 date = null;
442 sdf = null;
443 sdf = new SimpleDateFormat(default_granularity);
444 date = sdf.parse(pattern);
445 } catch(Exception ex) {
446 logger.error("invalid date format: " + pattern);
447 return -1;
448 }
449 } else {
450 logger.error("invalid date format: " + pattern);
451 return -1;
452 }
453 }
454 return date.getTime();
455 }
456 /** get the string representation of a time from a long value(long type)
457 */
458 public static String getTime(long milliseconds) {
459 Date date = new Date(milliseconds);
460 SimpleDateFormat sdf = new SimpleDateFormat(granularity);
461 return sdf.format(date);
462 }
463 public static Element createResponseHeader(Document response_doc, String verb) {
464 String tag_name = (oai_version.equals(OAI_VERSION2))? OAI_PMH : verb;
465 Element oai = response_doc.createElement(tag_name);
466 Element resp_date = response_doc.createElement(RESPONSE_DATE);
467 Element req = response_doc.createElement(REQUEST);
468 oai.appendChild(resp_date);
469 oai.appendChild(req);
470
471 if(oai_version.equals(OAI_VERSION2)) {
472 oai.setAttribute("xmlns", "http://www.openarchives.org/OAI/2.0/");
473 oai.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
474 oai.setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/2.0/ \n http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd");
475 } else {
476 oai.setAttribute("xmlns", "http://www.openarchives.com/OAI/1.1/OAI_" + verb);
477 oai.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
478 oai.setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/1.1/OAI_" + verb + "\n http://www.openarchives.org/OAI/1.1/OAI_" + verb + ".xsd");
479 }
480 return oai;
481 }
482 public static Element getMetadataPrefixElement(Document doc, String tag_name, String version) {
483 //examples of tag_name: dc, oai_dc:dc, etc.
484 Element oai = doc.createElement(tag_name);
485 if (version.equals(OAI_VERSION2)) {
486 oai.setAttribute("xmlns:oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/");
487 oai.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
488 oai.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
489 oai.setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/2.0/oai_dc/ \n http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
490 } else {
491 oai.setAttribute("xmlns", "http://www.openarchives.com/OAI/1.1/");
492 oai.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
493 oai.setAttribute("xsi:schemaLocation", "http://www.openarchives.org/OAI/1.1/" + tag_name + ".xsd");
494 }
495
496 return oai;
497 }
498 public static HashMap<String, Node> getChildrenMapByTagName(Node n, String tag_name) {
499
500 HashMap<String, Node> map= new HashMap<String, Node>();
501 Node child = n.getFirstChild();
502 while (child!=null) {
503 String name = child.getNodeName();
504 if(name.equals(tag_name)) {
505 map.put(name, child);
506 }
507 child = child.getNextSibling();
508 }
509 return map;
510 }
511
512 public static Element createOAIIdentifierXML(Document doc, String repository_id, String sample_collection, String sample_doc_id) {
513 String xml = "<oai-identifier xmlns=\"http://www.openarchives.org/OAI/2.0/oai-identifier\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.openarchives.org/OAI/2.0/oai-identifier\n http://www.openarchives.org/OAI/2.0/oai-identifier.xsd\">\n <scheme>oai</scheme>\n<repositoryIdentifier>" + repository_id + "</repositoryIdentifier>\n<delimiter>:</delimiter>\n<sampleIdentifier>oai:"+repository_id+":"+sample_collection+":"+sample_doc_id+"</sampleIdentifier>\n</oai-identifier>";
514
515 Document xml_doc = converter.getDOM(xml);
516 return (Element)doc.importNode(xml_doc.getDocumentElement(), true);
517
518
519 }
520
521 public static Element createGSDLElement(Document doc) {
522 String xml = "<gsdl xmlns=\"http://www.greenstone.org/namespace/gsdl_oaiinfo/1.0/gsdl_oaiinfo\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.greenstone.org/namespace/gsdl_oaiinfo/1.0/gsdl_oaiinfo\n http://www.greenstone.org/namespace/gsdl_oaiinfo/1.0/gsdl_oaiinfo.xsd\"></gsdl>";
523 Document xml_doc = converter.getDOM(xml);
524 return (Element)doc.importNode(xml_doc.getDocumentElement(), true);
525
526
527 }
528
529 public static Element createSet(Document doc, String spec, String name, String description) {
530
531 Element set_elem = doc.createElement(SET);
532 Element set_spec = doc.createElement(SET_SPEC);
533 GSXML.setNodeText(set_spec, spec);
534 set_elem.appendChild(set_spec);
535 Element set_name = doc.createElement(SET_NAME);
536 GSXML.setNodeText(set_name, name);
537 set_elem.appendChild(set_name);
538 if (description != null) {
539 Element set_description = doc.createElement(SET_DESCRIPTION);
540 GSXML.setNodeText(set_description, description);
541 set_elem.appendChild(set_description);
542 }
543 return set_elem;
544
545 }
546
547 /** returns the resumptionToken element to go into an OAI response */
548 public static Element createResumptionTokenElement(Document doc, String token_name, int total_size, int cursor, long expiration_time) {
549 Element token = doc.createElement(OAIXML.RESUMPTION_TOKEN);
550 if (total_size != -1) {
551 token.setAttribute(OAIXML.COMPLETE_LIST_SIZE, "" + total_size);
552 }
553 if (cursor != -1) {
554 token.setAttribute(OAIXML.CURSOR, "" + cursor);
555 }
556 if(expiration_time !=-1) {
557 token.setAttribute(OAIXML.EXPIRATION_DATE, getTime(expiration_time));
558 }
559
560 if (token != null) {
561 GSXML.setNodeText(token, token_name);
562 }
563 return token;
564 }
565
566}
567
568
569
570
571
572
Note: See TracBrowser for help on using the repository browser.