source: trunk/gli/src/org/greenstone/gatherer/msm/MetadataSet.java@ 7609

Last change on this file since 7609 was 7535, checked in by mdewsnip, 20 years ago

Removed unused constructor.

  • Property svn:keywords set to Author Date Id Revision
File size: 25.6 KB
Line 
1/**
2 *#########################################################################
3 *
4 * A component of the Gatherer application, part of the Greenstone digital
5 * library suite from the New Zealand Digital Library Project at the
6 * University of Waikato, New Zealand.
7 *
8 * <BR><BR>
9 *
10 * Author: John Thompson, Greenstone Digital Library, University of Waikato
11 *
12 * <BR><BR>
13 *
14 * Copyright (C) 1999 New Zealand Digital Library Project
15 *
16 * <BR><BR>
17 *
18 * This program is free software; you can redistribute it and/or modify
19 * it under the terms of the GNU General Public License as published by
20 * the Free Software Foundation; either version 2 of the License, or
21 * (at your option) any later version.
22 *
23 * <BR><BR>
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * <BR><BR>
31 *
32 * You should have received a copy of the GNU General Public License
33 * along with this program; if not, write to the Free Software
34 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
35 *########################################################################
36 */
37package org.greenstone.gatherer.msm;
38
39import java.io.*;
40import java.net.*;
41import java.util.*;
42import org.apache.xerces.dom.*;
43import org.greenstone.gatherer.Dictionary;
44import org.greenstone.gatherer.Gatherer;
45import org.greenstone.gatherer.valuetree.GValueModel;
46import org.greenstone.gatherer.valuetree.GValueNode;
47import org.greenstone.gatherer.util.StaticStrings;
48import org.greenstone.gatherer.util.Utility;
49import org.w3c.dom.*;
50/** An semi-data class to hold details about a loaded metadata set, it also provides some methods to manipulating the data within.
51 * @author John Thompson, Greenstone Digital Library, University of Waikato
52 * @version 2.3b
53 */
54public class MetadataSet {
55 /** The <Strong>Document</strong> of the DOM model. */
56 private Document document = null;
57 /** The document <strong>Element</strong> of the DOM model. */
58 private Element root = null;
59 /** The <strong>File</strong> this metadata set was loaded from. */
60 private File file = null;
61 /** A mapping from metadata elements to the root element of the value trees for that element. */
62 private Hashtable value_trees = null;
63 /** The list of metadata elements which are, of course, children of the root node. */
64 private NodeList elements = null;
65
66 private String current_language_code;
67 /** The description of this metadata set. Cached as it takes more computation time. */
68 private String description = null;
69 /** The name of this metadata set. Cached as it takes more computation time. */
70 private String name = null;
71 /** An element of the tree pruning filter enumeration, that indicates all nodes in the tree should be retained. */
72 static final int ALL_VALUES = 1;
73 /** An element of the tree pruning filter enumeration, that indicates only metadata Subject nodes or higher should remain after pruning. */
74 static final int SUBJECTS_ONLY = 2;
75 /** An element of the tree pruning filter enumeration, that indicates no value nodes i.e. the entire AssignedValues subtree, should remain after pruning. */
76 static final int NO_VALUES = 3;
77
78 public MetadataSet(String metadata_template) {
79 URL url = ClassLoader.getSystemResource(metadata_template);
80 try {
81 init(new File(URLDecoder.decode(url.getFile(), "UTF-8")));
82 }
83 catch(UnsupportedEncodingException exception) {
84 Gatherer.printStackTrace(exception);
85 }
86 }
87
88 /** Constructor.
89 * @param file The file the metadata set should be loaded from.
90 */
91 public MetadataSet(File file) {
92 init(file);
93 }
94
95 /** Copy constructor.
96 * @param original The original metadata set to copy from.
97 */
98 public MetadataSet(MetadataSet original) {
99 this.value_trees = new Hashtable();
100 // We have to create a new document.
101 document = new DocumentImpl(original.getDocument().getDoctype());
102 root = (Element) document.importNode(original.getDocument().getDocumentElement(), true);
103 document.appendChild(root);
104 elements = root.getElementsByTagName("Element");
105 file = original.getFile();
106 // Now for each element read in its value tree if present.
107 for(int i = elements.getLength() - 1; i >= 0; i--) {
108 ElementWrapper value_element_wrapper = new ElementWrapper((Element)elements.item(i));
109 GValueModel value_tree = original.getValueTree(value_element_wrapper);
110 Document value_document = value_tree.getDocument();
111 Document value_document_copy = new DocumentImpl(value_document.getDoctype());
112 Element value_element = value_document.getDocumentElement();
113 Element value_element_copy = (Element) value_document_copy.importNode(value_element, true);
114 value_document_copy.appendChild(value_element_copy);
115 GValueModel value_tree_copy = new GValueModel(value_element_wrapper, value_document_copy);
116 value_trees.put(value_element_wrapper, value_tree_copy);
117 }
118 }
119 /** Conditional copy constructor.
120 * @param original The original metadata set to copy from.
121 * @param condition An <i>int</i> which matches one of the tree pruning filter types.
122 */
123 public MetadataSet(MetadataSet original, int condition) {
124 this(original);
125 // Now based on condition, we may have to remove some nodes from
126 // this model.
127 switch(condition) {
128 case ALL_VALUES:
129 // Do nothing.
130 break;
131 case SUBJECTS_ONLY:
132 // For each element retrieve its AssignedValues element.
133 for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
134 ElementWrapper value_element = (ElementWrapper)keys.nextElement();
135 GValueModel value_tree = (GValueModel)value_trees.get(value_element);
136 Document value_tree_document = value_tree.getDocument();
137 Element value_tree_root_element = value_tree_document.getDocumentElement();
138 // Traverse tree and remove leaf nodes.
139 MSMUtils.traverseTree(value_tree_root_element, MSMUtils.NONE, true);
140 }
141 break;
142 case NO_VALUES:
143 // Remove assigned values trees.
144 value_trees.clear();
145 break;
146 }
147 }
148
149 /** Add a mds level attribute.
150 * @param name The name of the attribute to add as a <Strong>String</strong>.
151 * @param value The value as a <strong>String</strong>.
152 */
153 public void addAttribute(String name, String value) {
154 root.setAttribute(name, value);
155 }
156
157 /** Add a new default metadata element with the given name to this metadata set.
158 * @param name The name of this element as a <strong>String</strong>.
159 * @return An <strong>ElementWrapper</strong> around the newly created element or null if the element was not created.
160 */
161 public ElementWrapper addElement(String name, String language)
162 {
163 Text text = document.createTextNode(name);
164 Element identifier = document.createElementNS("","Attribute");
165 identifier.setAttribute("name","identifier");
166 identifier.setAttribute("language", language);
167 identifier.appendChild(text);
168 Element element = document.createElementNS("","Element");
169 element.setAttribute("name",name);
170 element.appendChild(identifier);
171 root.appendChild(element);
172 return new ElementWrapper(element);
173 }
174
175 /** Method to add a new metadata element to this metadata set, if and only if the element is not already present.
176 * @param others_element An <strong>Element</strong> we wish to add to this metadata set, that currently belongs to some other set.
177 * @param model A <strong>GValueModel</strong> value tree
178 * @return <i>null</i> if the add is successful, otherwise a <strong>String</strong> containing an error message (phrase key).
179 */
180 public String addElement(Element others_element, GValueModel model) {
181 if(!containsElement(others_element.getAttribute("name"))) {
182 // First get ownership of the new element, then add it.
183 Element our_element = (Element)document.importNode(others_element, true);
184 // add the value tree
185 root.appendChild(our_element);
186 if (model != null) {
187 addValueTree(new ElementWrapper(our_element), model);
188 }
189 return null;
190 }
191 else {
192 return "MSMPrompt.Name_Exists";
193 }
194 }
195
196 /** Method to add a new metadata element with the specified new name to this metadata set, if and only if the name is not already in use.
197 * @param others_element An <strong>Element</strong> we wish to add to this metadata set, that currently belongs to some other set.
198 * @param new_name The new name to be given this element, as a <strong>String</strong>.
199 * @param model A <strong>GValueModel</strong> value tree
200 * @return <i>null</i> if the add is successful, otherwise a <strong>String</strong> containing an error message (phrase key).
201 */
202 public String addElement(Element others_element, String new_name, GValueModel model) {
203 if(!containsElement(new_name)) {
204 // First get ownership of the new element, then add it.
205 Element our_element =
206 (Element) document.importNode(others_element, true);
207 // Change name
208 our_element.setAttribute("name", new_name);
209 // we also want to change the english identifier of this element
210 MSMUtils.setIdentifier(our_element, new_name);
211 // Add it to teh set
212 root.appendChild(our_element);
213 // add the value tree
214 if (model != null) {
215 addValueTree(new ElementWrapper(our_element), model);
216 }
217 return null;
218 }
219 else {
220 return "MSMPrompt.Name_Exists";
221 }
222 }
223 /** Add a value tree to a given metadata element.
224 * @param element The <strong>ElementWrapper</strong> containing the element you wish to add a value tree for.
225 * @param model A <strong>GValueModel</strong> value tree
226 */
227 public void addValueTree(ElementWrapper element, GValueModel model) {
228 ///ystem.err.println("Adding value tree for " + element.toString());
229 value_trees.put(element, model);
230 }
231
232 public int compare(Element e1, Element e2) {
233 int result = 0;
234 // Check that they're not the same element.
235 if(e1 != e2) {
236 int index_e1 = -1;
237 int index_e2 = -1;
238 // Locate the indexes for each element.
239 for(int i = 0; i < elements.getLength(); i++) {
240 Node element = elements.item(i);
241 if(element == e1) {
242 index_e1 = i;
243 }
244 if(element == e2) {
245 index_e2 = i;
246 }
247 }
248 if(index_e1 < index_e2) {
249 result = -1;
250 }
251 else {
252 result = 1;
253 }
254 }
255 return result;
256 }
257
258 /** A method to determine if this metadata set contains an element with a certain name (case sensitive).
259 * @param name A <strong>String</strong> which is the name of the element whose presence we are checking.
260 * @return A <i>boolean</i> which is <i>true</i> if the named element exists, <i>false</i> otherwise.
261 */
262 public boolean containsElement(String name) {
263 for(int i = 0; i < elements.getLength(); i++) {
264 Element sibling = (Element) elements.item(i);
265 String sibling_name = sibling.getAttribute("name");
266 if(sibling_name.equals(name)) {
267 return true;
268 }
269 }
270 return false;
271 }
272
273 public NamedNodeMap getAttributes() {
274 return root.getAttributes();
275 }
276
277 /** Method to retrieve the contact address of the metadata set creator.
278 * @return A <strong>String</strong> containing the address.
279 */
280 /* private String getContact() {
281 return root.getAttribute("contact");
282 } */
283 /** Method to retrieve the name of the creator of this metadata set.
284 * @return A <strong>String</strong> containing the name.
285 */
286 public String getCreator() {
287 return root.getAttribute("creator");
288 }
289 /** Method to retrieve the description of this metadata set. Note that this is language specific, so we determine the desired language from the Dictionary. If no such entry exists, first try returning the english version and failing that the first description found.
290 * @return The description as a <strong>String</strong>.
291 */
292 public String getDescription() {
293 if(current_language_code != null && !Gatherer.config.getLanguage().equals(current_language_code)) {
294 description = null;
295 }
296 if(description == null) {
297 description = getAttribute(StaticStrings.DESCRIPTION_ELEMENT, Dictionary.get("MSM.No_Description"));
298 }
299 return description;
300 }
301
302 /** Method to retrieve the <strong>Document</strong> associated with this metadata set.
303 * @return The <strong>Document</strong> representing this metadata set.
304 */
305 public Document getDocument() {
306 return document;
307 }
308 /** Method to retrieve the metadata element indicated by an index.
309 * @param index An <i>int</i> specifying the required element.
310 * @return The <strong>Element</strong> at the index.
311 */
312 public Element getElement(int index) {
313 return (Element)elements.item(index);
314 }
315 /** This method is used to acquire a reference to the element which matches the given metadata. Note that this is not the same as <i>metadata.getElement()</i> as the reference returned by it may now be obsolete.
316 * @param metadata A <strong>Metadata</strong> object representing an element and value assignment.
317 * @return A 'live' reference to an <strong>Element</strong> which is the same as that referenced by the given metadata, or <i>null</i> if there is no such element.
318 */
319 public Element getElement(Metadata metadata) {
320 return metadata.getElement().getElement();
321 }
322 /** This method is used to acquire a reference to the element which has the name specified. Note that this is not the same as <i>metadata.getElement()</i> as the reference returned by it may now be obsolete.
323 * @param name A <strong>String</strong> stating the desired objects name.
324 * @return A 'live' reference to an <strong>Element</strong> which is the same as that referenced by the given metadata, or <i>null</i> if there is no such element.
325 */
326 public Element getElement(String name) {
327 // Strip any namespace.
328 while(name.indexOf(".") != -1 && !name.equals(".")) {
329 name = name.substring(name.indexOf(".") + 1);
330 }
331 ///ystem.err.println("Get element named " + name);
332 for(int i = 0; i < elements.getLength(); i++) {
333 Element element = (Element) elements.item(i);
334 ///ystem.err.println("Compare to: " + element.getAttribute("name"));
335 if(element.getAttribute("name").equals(name)) {
336 return element;
337 }
338 }
339 return null;
340 }
341
342 public Element getElement(Element parent_element, String name) {
343 Gatherer.println("Get element named " + name + " from " + parent_element.getAttribute("name"));
344 NodeList elements = parent_element.getElementsByTagName("Element");
345 for(int i = 0; i < elements.getLength(); i++) {
346 Element element = (Element) elements.item(i);
347 if(element.getAttribute("name").equals(name) && element.getParentNode() == parent_element) {
348 elements = null;
349 return element;
350 }
351 }
352 elements = null;
353 return null;
354 }
355 /** Method to acquire a list of all the elements in this metadata set.
356 * @return A <strong>NodeList</strong> containing all of this sets elements.
357 */
358 public NodeList getElements() {
359 return elements;
360 }
361
362 /** Method to retrieve a list of all the elements in this metadata set, sorted.
363 * @return A <strong>Vector</strong> containing all of the elements of this sets, sorted.
364 */
365 public Vector getElementsSorted() {
366 Vector elements_list = new Vector();
367 for (int i = 0; i < elements.getLength(); i++) {
368 elements_list.add(new ElementWrapper((Element) elements.item(i)));
369 }
370 Collections.sort(elements_list, MSMUtils.METADATA_COMPARATOR);
371 return elements_list;
372 }
373
374 /** Method to retrieve the original file this metadata set was created from.
375 * @return A <strong>File</strong>.
376 */
377 public File getFile() {
378 return file;
379 }
380 /** Get the last changed attribute.
381 * @return Last changed as a <strong>String</strong>.
382 */
383 public String getLastChanged() {
384 return root.getAttribute("lastchanged");
385 }
386 /** Method to get this metadata sets name. Note that this is language specific, so we determine the desired language from the Dictionary. If no such entry exists, first try returning the english version and failing that the first name found.
387 * @return A <strong>String</strong> which contains its name.
388 */
389 public String getName() {
390 if(current_language_code != null && !Gatherer.config.getLanguage().equals(current_language_code)) {
391 name = null;
392 }
393 if(name == null) {
394 name = getAttribute(StaticStrings.NAME_ELEMENT, Dictionary.get("MSM.No_Name"));
395 }
396 return name;
397 }
398 /** Method to retrieve this metadata sets namespace.
399 * @return The namespace as a <strong>String</strong>.
400 */
401 public String getNamespace() {
402 return root.getAttribute("namespace");
403 }
404 /** Method to retrieve the root element, i.e. the Document Element, of the DOM model behind this metadata set.
405 * @return An <strong>Element</strong> which is at the root of the modal.
406 */
407 public Element getRoot() {
408 return root;
409 }
410 /** Retrieve the value tree from this set that matches the given element.
411 * @param element The target <strong>ElementWrapper</strong>.
412 * @return A <strong>GValueModel</strong> value tree, or <i>null</i> if no such element or value tree.
413 */
414 public GValueModel getValueTree(ElementWrapper element) {
415 GValueModel value_tree = null;
416 // Stinking hashtable get doesn't use the overridden equals. So I'll do a loop, which should be pretty small ie O(n) for n metadata elements.
417 for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
418 ElementWrapper sibling = (ElementWrapper) keys.nextElement();
419 if(sibling.equals(element)) {
420 value_tree = (GValueModel) value_trees.get(sibling);
421 break;
422 }
423 }
424 // If we've found no value tree, create a new one.
425 if(value_tree == null) {
426 value_tree = new GValueModel(element);
427 value_trees.put(element, value_tree);
428 }
429 return value_tree;
430 }
431 /** Remove a mds level attribute.
432 * @param name The name of the attribute to remove.
433 */
434 public void removeAttribute(String name) {
435 root.removeAttribute(name);
436 }
437 /** Method to remove the given element from this metadata set.
438 * @param element The <strong>Element</strong> to be removed.
439 */
440 public void removeElement(Element element) {
441 // we need to remove the value tree too!!
442 removeValueTree(new ElementWrapper(element));
443 root.removeChild(element);
444 }
445 /** Used to remove the value tree for a specific element.
446 * @param element The <strong>ElementWrapper</strong> whose tree you wish to remove.
447 * @return The <strong>GValueModel</strong> we just removed
448 */
449 public GValueModel removeValueTree(ElementWrapper element) {
450 for(Enumeration keys = value_trees.keys(); keys.hasMoreElements(); ) {
451 ElementWrapper sibling = (ElementWrapper) keys.nextElement();
452 if(sibling.equals(element)) {
453 GValueModel value_tree = (GValueModel) value_trees.get(sibling);
454 value_trees.remove(sibling);
455 return value_tree;
456 }
457 }
458 return null;
459 }
460 /** Set one of the mds level attributes.
461 * @param name The attribute to change.
462 * @param value its new value.
463 */
464 public void setAttribute(String name, String value) {
465 root.setAttribute(name, value);
466 }
467 /** Once the metadata set has been saved to a different location, this is used to update the file parameter.
468 * @param file The new location of this metadata set <strong>File</strong>.
469 */
470 public void setFile(File file) {
471 this.file = file;
472 }
473
474 public void setName(String name) {
475 // Retrieve the name element. We look for the first english one.
476 Element name_element = null;
477 Element metadataset_element = document.getDocumentElement();
478 NodeList name_elements = metadataset_element.getElementsByTagName(Utility.NAME_ELEMENT);
479 for(int i = 0; i < name_elements.getLength(); i++) {
480 Element possible_name_element = (Element) name_elements.item(i);
481 if(possible_name_element.getAttribute(Utility.LANGUAGE_ATTRIBUTE).equals(Utility.ENGLISH_VALUE)) {
482 // Found it.
483 name_element = possible_name_element;
484 }
485 }
486 // If there is none add one. Note that we can only add english metadata sets. Although others can edit them to add further names as necessary.
487 if(name_element == null) {
488 name_element = document.createElement(Utility.NAME_ELEMENT);
489 name_element.setAttribute(Utility.LANGUAGE_ATTRIBUTE, Utility.ENGLISH_VALUE);
490 metadataset_element.insertBefore(name_element, metadataset_element.getFirstChild());
491 }
492 // Replace the text node
493 while(name_element.hasChildNodes()) {
494 name_element.removeChild(name_element.getFirstChild());
495 }
496 name_element.appendChild(document.createTextNode(name));
497 }
498
499 /** Method to determine the number of elements in this set.
500 * @return An <i>int</i> specifying the element count.
501 */
502 public int size() {
503 return elements.getLength();
504 }
505 /** Method to translate this class into a meaningful string, which in this case is the metadata sets name.
506 * @return The metadata sets name as a <strong>String</strong>.
507 */
508 public String toString() {
509 String name = getName();
510 // If there is no given name, then use the namespace as there is garaunteed to be one of them.
511 if(name == null || name.length() == 0) {
512 name = root.getAttribute("namespace");
513 }
514 // Append namespace
515 String namespace = root.getAttribute("namespace");
516 if(namespace == null || namespace.equals("")) {
517 namespace = Utility.EXTRACTED_METADATA_NAMESPACE;
518 }
519 name = name + " (" + namespace + ")";
520 return name;
521 }
522
523 /** This method retrieves the required attribute from the Metadata Set, typically it's name or it's description. Note that this method is language dependant, and moreover supports both legacy metadata sets and the new sets optimized for multiple languages.
524 * @param element_name the name of the type of element the required information is in as a String
525 * @param default_string the value to return if no such element is found also as a String
526 * @see org.greenstone.gatherer.Configuration#getLanguage()
527 * @see org.greenstone.gatherer.Gatherer#config
528 * @see org.greenstone.gatherer.msm.MSMUtils#getValue(Node)
529 * @see org.greenstone.gatherer.util.StaticStrings#CODE_ATTRIBUTE
530 * @see org.greenstone.gatherer.util.StaticStrings#SETLANGUAGE_ELEMENT
531 */
532 private String getAttribute(String element_name, String default_string) {
533 String result = null;
534
535 // Determine the language code.
536 current_language_code = Gatherer.config.getLanguage();
537
538 ///ystem.err.println("Searching for the " + element_name + " in " + current_language_code);
539
540 // New Metadata Set Format makes use of deferred-node-expansion to save memory - rather than create nodes for a name and description in each language, nodes which have potentially huge strings, we instead create simplier SETLANGUAGE nodes, and then only expand the one in the desired language. Of course if a user happens to change to every available language slightly more memory will be used than in the old method. For instance consider the DLS with 25 languages, each with a name node of 50 bytes and an descriptions of 500. Thus old style > 13750 bytes while new style < 600.
541 NodeList set_language_elements = document.getElementsByTagName(StaticStrings.SETLANGUAGE_ELEMENT);
542 for(int b = 0; b < set_language_elements.getLength(); b++) {
543 Element set_language_element = (Element) set_language_elements.item(b);
544 String code = set_language_element.getAttribute(StaticStrings.CODE_ATTRIBUTE).toLowerCase();
545 if(code.equals(current_language_code)) {
546 NodeList specific_elements = set_language_element.getElementsByTagName(element_name);
547 if(specific_elements.getLength() > 0) {
548 Element specific_element = (Element) specific_elements.item(0);
549 result = MSMUtils.getValue(specific_element);
550 specific_element = null;
551 }
552 specific_elements = null;
553 }
554 code = null;
555 set_language_element = null;
556 }
557 set_language_elements = null;
558 // And we may be all done
559 if(result != null) {
560 return result;
561 }
562
563 // Failing that we move on to an older style search - start by recovering all Name elements
564 NodeList possible_elements = document.getElementsByTagName(element_name);
565 // Iterate through the available names looking for the appropriate one. Also make note of the first name, then overwrite it with any english one.
566 boolean found = false;
567 for(int i = 0; !found && i < possible_elements.getLength(); i++) {
568 Element possible_element = (Element) possible_elements.item(i);
569 String possible_element_code = possible_element.getAttribute("language").toLowerCase();
570 if(possible_element_code.equals(current_language_code) || name == null) {
571 result = MSMUtils.getValue(possible_element);
572 found = true;
573 }
574 possible_element_code = null;
575 possible_element = null;
576 }
577 possible_elements = null;
578 // Failing all that set an error message
579 if(result == null) {
580 result = default_string;
581 }
582 return result;
583 }
584
585 private void init(File file) {
586 this.file = file;
587 this.value_trees = new Hashtable();
588 this.document = Utility.parse(file, false);
589 if(document != null) {
590 this.elements = document.getElementsByTagName("Element");
591 this.root = document.getDocumentElement();
592 // Now for each element read in its value tree if present.
593 for(int i = elements.getLength() - 1; i >= 0; i--) {
594 ElementWrapper value_element = new ElementWrapper((Element)elements.item(i));
595 File value_file = new File(file.getParentFile(), value_element.getName() + ".mdv");
596 ///ystem.err.println("Searching for " + value_file.getAbsolutePath());
597 if(value_file.exists()) {
598 Document value_document = Utility.parse(value_file, false);
599 if(value_document != null) {
600 value_trees.put(value_element, new GValueModel(value_element, value_document));
601 }
602 else {
603 Gatherer.println("Error! Missing mdv file: " + value_file.getAbsolutePath());
604 }
605 }
606 }
607 }
608 else {
609 Gatherer.println("Error! Missing mds file: " + file.getAbsolutePath());
610 }
611 }
612}
Note: See TracBrowser for help on using the repository browser.