1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.ole.sys.service.impl;
17
18 import java.io.File;
19 import java.io.FileNotFoundException;
20 import java.io.PrintStream;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26
27 import org.apache.commons.lang.StringUtils;
28 import org.kuali.ole.sys.OLEConstants;
29 import org.kuali.ole.sys.Message;
30 import org.kuali.ole.sys.batch.service.WrappingBatchService;
31 import org.kuali.ole.sys.context.SpringContext;
32 import org.kuali.ole.sys.report.BusinessObjectReportHelper;
33 import org.kuali.ole.sys.service.ReportWriterService;
34 import org.kuali.rice.core.api.datetime.DateTimeService;
35 import org.kuali.rice.krad.bo.BusinessObject;
36 import org.kuali.rice.krad.util.ObjectUtils;
37
38
39
40
41
42
43
44
45
46 public class ReportWriterTextServiceImpl implements ReportWriterService, WrappingBatchService {
47 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReportWriterTextServiceImpl.class);
48
49
50
51 protected static final int INITIAL_LINE_NUMBER = 0;
52
53 protected String filePath;
54 protected String fileNamePrefix;
55 protected String fileNameSuffix;
56 protected String title;
57 protected int pageWidth;
58 protected int pageLength;
59 protected int initialPageNumber;
60 protected String errorSubTitle;
61 protected String statisticsLabel;
62 protected String statisticsLeftPadding;
63 private String parametersLabel;
64 private String parametersLeftPadding;
65 protected String pageLabel;
66 protected String newLineCharacter;
67 protected DateTimeService dateTimeService;
68 protected boolean aggregationModeOn;
69
70
71
72
73
74 protected Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames;
75
76
77 protected Map<Class<? extends BusinessObject>, BusinessObjectReportHelper> businessObjectReportHelpers;
78
79 protected PrintStream printStream;
80 protected int page;
81 protected int line = INITIAL_LINE_NUMBER;
82 protected String errorFormat;
83
84
85
86
87 protected boolean modeStatistics = false;
88
89
90
91
92 protected boolean modeParameters = false;
93
94
95 protected boolean newPage = true;
96
97
98 protected Class<? extends BusinessObject> businessObjectClass;
99
100 public ReportWriterTextServiceImpl() {
101
102 }
103
104
105
106
107 public void initialize() {
108 try {
109 printStream = new PrintStream(generateFullFilePath());
110 }
111 catch (FileNotFoundException e) {
112 throw new RuntimeException(e);
113 }
114
115 page = initialPageNumber;
116 initializeBusinessObjectReportHelpers();
117
118 this.writeHeader(title);
119 }
120
121 protected void initializeBusinessObjectReportHelpers() {
122 businessObjectReportHelpers = new HashMap<Class<? extends BusinessObject>, BusinessObjectReportHelper>();
123 if (classToBusinessObjectReportHelperBeanNames != null) {
124 for (Class<? extends BusinessObject> clazz : classToBusinessObjectReportHelperBeanNames.keySet()) {
125 String businessObjectReportHelperBeanName = classToBusinessObjectReportHelperBeanNames.get(clazz);
126 BusinessObjectReportHelper reportHelper = (BusinessObjectReportHelper) SpringContext.getService(businessObjectReportHelperBeanName);
127 if (ObjectUtils.isNull(reportHelper)) {
128 LOG.error("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
129 throw new RuntimeException("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
130 }
131 businessObjectReportHelpers.put(clazz, reportHelper);
132 }
133 }
134 }
135
136 protected String generateFullFilePath() {
137 if (aggregationModeOn) {
138 return filePath + File.separator + this.fileNamePrefix + fileNameSuffix;
139 }
140 else {
141 return filePath + File.separator + this.fileNamePrefix + dateTimeService.toDateTimeStringForFilename(dateTimeService.getCurrentDate()) + fileNameSuffix;
142 }
143 }
144
145
146
147
148 public void destroy() {
149 if(printStream != null) {
150 printStream.close();
151 printStream = null;
152 }
153
154
155 page = initialPageNumber;
156 line = INITIAL_LINE_NUMBER;
157 modeStatistics = false;
158 modeParameters = false;
159 newPage = true;
160 businessObjectClass = null;
161 }
162
163
164
165
166 public void writeSubTitle(String message) {
167 if (message.length() > pageWidth) {
168 LOG.warn("sub title to be written exceeds pageWidth. Printing anyway.");
169 this.writeFormattedMessageLine(message);
170 }
171 else {
172 int padding = (pageWidth - message.length()) / 2;
173 this.writeFormattedMessageLine("%" + (padding + message.length()) + "s", message);
174 }
175 }
176
177
178
179
180 public void writeError(BusinessObject businessObject, Message message) {
181 this.writeError(businessObject, message, true);
182 }
183
184
185
186
187
188 public void writeError(BusinessObject businessObject, Message message, boolean printBusinessObjectValues) {
189
190
191 if (newPage || businessObjectClass == null || !businessObjectClass.getName().equals(businessObject.getClass().getName())) {
192 if (businessObjectClass == null) {
193
194 this.writeSubTitle(errorSubTitle);
195 }
196 else if (!businessObjectClass.getName().equals(businessObject.getClass().getName())) {
197
198 this.writeNewLines(1);
199 }
200
201 this.writeErrorHeader(businessObject);
202 newPage = false;
203 businessObjectClass = businessObject.getClass();
204 }
205
206
207 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
208
209
210 List<Object> formatterArgs = new ArrayList<Object>();
211 if (printBusinessObjectValues) {
212 formatterArgs.addAll(businessObjectReportHelper.getValues(businessObject));
213 }
214 else {
215 formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
216 }
217
218
219 int maxMessageLength = Integer.parseInt(StringUtils.substringBefore(StringUtils.substringAfterLast(errorFormat, "%-"), "s"));
220 String messageToPrint = message.getMessage();
221
222 boolean firstMessageLine = true;
223 while (messageToPrint.length() > 0 && StringUtils.isNotBlank(messageToPrint)) {
224 if (!firstMessageLine) {
225 formatterArgs = new ArrayList<Object>();
226 formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
227 }
228 else {
229 firstMessageLine = false;
230 }
231
232 messageToPrint = StringUtils.trim(messageToPrint);
233 String messageLine = messageToPrint;
234 if (messageLine.length() > maxMessageLength) {
235 messageLine = StringUtils.substring(messageLine, 0, maxMessageLength);
236 if (StringUtils.contains(messageLine, " ")) {
237 messageLine = StringUtils.substringBeforeLast(messageLine, " ");
238 }
239 }
240
241 formatterArgs.add(new Message(messageLine, message.getType()));
242 this.writeFormattedMessageLine(errorFormat, formatterArgs.toArray());
243
244 messageToPrint = StringUtils.removeStart(messageToPrint, messageLine);
245 }
246 }
247
248
249
250
251 public void writeError(BusinessObject businessObject, List<Message> messages) {
252 int i = 0;
253 for (Iterator<Message> messagesIter = messages.iterator(); messagesIter.hasNext();) {
254 Message message = messagesIter.next();
255
256 if (i == 0) {
257
258 this.writeError(businessObject, message, true);
259 }
260 else {
261
262 this.writeError(businessObject, message, false);
263 }
264
265 i++;
266 }
267 }
268
269
270
271
272 public void writeNewLines(int lines) {
273 for (int i = 0; i < lines; i++) {
274 this.writeFormattedMessageLine("");
275 }
276 }
277
278
279
280
281 public void writeStatisticLine(String message, Object... args) {
282
283 if (!modeStatistics) {
284 this.modeStatistics = true;
285
286
287 if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
288 this.pageBreak();
289 }
290
291 this.writeFormattedMessageLine("*********************************************************************************************************************************");
292 this.writeFormattedMessageLine("*********************************************************************************************************************************");
293 this.writeFormattedMessageLine("*******************" + statisticsLabel + "*******************");
294 this.writeFormattedMessageLine("*********************************************************************************************************************************");
295 this.writeFormattedMessageLine("*********************************************************************************************************************************");
296 }
297
298 this.writeFormattedMessageLine(statisticsLeftPadding + message, args);
299 }
300
301
302
303
304 public void writeParameterLine(String message, Object... args) {
305
306 if (!modeParameters) {
307 this.modeParameters = true;
308
309
310 if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
311 this.pageBreak();
312 }
313
314 this.writeFormattedMessageLine("*********************************************************************************************************************************");
315 this.writeFormattedMessageLine("*********************************************************************************************************************************");
316 this.writeFormattedMessageLine("*******************" + getParametersLabel() + "*******************");
317 this.writeFormattedMessageLine("*********************************************************************************************************************************");
318 this.writeFormattedMessageLine("*********************************************************************************************************************************");
319 }
320
321 this.writeFormattedMessageLine(getParametersLeftPadding() + message, args);
322 }
323
324
325
326
327 public void writeFormattedMessageLine(String format) {
328 this.writeFormattedMessageLine(format, new Object());
329 }
330
331
332
333
334 public void writeFormattedMessageLine(String format, Object... args) {
335 if (format.indexOf("% s") > -1) {
336 LOG.warn("Cannot properly format: "+format);
337 }
338 else {
339 Object[] escapedArgs = escapeArguments(args);
340 if (LOG.isDebugEnabled()) {
341 LOG.debug("writeFormattedMessageLine, format: "+format);
342 }
343
344 String message = null;
345
346 if (escapedArgs.length > 0) {
347 message = String.format(format + newLineCharacter, escapedArgs);
348 } else {
349 message = format+newLineCharacter;
350 }
351
352
353
354 if (message.length() > pageWidth) {
355 if (LOG.isDebugEnabled()) {
356 LOG.debug("message is out of bounds writing anyway");
357 }
358 }
359
360 printStream.print(message);
361 printStream.flush();
362
363 line++;
364 if (line >= pageLength) {
365 this.pageBreak();
366 }
367 }
368 }
369
370
371
372
373
374
375 protected boolean allFormattingEscaped(String format) {
376 int currPoint = 0;
377 int currIndex = format.indexOf('%', currPoint);
378 while (currIndex > -1) {
379 char nextChar = format.charAt(currIndex+1);
380 if (nextChar != '%') {
381 return false;
382 }
383 currPoint = currIndex + 2;
384 }
385 return true;
386 }
387
388
389
390
391 public void pageBreak() {
392
393
394 printStream.printf("%c" + newLineCharacter, 12);
395 page++;
396 line = INITIAL_LINE_NUMBER;
397 newPage = true;
398
399 this.writeHeader(title);
400 }
401
402
403
404
405
406
407 protected void writeHeader(String title) {
408 String headerText = String.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM", dateTimeService.getCurrentDate());
409 int reportTitlePadding = pageWidth / 2 - headerText.length() - title.length() / 2;
410 headerText = String.format("%s%" + (reportTitlePadding + title.length()) + "s%" + reportTitlePadding + "s", headerText, title, "");
411
412 if (aggregationModeOn) {
413 this.writeFormattedMessageLine("%s%s%s", headerText, pageLabel, OLEConstants.REPORT_WRITER_SERVICE_PAGE_NUMBER_PLACEHOLDER);
414 }
415 else {
416 this.writeFormattedMessageLine("%s%s%,9d", headerText, pageLabel, page);
417 }
418 this.writeNewLines(1);
419 }
420
421
422
423
424
425
426 protected void writeErrorHeader(BusinessObject businessObject) {
427 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
428 List<String> errorHeader = businessObjectReportHelper.getTableHeader(pageWidth);
429
430
431 if (errorHeader.size() + line >= pageLength) {
432 this.pageBreak();
433 }
434
435
436 for (Iterator<String> headers = errorHeader.iterator(); headers.hasNext();) {
437 String header = headers.next();
438
439 if (headers.hasNext()) {
440 this.writeFormattedMessageLine("%s", header);
441 }
442 else {
443 errorFormat = header;
444 }
445 }
446 }
447
448
449
450
451 public void writeTableHeader(BusinessObject businessObject) {
452 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
453
454 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
455 String tableHeaderFormat = tableDefinition.get(OLEConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
456
457 String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
458 this.writeMultipleFormattedMessageLines(headerLines);
459 }
460
461
462
463
464
465 public void writeTableHeader(Class<? extends BusinessObject> businessObjectClass) {
466 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjectClass);
467
468 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
469 String tableHeaderFormat = tableDefinition.get(OLEConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
470
471 String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
472 this.writeMultipleFormattedMessageLines(headerLines);
473 }
474
475
476
477
478 public void writeTableRowSeparationLine(BusinessObject businessObject) {
479 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
480 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
481
482 String separationLine = tableDefinition.get(OLEConstants.ReportConstants.SEPARATOR_LINE_KEY);
483 this.writeFormattedMessageLine(separationLine);
484 }
485
486
487
488
489 public void writeTableRow(BusinessObject businessObject) {
490 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
491 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
492
493 String tableCellFormat = tableDefinition.get(OLEConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);
494 List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);
495
496 String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
497 this.writeMultipleFormattedMessageLines(rowMessageLines);
498 }
499
500
501
502
503 public void writeTableRowWithColspan(BusinessObject businessObject) {
504 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
505 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
506
507 String tableCellFormat = businessObjectReportHelper.getTableCellFormat(true, true, StringUtils.EMPTY);
508 List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, true);
509
510 String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
511 this.writeMultipleFormattedMessageLines(rowMessageLines);
512 }
513
514
515
516
517 public void writeTable(List<? extends BusinessObject> businessObjects, boolean isHeaderRepeatedInNewPage, boolean isRowBreakAcrossPageAllowed) {
518 if (ObjectUtils.isNull(businessObjects) || businessObjects.isEmpty()) {
519 return;
520 }
521
522 BusinessObject firstBusinessObject = businessObjects.get(0);
523 this.writeTableHeader(firstBusinessObject);
524
525 BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjects.get(0));
526 Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
527 String tableHeaderFormat = tableDefinition.get(OLEConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
528 String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
529
530 String tableCellFormat = tableDefinition.get(OLEConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);
531
532 for (BusinessObject businessObject : businessObjects) {
533
534 List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);
535 String[] messageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
536
537 boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
538 if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
539 this.pageBreak();
540
541 if (isHeaderRepeatedInNewPage) {
542 this.writeTableHeader(firstBusinessObject);
543 }
544 }
545
546 this.writeMultipleFormattedMessageLines(messageLines, headerLines, isRowBreakAcrossPageAllowed);
547 }
548
549 }
550
551
552
553
554
555
556
557 public BusinessObjectReportHelper getBusinessObjectReportHelper(BusinessObject businessObject) {
558 if (LOG.isDebugEnabled()) {
559 if (businessObject == null) {
560 LOG.debug("reporting "+filePath+" but can't because null business object sent in");
561 } else if (businessObjectReportHelpers == null) {
562 LOG.debug("Logging "+businessObject+" in report "+filePath+" but businessObjectReportHelpers are null");
563 }
564 }
565 BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObject.getClass());
566 if (ObjectUtils.isNull(businessObjectReportHelper)) {
567 throw new RuntimeException(businessObject.getClass().toString() + " is not handled");
568 }
569
570 return businessObjectReportHelper;
571 }
572
573
574
575
576
577
578
579 public BusinessObjectReportHelper getBusinessObjectReportHelper(Class<? extends BusinessObject> businessObjectClass) {
580 BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObjectClass);
581 if (ObjectUtils.isNull(businessObjectReportHelper)) {
582 throw new RuntimeException(businessObjectClass.getName() + " is not handled");
583 }
584
585 return businessObjectReportHelper;
586 }
587
588
589
590
591
592
593
594
595 protected void writeMultipleFormattedMessageLines(String[] messageLines, String[] headerLinesInNewPage, boolean isRowBreakAcrossPageAllowed) {
596 int currentPageNumber = this.page;
597
598 for (String line : messageLines) {
599 boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
600 if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
601 this.pageBreak();
602 }
603
604 if (currentPageNumber < this.page && ObjectUtils.isNotNull(headerLinesInNewPage)) {
605 currentPageNumber = this.page;
606
607 for (String headerLine : headerLinesInNewPage) {
608 this.writeFormattedMessageLine(headerLine);
609 }
610 }
611
612 this.writeFormattedMessageLine(line);
613 }
614 }
615
616
617
618
619
620
621
622 public void writeMultipleFormattedMessageLines(String[] messageLines) {
623 this.writeMultipleFormattedMessageLines(messageLines, null, false);
624 }
625
626 public void writeMultipleFormattedMessageLines(String format, Object... args) {
627 Object[] escapedArgs = escapeArguments(args);
628 String[] messageLines = getMultipleFormattedMessageLines(format, escapedArgs);
629 writeMultipleFormattedMessageLines(messageLines);
630 }
631
632
633
634
635
636
637
638
639 public String[] getMultipleFormattedMessageLines(String format, Object... args) {
640 Object[] escapedArgs = escapeArguments(args);
641 String message = String.format(format, escapedArgs);
642 return StringUtils.split(message, newLineCharacter);
643 }
644
645
646
647
648
649
650
651 protected Object[] escapeArguments(Object... args) {
652 Object[] escapedArgs = new Object[args.length];
653 for (int i = 0; i < args.length; i++) {
654 Object arg = args[i];
655 if (arg == null) {
656 args[i] = "";
657 } else if (arg != null && arg instanceof String) {
658 String escapedArg = escapeFormatCharacters((String)arg);
659 escapedArgs[i] = escapedArg;
660 }
661 else {
662 escapedArgs[i] = arg;
663 }
664 }
665
666 return escapedArgs;
667 }
668
669
670
671
672
673
674
675
676 protected String escapeFormatCharacters(String replacementString) {
677 String escapedString = replacementString;
678 for (int i = 0; i < OLEConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS.length; i++) {
679 String characterToEscape = OLEConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS[i];
680 escapedString = StringUtils.replace(escapedString, characterToEscape, characterToEscape + characterToEscape);
681 }
682
683 return escapedString;
684 }
685
686
687
688
689
690
691 public void setFilePath(String filePath) {
692 this.filePath = filePath;
693 }
694
695
696
697
698
699
700 public void setFileNamePrefix(String fileNamePrefix) {
701 this.fileNamePrefix = fileNamePrefix;
702 }
703
704
705
706
707
708
709 public void setFileNameSuffix(String fileNameSuffix) {
710 this.fileNameSuffix = fileNameSuffix;
711 }
712
713
714
715
716
717
718 public void setTitle(String title) {
719 this.title = title;
720 }
721
722
723
724
725
726
727 public void setPageWidth(int pageWidth) {
728 this.pageWidth = pageWidth;
729 }
730
731
732
733
734
735
736 public void setPageLength(int pageLength) {
737 this.pageLength = pageLength;
738 }
739
740
741
742
743
744
745 public void setInitialPageNumber(int initialPageNumber) {
746 this.initialPageNumber = initialPageNumber;
747 }
748
749
750
751
752
753
754 public void setErrorSubTitle(String errorSubTitle) {
755 this.errorSubTitle = errorSubTitle;
756 }
757
758
759
760
761
762
763 public void setStatisticsLabel(String statisticsLabel) {
764 this.statisticsLabel = statisticsLabel;
765 }
766
767
768
769
770
771
772 public void setStatisticsLeftPadding(String statisticsLeftPadding) {
773 this.statisticsLeftPadding = statisticsLeftPadding;
774 }
775
776
777
778
779
780
781 public void setPageLabel(String pageLabel) {
782 this.pageLabel = pageLabel;
783 }
784
785
786
787
788
789
790 public void setNewLineCharacter(String newLineCharacter) {
791 this.newLineCharacter = newLineCharacter;
792 }
793
794
795
796
797
798
799 public void setDateTimeService(DateTimeService dateTimeService) {
800 this.dateTimeService = dateTimeService;
801 }
802
803
804
805
806
807
808
809 public void setClassToBusinessObjectReportHelperBeanNames(Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames) {
810 this.classToBusinessObjectReportHelperBeanNames = classToBusinessObjectReportHelperBeanNames;
811 }
812
813
814
815
816
817 public String getParametersLabel() {
818 return parametersLabel;
819 }
820
821
822
823
824
825 public void setParametersLabel(String parametersLabel) {
826 this.parametersLabel = parametersLabel;
827 }
828
829
830
831
832
833 public String getParametersLeftPadding() {
834 return parametersLeftPadding;
835 }
836
837
838
839
840
841 public void setParametersLeftPadding(String parametersLeftPadding) {
842 this.parametersLeftPadding = parametersLeftPadding;
843 }
844
845
846
847
848
849 public boolean isAggregationModeOn() {
850 return aggregationModeOn;
851 }
852
853
854
855
856
857 public void setAggregationModeOn(boolean aggregationModeOn) {
858 this.aggregationModeOn = aggregationModeOn;
859 }
860
861
862 }