1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.configuration;
18
19 import java.io.Reader;
20 import java.io.StringReader;
21 import java.io.StringWriter;
22 import java.util.Iterator;
23
24 import org.apache.commons.configuration.event.ConfigurationEvent;
25
26 import junit.framework.TestCase;
27
28 /***
29 * Test class for PropertiesConfigurationLayout.
30 *
31 * @author <a
32 * href="http://jakarta.apache.org/commons/configuration/team-list.html">Commons
33 * Configuration team</a>
34 * @version $Id: TestPropertiesConfigurationLayout.java 439648 2006-09-02 20:42:10Z oheger $
35 */
36 public class TestPropertiesConfigurationLayout extends TestCase
37 {
38 /*** Constant for the line break character. */
39 static final String CR = System.getProperty("line.separator");
40
41 /*** Constant for a test property key. */
42 static final String TEST_KEY = "myProperty";
43
44 /*** Constant for a test comment. */
45 static final String TEST_COMMENT = "A comment for my test property";
46
47 /*** Constant for a test property value. */
48 static final String TEST_VALUE = "myPropertyValue";
49
50 /*** The layout object under test. */
51 PropertiesConfigurationLayout layout;
52
53 /*** The associated configuration object. */
54 LayoutTestConfiguration config;
55
56 /*** A properties builder that can be used for testing. */
57 PropertiesBuilder builder;
58
59 protected void setUp() throws Exception
60 {
61 super.setUp();
62 config = new LayoutTestConfiguration();
63 layout = new PropertiesConfigurationLayout(config);
64 config.setLayout(layout);
65 builder = new PropertiesBuilder();
66 }
67
68 /***
69 * Tests a newly created instance.
70 */
71 public void testInit()
72 {
73 assertTrue("Object contains keys", layout.getKeys().isEmpty());
74 assertNull("Header comment not null", layout.getHeaderComment());
75 Iterator it = config.getConfigurationListeners().iterator();
76 assertTrue("No event listener registered", it.hasNext());
77 assertSame("Layout not registered as event listener", layout, it.next());
78 assertFalse("Multiple event listeners registered", it.hasNext());
79 assertSame("Configuration not stored", config, layout
80 .getConfiguration());
81 assertFalse("Force single line flag set", layout.isForceSingleLine());
82 }
83
84 /***
85 * Tests creating a layout object with a null configuration. This should
86 * cause an exception.
87 */
88 public void testInitNull()
89 {
90 try
91 {
92 new PropertiesConfigurationLayout(null);
93 fail("Could create instance with null config!");
94 }
95 catch (IllegalArgumentException iex)
96 {
97
98 }
99 }
100
101 /***
102 * Tests reading a simple properties file.
103 */
104 public void testReadSimple() throws ConfigurationException
105 {
106 builder.addComment(TEST_COMMENT);
107 builder.addProperty(TEST_KEY, TEST_VALUE);
108 layout.load(builder.getReader());
109 assertNull("A header comment was found", layout.getHeaderComment());
110 assertEquals("Wrong number of properties", 1, layout.getKeys().size());
111 assertTrue("Property not found", layout.getKeys().contains(TEST_KEY));
112 assertEquals("Comment not found", TEST_COMMENT, layout
113 .getCanonicalComment(TEST_KEY, false));
114 assertEquals("Wrong number of blanc lines", 0, layout
115 .getBlancLinesBefore(TEST_KEY));
116 assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
117 assertEquals("Property not stored in config", TEST_VALUE, config
118 .getString(TEST_KEY));
119 }
120
121 /***
122 * Tests whether blanc lines before a property are correctly detected.
123 */
124 public void testBlancLines() throws ConfigurationException
125 {
126 builder.addProperty("prop", "value");
127 builder.addComment(null);
128 builder.addComment(null);
129 builder.addComment(TEST_COMMENT);
130 builder.addComment(null);
131 builder.addProperty(TEST_KEY, TEST_VALUE);
132 layout.load(builder.getReader());
133 assertEquals("Wrong number of blanc lines", 2, layout
134 .getBlancLinesBefore(TEST_KEY));
135 assertEquals("Wrong comment", TEST_COMMENT + CR, layout
136 .getCanonicalComment(TEST_KEY, false));
137 assertEquals("Wrong property value", TEST_VALUE, config
138 .getString(TEST_KEY));
139 }
140
141 /***
142 * Tests the single line flag for a simple property definition.
143 */
144 public void testIsSingleLine() throws ConfigurationException
145 {
146 builder.addProperty(TEST_KEY, TEST_VALUE + "," + TEST_VALUE + "2");
147 layout.load(builder.getReader());
148 assertTrue("Wrong single line flag", layout.isSingleLine(TEST_KEY));
149 assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
150 .size());
151 }
152
153 /***
154 * Tests the single line flag if there are multiple property definitions.
155 */
156 public void testIsSingleLineMulti() throws ConfigurationException
157 {
158 builder.addProperty(TEST_KEY, TEST_VALUE);
159 builder.addProperty("anotherProp", "a value");
160 builder.addProperty(TEST_KEY, TEST_VALUE + "2");
161 layout.load(builder.getReader());
162 assertFalse("Wrong single line flag", layout.isSingleLine(TEST_KEY));
163 assertEquals("Wrong number of values", 2, config.getList(TEST_KEY)
164 .size());
165 }
166
167 /***
168 * Tests whether comments are combined for multiple occurrences.
169 */
170 public void testCombineComments() throws ConfigurationException
171 {
172 builder.addComment(TEST_COMMENT);
173 builder.addProperty(TEST_KEY, TEST_VALUE);
174 builder.addComment(null);
175 builder.addComment(TEST_COMMENT);
176 builder.addProperty(TEST_KEY, TEST_VALUE + "2");
177 layout.load(builder.getReader());
178 assertEquals("Wrong combined comment",
179 TEST_COMMENT + CR + TEST_COMMENT, layout.getCanonicalComment(
180 TEST_KEY, false));
181 assertEquals("Wrong combined blanc numbers", 0, layout
182 .getBlancLinesBefore(TEST_KEY));
183 }
184
185 /***
186 * Tests if a header comment is detected.
187 */
188 public void testHeaderComment() throws ConfigurationException
189 {
190 builder.addComment(TEST_COMMENT);
191 builder.addComment(null);
192 builder.addProperty(TEST_KEY, TEST_VALUE);
193 layout.load(builder.getReader());
194 assertEquals("Wrong header comment", TEST_COMMENT, layout
195 .getCanonicalHeaderComment(false));
196 assertNull("Wrong comment for property", layout.getCanonicalComment(
197 TEST_KEY, false));
198 }
199
200 /***
201 * Tests if a header comment containing blanc lines is correctly detected.
202 */
203 public void testHeaderCommentWithBlancs() throws ConfigurationException
204 {
205 builder.addComment(TEST_COMMENT);
206 builder.addComment(null);
207 builder.addComment(TEST_COMMENT);
208 builder.addComment(null);
209 builder.addProperty(TEST_KEY, TEST_VALUE);
210 layout.load(builder.getReader());
211 assertEquals("Wrong header comment", TEST_COMMENT + CR + CR
212 + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
213 assertNull("Wrong comment for property", layout.getComment(TEST_KEY));
214 }
215
216 /***
217 * Tests if a header comment is correctly detected when it contains blanc
218 * lines and the first property has a comment, too.
219 */
220 public void testHeaderCommentWithBlancsAndPropComment()
221 throws ConfigurationException
222 {
223 builder.addComment(TEST_COMMENT);
224 builder.addComment(null);
225 builder.addComment(TEST_COMMENT);
226 builder.addComment(null);
227 builder.addComment(TEST_COMMENT);
228 builder.addProperty(TEST_KEY, TEST_VALUE);
229 layout.load(builder.getReader());
230 assertEquals("Wrong header comment", TEST_COMMENT + CR + CR
231 + TEST_COMMENT, layout.getCanonicalHeaderComment(false));
232 assertEquals("Wrong comment for property", TEST_COMMENT, layout
233 .getCanonicalComment(TEST_KEY, false));
234 }
235
236 /***
237 * Tests fetching a canonical header comment when no comment is set.
238 */
239 public void testHeaderCommentNull()
240 {
241 assertNull("No null comment with comment chars", layout
242 .getCanonicalHeaderComment(true));
243 assertNull("No null comment without comment chars", layout
244 .getCanonicalHeaderComment(false));
245 }
246
247 /***
248 * Tests if a property add event is correctly processed.
249 */
250 public void testEventAdd()
251 {
252 ConfigurationEvent event = new ConfigurationEvent(this,
253 AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
254 false);
255 layout.configurationChanged(event);
256 assertTrue("Property not stored", layout.getKeys().contains(TEST_KEY));
257 assertEquals("Blanc lines before new property", 0, layout
258 .getBlancLinesBefore(TEST_KEY));
259 assertTrue("No single line property", layout.isSingleLine(TEST_KEY));
260 }
261
262 /***
263 * Tests adding a property multiple time through an event. The property
264 * should then be a multi-line property.
265 */
266 public void testEventAddMultiple()
267 {
268 ConfigurationEvent event = new ConfigurationEvent(this,
269 AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
270 false);
271 layout.configurationChanged(event);
272 layout.configurationChanged(event);
273 assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
274 }
275
276 /***
277 * Tests if an add event is correctly processed if the affected property is
278 * already stored in the layout object.
279 */
280 public void testEventAddExisting() throws ConfigurationException
281 {
282 builder.addComment(TEST_COMMENT);
283 builder.addProperty(TEST_KEY, TEST_VALUE);
284 layout.load(builder.getReader());
285 ConfigurationEvent event = new ConfigurationEvent(this,
286 AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
287 false);
288 layout.configurationChanged(event);
289 assertFalse("No multi-line property", layout.isSingleLine(TEST_KEY));
290 assertEquals("Comment was modified", TEST_COMMENT, layout
291 .getCanonicalComment(TEST_KEY, false));
292 }
293
294 /***
295 * Tests if a set property event for a non existing property is correctly
296 * handled.
297 */
298 public void testEventSetNonExisting()
299 {
300 ConfigurationEvent event = new ConfigurationEvent(this,
301 AbstractConfiguration.EVENT_SET_PROPERTY, TEST_KEY, TEST_VALUE,
302 false);
303 layout.configurationChanged(event);
304 assertTrue("New property was not found", layout.getKeys().contains(
305 TEST_KEY));
306 }
307
308 /***
309 * Tests if a property delete event is correctly processed.
310 */
311 public void testEventDelete()
312 {
313 ConfigurationEvent event = new ConfigurationEvent(this,
314 AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
315 false);
316 layout.configurationChanged(event);
317 event = new ConfigurationEvent(this,
318 AbstractConfiguration.EVENT_CLEAR_PROPERTY, TEST_KEY, null,
319 false);
320 layout.configurationChanged(event);
321 assertFalse("Property still existing", layout.getKeys().contains(
322 TEST_KEY));
323 }
324
325 /***
326 * Tests if a clear event is correctly processed.
327 */
328 public void testEventClearConfig() throws ConfigurationException
329 {
330 fillLayout();
331 ConfigurationEvent event = new ConfigurationEvent(this,
332 AbstractConfiguration.EVENT_CLEAR, null, null, false);
333 layout.configurationChanged(event);
334 assertTrue("Keys not empty", layout.getKeys().isEmpty());
335 assertNull("Header comment was not reset", layout.getHeaderComment());
336 }
337
338 /***
339 * Tests if a before update event is correctly ignored.
340 */
341 public void testEventAddBefore()
342 {
343 ConfigurationEvent event = new ConfigurationEvent(this,
344 AbstractConfiguration.EVENT_ADD_PROPERTY, TEST_KEY, TEST_VALUE,
345 true);
346 layout.configurationChanged(event);
347 assertFalse("Property already stored", layout.getKeys().contains(
348 TEST_KEY));
349 }
350
351 /***
352 * Tests if a reload update is correctly processed.
353 */
354 public void testEventReload()
355 {
356 fillLayout();
357 ConfigurationEvent event = new ConfigurationEvent(this,
358 AbstractFileConfiguration.EVENT_RELOAD, null, null, true);
359 layout.configurationChanged(event);
360 assertTrue("Keys not empty", layout.getKeys().isEmpty());
361 assertNull("Header comment was not reset", layout.getHeaderComment());
362 }
363
364 /***
365 * Tests the event after a reload has been performed. This should be
366 * ignored.
367 */
368 public void testEventReloadAfter()
369 {
370 fillLayout();
371 ConfigurationEvent event = new ConfigurationEvent(this,
372 AbstractFileConfiguration.EVENT_RELOAD, null, null, false);
373 layout.configurationChanged(event);
374 assertFalse("Keys are empty", layout.getKeys().isEmpty());
375 assertNotNull("Header comment was reset", layout.getHeaderComment());
376 }
377
378 /***
379 * Tests a recursive load call.
380 */
381 public void testRecursiveLoadCall() throws ConfigurationException
382 {
383 PropertiesBuilder b = new PropertiesBuilder();
384 b.addComment("A nested header comment.");
385 b.addComment("With multiple lines");
386 b.addComment(null);
387 b.addComment("Second comment");
388 b.addProperty(TEST_KEY, TEST_VALUE);
389 b.addProperty(TEST_KEY + "2", TEST_VALUE + "2");
390 config.builder = b;
391
392 builder.addComment("Header comment");
393 builder.addComment(null);
394 builder.addComment(TEST_COMMENT);
395 builder.addProperty(TEST_KEY, TEST_VALUE);
396 builder.addComment("Include file");
397 builder.addProperty(PropertiesConfiguration.getInclude(), "test");
398
399 layout.load(builder.getReader());
400
401 assertEquals("Wrong header comment", "Header comment", layout
402 .getCanonicalHeaderComment(false));
403 assertFalse("Include property was stored", layout.getKeys().contains(
404 PropertiesConfiguration.getInclude()));
405 assertEquals("Wrong comment for property", TEST_COMMENT + CR
406 + "A nested header comment." + CR + "With multiple lines" + CR
407 + CR + "Second comment", layout.getCanonicalComment(TEST_KEY,
408 false));
409 }
410
411 /***
412 * Tests whether the output of the layout object is identical to the source
413 * file (at least for simple properties files).
414 */
415 public void testReadAndWrite() throws ConfigurationException
416 {
417 builder.addComment("This is my test properties file,");
418 builder.addComment("which contains a header comment.");
419 builder.addComment(null);
420 builder.addComment(TEST_COMMENT);
421 builder.addProperty(TEST_KEY, TEST_COMMENT);
422 builder.addComment(null);
423 builder.addComment(null);
424 builder.addComment("Another comment");
425 builder.addProperty("property", "and a value");
426 layout.load(builder.getReader());
427 checkLayoutString(builder.toString());
428 }
429
430 /***
431 * Tests if the content of the layout object is correctly written.
432 */
433 public void testSave() throws ConfigurationException
434 {
435 config.addProperty(TEST_KEY, TEST_VALUE);
436 layout.setComment(TEST_KEY, TEST_COMMENT);
437 config.addProperty(TEST_KEY, TEST_VALUE + "2");
438 config.addProperty("AnotherProperty", "AnotherValue");
439 config.addProperty("AnotherProperty", "3rdValue");
440 layout.setComment("AnotherProperty", "AnotherComment");
441 layout.setBlancLinesBefore("AnotherProperty", 2);
442 layout.setSingleLine("AnotherProperty", true);
443 layout.setHeaderComment("A header comment" + CR + "for my properties");
444 checkLayoutString("# A header comment" + CR + "# for my properties"
445 + CR + CR + "# " + TEST_COMMENT + CR + TEST_KEY + " = "
446 + TEST_VALUE + CR + TEST_KEY + " = " + TEST_VALUE + "2" + CR
447 + CR + CR + "# AnotherComment" + CR
448 + "AnotherProperty = AnotherValue,3rdValue" + CR);
449 }
450
451 /***
452 * Tests the force single line flag.
453 */
454 public void testSaveForceSingleLine() throws ConfigurationException
455 {
456 config.setListDelimiter(';');
457 config.addProperty(TEST_KEY, TEST_VALUE);
458 config.addProperty(TEST_KEY, TEST_VALUE + "2");
459 config.addProperty("AnotherProperty", "value1;value2;value3");
460 layout.setComment(TEST_KEY, TEST_COMMENT);
461 layout.setForceSingleLine(true);
462 checkLayoutString("# " + TEST_COMMENT + CR + TEST_KEY + " = "
463 + TEST_VALUE + ';' + TEST_VALUE + "2" + CR
464 + "AnotherProperty = value1;value2;value3" + CR);
465 }
466
467 /***
468 * Tests the trimComment method.
469 */
470 public void testTrimComment()
471 {
472 assertEquals("Wrong trimmed comment", "This is a comment" + CR
473 + "that spans multiple" + CR + "lines in a" + CR
474 + " complex way.", PropertiesConfigurationLayout.trimComment(
475 " # This is a comment" + CR + "that spans multiple" + CR
476 + "!lines in a" + CR + " complex way.", false));
477 }
478
479 /***
480 * Tests trimming a comment with trailing CRs.
481 */
482 public void testTrimCommentTrainlingCR()
483 {
484 assertEquals("Wrong trimmed comment", "Comment with" + CR
485 + "trailing CR" + CR, PropertiesConfigurationLayout
486 .trimComment("Comment with" + CR + "! trailing CR" + CR, false));
487 }
488
489 /***
490 * Tests enforcing comment characters in a comment.
491 */
492 public void testTrimCommentFalse()
493 {
494 assertEquals("Wrong trimmed comment", "# Comment with" + CR
495 + " ! some mixed " + CR + "#comment" + CR + "# lines",
496 PropertiesConfigurationLayout.trimComment("Comment with" + CR
497 + " ! some mixed " + CR + "#comment" + CR + "lines",
498 true));
499 }
500
501 /***
502 * Tests accessing data for a property, which is not stored.
503 */
504 public void testGetNonExistingLayouData()
505 {
506 assertNull("A comment was found", layout.getComment("unknown"));
507 assertTrue("A multi-line property", layout.isSingleLine("unknown"));
508 assertEquals("Leading blanc lines", 0, layout
509 .getBlancLinesBefore("unknown"));
510 }
511
512 /***
513 * Tests accessing a property with a null key. This should throw an
514 * exception.
515 */
516 public void testGetNullLayouttData()
517 {
518 try
519 {
520 layout.setComment(null, TEST_COMMENT);
521 fail("Could access null property key!");
522 }
523 catch (IllegalArgumentException iex)
524 {
525
526 }
527 }
528
529 /***
530 * Tests resetting a comment.
531 */
532 public void testSetNullComment()
533 {
534 fillLayout();
535 layout.setComment(TEST_KEY, null);
536 assertNull("Comment was not reset", layout.getComment(TEST_KEY));
537 }
538
539 /***
540 * Tests saving when a comment for a non existing property is contained in
541 * the layout object. This comment should be ignored.
542 */
543 public void testSaveCommentForUnexistingProperty()
544 throws ConfigurationException
545 {
546 fillLayout();
547 layout.setComment("NonExistingKey", "NonExistingComment");
548 String output = getLayoutString();
549 assertTrue("Non existing key was found", output
550 .indexOf("NonExistingKey") < 0);
551 assertTrue("Non existing comment was found", output
552 .indexOf("NonExistingComment") < 0);
553 }
554
555 /***
556 * Tests saving an empty layout object.
557 */
558 public void testSaveEmptyLayout() throws ConfigurationException
559 {
560 checkLayoutString("");
561 }
562
563 /***
564 * Tests the copy constructor.
565 */
566 public void testInitCopy()
567 {
568 fillLayout();
569 PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(
570 config, layout);
571 assertEquals("Wrong number of keys", layout.getKeys().size(), l2
572 .getKeys().size());
573 for (Iterator it = layout.getKeys().iterator(); it.hasNext();)
574 {
575 Object key = it.next();
576 assertTrue("Key was not found: " + key, l2.getKeys().contains(key));
577 }
578 }
579
580 /***
581 * Tests if the copy and the original are independend from each other.
582 */
583 public void testInitCopyModify()
584 {
585 fillLayout();
586 PropertiesConfigurationLayout l2 = new PropertiesConfigurationLayout(
587 config, layout);
588 assertEquals("Comments are not equal", layout.getComment(TEST_KEY), l2
589 .getComment(TEST_KEY));
590 layout.setComment(TEST_KEY, "A new comment");
591 assertEquals("Comment was changed", TEST_COMMENT, l2
592 .getCanonicalComment(TEST_KEY, false));
593 l2.setBlancLinesBefore(TEST_KEY, l2.getBlancLinesBefore(TEST_KEY) + 1);
594 assertFalse("Blanc lines do not differ", layout
595 .getBlancLinesBefore(TEST_KEY) == l2
596 .getBlancLinesBefore(TEST_KEY));
597 }
598
599 /***
600 * Helper method for filling the layout object with some properties.
601 */
602 private void fillLayout()
603 {
604 builder.addComment("A header comment");
605 builder.addComment(null);
606 builder.addProperty("prop", "value");
607 builder.addComment(TEST_COMMENT);
608 builder.addProperty(TEST_KEY, TEST_VALUE);
609 builder.addProperty("anotherProp", "anotherValue");
610 try
611 {
612 layout.load(builder.getReader());
613 }
614 catch (ConfigurationException cex)
615 {
616
617 fail("Exception was thrown: " + cex);
618 }
619 }
620
621 /***
622 * Writes the layout's data into a string.
623 *
624 * @return the layout file's content as string
625 * @throws ConfigurationException if an error occurs
626 */
627 private String getLayoutString() throws ConfigurationException
628 {
629 StringWriter out = new StringWriter();
630 layout.save(out);
631 return out.toString();
632 }
633
634 /***
635 * Checks if the layout's output is correct.
636 *
637 * @param expected the expected result
638 * @throws ConfigurationException if an error occurs
639 */
640 private void checkLayoutString(String expected)
641 throws ConfigurationException
642 {
643 assertEquals("Wrong layout file content", expected, getLayoutString());
644 }
645
646 /***
647 * A helper class used for constructing test properties files.
648 */
649 static class PropertiesBuilder
650 {
651 /*** A buffer for storing the data. */
652 private StringBuffer buf = new StringBuffer();
653
654 /*** A counter for varying the comment character. */
655 private int commentCounter;
656
657 /***
658 * Adds a property to the simulated file.
659 *
660 * @param key the property key
661 * @param value the value
662 */
663 public void addProperty(String key, String value)
664 {
665 buf.append(key).append(" = ").append(value).append(CR);
666 }
667
668 /***
669 * Adds a comment line.
670 *
671 * @param s the comment (can be <b>null</b>, then a blanc line is
672 * added)
673 */
674 public void addComment(String s)
675 {
676 if (s != null)
677 {
678 if (commentCounter % 2 == 0)
679 {
680 buf.append("# ");
681 }
682 else
683 {
684 buf.append("! ");
685 }
686 buf.append(s);
687 }
688 buf.append(CR);
689 }
690
691 /***
692 * Returns a reader for the simulated properties.
693 *
694 * @return a reader
695 */
696 public Reader getReader()
697 {
698 return new StringReader(buf.toString());
699 }
700
701 /***
702 * Returns a string representation of the buffer's content.
703 *
704 * @return the buffer as string
705 */
706 public String toString()
707 {
708 return buf.toString();
709 }
710 }
711
712 /***
713 * A mock properties configuration implementation that is used to check
714 * whether some expected methods are called.
715 */
716 static class LayoutTestConfiguration extends PropertiesConfiguration
717 {
718 /*** Stores a builder object. */
719 public PropertiesBuilder builder;
720
721 /***
722 * Simulates the propertyLoaded() callback. If a builder was set, a
723 * load() call on the layout is invoked.
724 */
725 boolean propertyLoaded(String key, String value)
726 throws ConfigurationException
727 {
728 if (builder == null)
729 {
730 return super.propertyLoaded(key, value);
731 }
732 else
733 {
734 if (PropertiesConfiguration.getInclude().equals(key))
735 {
736 getLayout().load(builder.getReader());
737 return false;
738 }
739 else
740 {
741 return true;
742 }
743 }
744 }
745 }
746 }