1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.atlassian.theplugin.idea.crucible;
18
19
20 import com.atlassian.theplugin.commons.bamboo.StausIconBambooListener;
21 import com.atlassian.theplugin.commons.crucible.*;
22 import com.atlassian.theplugin.commons.crucible.api.model.*;
23 import com.atlassian.theplugin.commons.util.Logger;
24 import com.atlassian.theplugin.configuration.ProjectConfigurationBean;
25 import com.atlassian.theplugin.idea.IdeaHelper;
26 import com.atlassian.theplugin.idea.ProgressAnimationProvider;
27 import com.atlassian.theplugin.idea.ThePluginProjectComponent;
28 import com.atlassian.theplugin.idea.crucible.events.ShowReviewEvent;
29 import com.atlassian.theplugin.idea.crucible.comments.CrucibleReviewActionListener;
30 import com.atlassian.theplugin.idea.bamboo.ToolWindowBambooContent;
31 import com.atlassian.theplugin.idea.ui.CollapsibleTable;
32 import com.atlassian.theplugin.idea.ui.TableColumnProvider;
33 import com.atlassian.theplugin.idea.ui.TableItemSelectedListener;
34 import com.atlassian.theplugin.util.PluginUtil;
35 import com.intellij.openapi.actionSystem.ActionGroup;
36 import com.intellij.openapi.actionSystem.ActionManager;
37 import com.intellij.openapi.actionSystem.ActionToolbar;
38 import com.intellij.openapi.ui.VerticalFlowLayout;
39 import com.intellij.openapi.util.Key;
40 import com.intellij.openapi.project.*;
41 import com.intellij.openapi.project.Project;
42 import com.intellij.ui.table.TableView;
43 import com.intellij.util.ui.ListTableModel;
44 import com.intellij.util.ui.UIUtil;
45 import thirdparty.javaworld.ClasspathHTMLEditorKit;
46
47 import javax.swing.*;
48 import java.awt.*;
49 import java.util.*;
50 import java.util.List;
51
52 public class CrucibleTableToolWindowPanel extends JPanel implements CrucibleStatusListener, TableItemSelectedListener,
53 CrucibleReviewActionListener {
54
55 private static final Key<CrucibleTableToolWindowPanel> WINDOW_PROJECT_KEY = Key.create(CrucibleTableToolWindowPanel.class.getName());
56 private Project project;
57 private transient ActionToolbar filterEditToolbar;
58 private static CrucibleTableToolWindowPanel instance;
59 private TableColumnProvider columnProvider;
60
61 private CrucibleCustomFilterPanel crucibleCustomFilterPanel;
62
63 private ProjectConfigurationBean projectConfiguration;
64 private JPanel toolBarPanel;
65 private JPanel dataPanelsHolder;
66 private ToolWindowBambooContent editorPane;
67
68 public CrucibleFiltersBean getFilters() {
69 return filters;
70 }
71
72 private transient CrucibleFiltersBean filters;
73
74 protected JScrollPane tablePane;
75 protected ListTableModel listTableModel;
76 protected static final Dimension ED_PANE_MINE_SIZE = new Dimension(200, 200);
77 protected ProgressAnimationProvider progressAnimation = new ProgressAnimationProvider();
78
79 protected TableColumnProvider tableColumnProvider = new CrucibleTableColumnProviderImpl();
80
81 private ReviewDataInfoAdapter selectedItem;
82
83 private Map<PredefinedFilter, CollapsibleTable> tables = new HashMap<PredefinedFilter, CollapsibleTable>();
84 private Map<String, CollapsibleTable> customTables = new HashMap<String, CollapsibleTable>();
85 private CollapsibleTable crucible15Table = null;
86
87 private static CrucibleServerFacade serverFacade;
88 private CrucibleVersion crucibleVersion = CrucibleVersion.UNKNOWN;
89 private static final String TO_REVIEW_AS_ACTIVE_REVIEWER = "To review as active reviewer";
90
91 protected String getInitialMessage() {
92
93 return "Waiting for Crucible review info.";
94 }
95
96 protected String getToolbarActionGroup() {
97 return "ThePlugin.CrucibleToolWindowToolBar";
98 }
99
100 protected String getPopupActionGroup() {
101 return "ThePlugin.Crucible.ReviewPopupMenu";
102 }
103
104 protected TableColumnProvider getTableColumnProvider() {
105 if (columnProvider == null) {
106 columnProvider = new CrucibleTableColumnProviderImpl();
107 }
108 return columnProvider;
109 }
110
111 public void applyAdvancedFilter() {
112 if (crucibleCustomFilterPanel.getFilter() != null) {
113 CustomFilterBean filter = crucibleCustomFilterPanel.getFilter();
114
115 filters.getManualFilter().put(filter.getTitle(), filter);
116 projectConfiguration.
117 getCrucibleConfiguration().getCrucibleFilters().getManualFilter().put(filter.getTitle(), filter);
118 CrucibleStatusChecker checker = null;
119 ThePluginProjectComponent projectComponent = IdeaHelper.getCurrentProjectComponent();
120 if (projectComponent != null) {
121 checker = projectComponent.getCrucibleStatusChecker();
122 }
123 refreshReviews(checker);
124 }
125 hideCrucibleCustomFilter();
126 }
127
128 public void cancelAdvancedFilter() {
129 hideCrucibleCustomFilter();
130 }
131
132
133 public void clearAdvancedFilter() {
134 }
135
136 public static CrucibleTableToolWindowPanel getInstance(com.intellij.openapi.project.Project project, ProjectConfigurationBean projectConfigurationBean) {
137
138 CrucibleTableToolWindowPanel window = project.getUserData(WINDOW_PROJECT_KEY);
139
140 if (window == null) {
141 window = new CrucibleTableToolWindowPanel(project, projectConfigurationBean);
142 project.putUserData(WINDOW_PROJECT_KEY, window);
143 serverFacade = CrucibleServerFacadeImpl.getInstance();
144 }
145 return window;
146 }
147
148 public CrucibleTableToolWindowPanel(Project project, ProjectConfigurationBean projectConfigurationBean) {
149 super(new BorderLayout());
150 this.project = project;
151 this.projectConfiguration = projectConfigurationBean;
152
153 setBackground(UIUtil.getTreeTextBackground());
154
155 toolBarPanel = new JPanel(new BorderLayout());
156 ActionManager actionManager = ActionManager.getInstance();
157 ActionGroup toolbar = (ActionGroup) actionManager.getAction(getToolbarActionGroup());
158 ActionToolbar actionToolbar = actionManager.createActionToolbar(
159 "atlassian.toolwindow.serverToolBar", toolbar, true);
160 toolBarPanel.add(actionToolbar.getComponent(), BorderLayout.NORTH);
161 add(toolBarPanel, BorderLayout.NORTH);
162
163 editorPane = new ToolWindowBambooContent();
164 editorPane.setEditorKit(new ClasspathHTMLEditorKit());
165 JScrollPane pane = setupPane(editorPane, wrapBody(getInitialMessage()));
166 editorPane.setMinimumSize(ED_PANE_MINE_SIZE);
167 add(pane, BorderLayout.SOUTH);
168
169 dataPanelsHolder = new JPanel();
170 dataPanelsHolder.setLayout(new VerticalFlowLayout());
171
172 tablePane = new JScrollPane(dataPanelsHolder,
173 JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
174 tablePane.setWheelScrollingEnabled(true);
175
176 add(tablePane, BorderLayout.CENTER);
177
178 progressAnimation.configure(this, tablePane, BorderLayout.CENTER);
179
180
181 createFilterEditToolBar("atlassian.toolwindow.crucibleFilterEditToolBar", "ThePlugin.Crucible.FilterEditToolBar");
182 this.crucibleCustomFilterPanel = new CrucibleCustomFilterPanel();
183 filters = projectConfiguration.getCrucibleConfiguration().getCrucibleFilters();
184 }
185
186 private void switchToCrucible16Filter() {
187 for (int i = 0; i < projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getPredefinedFilters().length; ++i)
188 {
189 if (crucible15Table != null) {
190 dataPanelsHolder.remove(crucible15Table);
191 crucible15Table = null;
192 }
193 showPredefinedFilter(
194 PredefinedFilter.values()[i],
195 projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getPredefinedFilters()[i],
196 null);
197 }
198
199 for (String s : projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().keySet()) {
200 CustomFilterBean filter = projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().get(s);
201 if (filter.isEnabled()) {
202 this.showCustomFilter(true, null);
203 break;
204 }
205 }
206 }
207
208
209
210
211
212
213
214 public void showPredefinedFilter(PredefinedFilter filter, boolean visible, CrucibleStatusChecker checker) {
215 if (visible) {
216 CollapsibleTable table = new CollapsibleTable(
217 tableColumnProvider,
218 projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
219 filter.getFilterName(),
220 "atlassian.toolwindow.serverToolBar",
221 "ThePlugin.CrucibleReviewToolBar",
222 "Context menu",
223 getPopupActionGroup());
224 table.addItemSelectedListener(this);
225 TableView.restore(projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
226 table.getTable());
227
228 dataPanelsHolder.add(table);
229 tables.put(filter, table);
230
231 refreshReviews(checker);
232 } else {
233 if (tables.containsKey(filter)) {
234 dataPanelsHolder.remove(tables.get(filter));
235 tables.remove(filter);
236 }
237 }
238 dataPanelsHolder.validate();
239 tablePane.repaint();
240 }
241
242 public void switchToCrucible15Filter() {
243 crucible15Table = new CollapsibleTable(
244 tableColumnProvider,
245 projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
246 TO_REVIEW_AS_ACTIVE_REVIEWER,
247 "atlassian.toolwindow.serverToolBar",
248 "ThePlugin.CrucibleReviewToolBar",
249 "Context menu",
250 getPopupActionGroup());
251 crucible15Table.addItemSelectedListener(this);
252 TableView.restore(projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
253 crucible15Table.getTable());
254
255 for (CollapsibleTable table : tables.values()) {
256 dataPanelsHolder.remove(table);
257 }
258 for (CollapsibleTable table : customTables.values()) {
259 dataPanelsHolder.remove(table);
260 }
261
262 tables.clear();
263 customTables.clear();
264 dataPanelsHolder.add(crucible15Table);
265 dataPanelsHolder.validate();
266 tablePane.repaint();
267 }
268
269 protected JScrollPane setupPane(JEditorPane pane, String initialText) {
270 pane.setText(initialText);
271 JScrollPane scrollPane = new JScrollPane(pane,
272 JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
273 scrollPane.setWheelScrollingEnabled(true);
274 return scrollPane;
275 }
276
277 protected String wrapBody(String s) {
278 return "<html>" + StausIconBambooListener.BODY_WITH_STYLE + s + "</body></html>";
279
280 }
281
282 protected void setStatusMessage(String msg) {
283 setStatusMessage(msg, false);
284 }
285
286 protected void setStatusMessage(String msg, boolean isError) {
287 editorPane.setBackground(isError ? Color.RED : Color.WHITE);
288 editorPane.setText(wrapBody("<table width=\"100%\"><tr><td colspan=\"2\">" + msg + "</td></tr></table>"));
289 }
290
291 public ProgressAnimationProvider getProgressAnimation() {
292 return progressAnimation;
293 }
294
295 public CrucibleVersion getCrucibleVersion() {
296 return crucibleVersion;
297 }
298
299
300
301
302 public void updateReviews(Collection<ReviewInfo> reviews) {
303 this.crucibleVersion = CrucibleVersion.CRUCIBLE_15;
304 if (crucible15Table == null) {
305 switchToCrucible15Filter();
306 }
307 List<ReviewDataInfoAdapter> reviewDataInfoAdapters = new ArrayList<ReviewDataInfoAdapter>();
308 for (ReviewInfo review : reviews) {
309 reviewDataInfoAdapters.add(new ReviewDataInfoAdapter(review));
310 }
311
312 crucible15Table.getListTableModel().setItems(reviewDataInfoAdapters);
313 crucible15Table.getListTableModel().fireTableDataChanged();
314 crucible15Table.getTable().revalidate();
315 crucible15Table.getTable().setEnabled(true);
316 crucible15Table.getTable().setForeground(UIUtil.getActiveTextColor());
317 crucible15Table.setTitle(TO_REVIEW_AS_ACTIVE_REVIEWER + " (" + reviewDataInfoAdapters.size() + ")");
318 crucible15Table.expand();
319
320 StringBuffer sb = new StringBuffer();
321 sb.append("Loaded <b>");
322 sb.append(reviews.size());
323 sb.append(" open code reviews</b> for you.");
324 setStatusMessage(sb.toString());
325 }
326
327
328
329
330 public void updateReviews(Map<PredefinedFilter, List<ReviewInfo>> reviews, Map<String, List<ReviewInfo>> customFilterReviews) {
331 this.crucibleVersion = CrucibleVersion.CRUCIBLE_16;
332 if (tables.isEmpty()) {
333 switchToCrucible16Filter();
334 }
335 int reviewCount = 0;
336 for (PredefinedFilter predefinedFilter : reviews.keySet()) {
337 List<ReviewInfo> reviewList = reviews.get(predefinedFilter);
338 if (reviewList != null) {
339 List<ReviewDataInfoAdapter> reviewDataInfoAdapters = new ArrayList<ReviewDataInfoAdapter>();
340 for (ReviewInfo review : reviewList) {
341 reviewDataInfoAdapters.add(new ReviewDataInfoAdapter(review));
342 }
343 CollapsibleTable table = tables.get(predefinedFilter);
344 if (table != null) {
345 table.getListTableModel().setItems(reviewDataInfoAdapters);
346 table.getListTableModel().fireTableDataChanged();
347 table.getTable().revalidate();
348 table.setEnabled(true);
349 table.setForeground(UIUtil.getActiveTextColor());
350 table.setTitle(predefinedFilter.getFilterName() + " (" + reviewList.size() + ")");
351 }
352 reviewCount += reviewList.size();
353 }
354 }
355
356 for (String filterName : customFilterReviews.keySet()) {
357 List<ReviewInfo> reviewList = customFilterReviews.get(filterName);
358 if (reviewList != null) {
359 List<ReviewDataInfoAdapter> reviewDataInfoAdapters = new ArrayList<ReviewDataInfoAdapter>();
360 for (ReviewInfo review : reviewList) {
361 reviewDataInfoAdapters.add(new ReviewDataInfoAdapter(review));
362 }
363 CollapsibleTable table = customTables.get(filterName);
364 if (table != null) {
365 table.getListTableModel().setItems(reviewDataInfoAdapters);
366 table.getListTableModel().fireTableDataChanged();
367 table.getTable().revalidate();
368 table.setEnabled(true);
369 table.setForeground(UIUtil.getActiveTextColor());
370 table.setTitle(filterName + " (" + reviewList.size() + ")");
371 }
372 reviewCount += reviewList.size();
373 }
374 }
375
376
377 StringBuffer sb = new StringBuffer();
378 sb.append("Loaded <b>");
379 sb.append(reviewCount);
380 sb.append(" code reviews</b> for defined filters.");
381 setStatusMessage(sb.toString());
382 }
383
384 public void itemSelected(Object item, int noClicks) {
385 selectedItem = (ReviewDataInfoAdapter) item;
386 if (noClicks == 2) {
387 if (item != null && item instanceof ReviewDataInfoAdapter) {
388 ReviewDataInfoAdapter review = (ReviewDataInfoAdapter) item;
389 IdeaHelper.getReviewActionEventBroker().trigger(new ShowReviewEvent(this, review));
390 }
391 }
392 }
393
394 public void resetState() {
395 }
396
397 public final void hideCrucibleCustomFilter() {
398 setScrollPaneViewport(dataPanelsHolder);
399 filterEditToolbarSetVisible(false);
400 }
401
402 public void collapseAllPanels() {
403 for (CollapsibleTable collapsibleTable : tables.values()) {
404 collapsibleTable.collapse();
405 }
406 for (CollapsibleTable collapsibleTable : customTables.values()) {
407 collapsibleTable.collapse();
408 }
409 }
410
411 public void expandAllPanels() {
412 for (CollapsibleTable collapsibleTable : tables.values()) {
413 collapsibleTable.expand();
414 }
415 for (CollapsibleTable collapsibleTable : customTables.values()) {
416 collapsibleTable.expand();
417 }
418 }
419
420 protected void filterEditToolbarSetVisible(boolean visible) {
421 filterEditToolbar.getComponent().setVisible(visible);
422 }
423
424 protected void createFilterEditToolBar(String place, String toolbarName) {
425 ActionManager actionManager = ActionManager.getInstance();
426 ActionGroup filterEditToolBar = (ActionGroup) actionManager.getAction(toolbarName);
427 filterEditToolbar = actionManager.createActionToolbar(place,
428 filterEditToolBar, true);
429 toolBarPanel.add(filterEditToolbar.getComponent(), BorderLayout.SOUTH);
430 filterEditToolbarSetVisible(false);
431 }
432
433 protected void setScrollPaneViewport(JComponent component) {
434 tablePane.setViewportView(component);
435 }
436
437
438 public PermId getSelectedReviewId() {
439 if (selectedItem != null) {
440 return this.selectedItem.getPermaId();
441 }
442 return null;
443 }
444
445 private void showCrucibleCustomFilterPanel() {
446 filterEditToolbarSetVisible(true);
447 setScrollPaneViewport(crucibleCustomFilterPanel.$$$getRootComponent$$$());
448 }
449
450 public void showCrucibleCustomFilter() {
451 if (!projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().isEmpty()) {
452 for (String filterName : projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().keySet()) {
453 crucibleCustomFilterPanel.setFilter(projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().get(filterName));
454 break;
455 }
456 showCrucibleCustomFilterPanel();
457 } else {
458 addCustomFilter();
459 }
460 }
461
462 public void addCustomFilter() {
463 String newName = FilterNameUtil.suggestNewName(projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter());
464 CustomFilterBean newFilter = new CustomFilterBean();
465 newFilter.setTitle(newName);
466 crucibleCustomFilterPanel.setFilter(newFilter);
467 showCrucibleCustomFilterPanel();
468 }
469
470 public void removeCustomFilter() {
471
472 }
473
474 public void removeItemSelectedListener(TableItemSelectedListener listener) {
475 for (CollapsibleTable table : tables.values()) {
476 table.removeItemSelectedListener(listener);
477 }
478
479
480 for (CollapsibleTable table : customTables.values()) {
481 table.removeItemSelectedListener(listener);
482 }
483 }
484
485
486 public void showCustomFilter(boolean visible, CrucibleStatusChecker checker) {
487 if (!projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().isEmpty()) {
488 for (String filterName : projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().keySet()) {
489 CustomFilterBean filter = projectConfiguration.getCrucibleConfiguration().getCrucibleFilters().getManualFilter().get(filterName);
490 if (visible) {
491 if (!customTables.containsKey(filter.getTitle())) {
492 CollapsibleTable table = new CollapsibleTable(
493 tableColumnProvider,
494 projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
495 filter.getTitle(),
496 "atlassian.toolwindow.serverToolBar",
497 "ThePlugin.CrucibleReviewToolBar",
498 "Context menu",
499 getPopupActionGroup());
500 table.addItemSelectedListener(this);
501 TableView.restore(projectConfiguration.getCrucibleConfiguration().getTableConfiguration(),
502 table.getTable());
503
504 dataPanelsHolder.add(table);
505 customTables.put(filter.getTitle(), table);
506
507 refreshReviews(checker);
508 }
509 } else {
510 if (customTables.containsKey(filter.getTitle())) {
511 dataPanelsHolder.remove(customTables.get(filter.getTitle()));
512 customTables.remove(filter.getTitle());
513 }
514 }
515 }
516 dataPanelsHolder.validate();
517 tablePane.repaint();
518 }
519 }
520
521 public void refreshReviews(final CrucibleStatusChecker checker) {
522 if (checker != null) {
523 if (checker.canSchedule()) {
524 final ProgressAnimationProvider animator = getProgressAnimation();
525 final Logger log = PluginUtil.getLogger();
526
527 new Thread(new Runnable() {
528 public void run() {
529 Thread t = new Thread(checker.newTimerTask(), "Manual Crucible panel refresh (checker)");
530 animator.startProgressAnimation();
531 t.start();
532 try {
533 t.join();
534 } catch (InterruptedException e) {
535 log.warn(e.toString());
536 } finally {
537 animator.stopProgressAnimation();
538 }
539 }
540 }, "Manual Crucible panel refresh").start();
541 }
542 }
543 }
544
545 public ProjectConfigurationBean getProjectConfiguration() {
546 return projectConfiguration;
547 }
548
549 public void getReviewComments() {
550 if (selectedItem != null) {
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728 }
729 }
730
731 public void focusOnReview(ReviewDataInfoAdapter reviewDataInfoAdapter) {
732
733 }
734
735 public void focusOnFile(ReviewDataInfoAdapter reviewDataInfoAdapter, ReviewItem reviewItem) {
736
737 }
738
739 public void focusOnGeneralComment(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
740
741 }
742
743 public void focusOnGeneralCommentReply(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
744
745 }
746
747 public void focusOnVersionedComment(ReviewDataInfoAdapter reviewDataInfoAdapter, ReviewItem reviewItem, Collection<VersionedComment> versionedComments, VersionedComment versionedComment) {
748
749 }
750
751 public void focusOnVersionedCommentReply(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
752
753 }
754
755 public void showReview(ReviewDataInfoAdapter reviewItem) {
756
757 }
758
759 public void showReviewedFileItem(ReviewDataInfoAdapter reviewDataInfoAdapter, ReviewItem reviewItem) {
760
761 }
762
763 public void showGeneralComment(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
764
765 }
766
767 public void showGeneralCommentReply(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
768
769 }
770
771 public void showVersionedComment(ReviewDataInfoAdapter reviewDataInfoAdapter, ReviewItem reviewItem, Collection<VersionedComment> versionedComments, VersionedComment versionedComment) {
772
773 }
774
775 public void focusOnVersionedComment(ReviewDataInfoAdapter reviewDataInfoAdapter, VersionedComment versionedComment) {
776
777 }
778
779 public void showVersionedCommentReply(ReviewDataInfoAdapter reviewDataInfoAdapter, GeneralComment comment) {
780
781 }
782 }