Project

General

Profile

Download (21.7 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2017 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.vaadin.mvp;
10

    
11
import java.util.Map;
12

    
13
import org.apache.log4j.Logger;
14

    
15
import com.vaadin.data.Validator.InvalidValueException;
16
import com.vaadin.data.fieldgroup.BeanFieldGroup;
17
import com.vaadin.data.fieldgroup.FieldGroup.CommitEvent;
18
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
19
import com.vaadin.data.fieldgroup.FieldGroup.CommitHandler;
20
import com.vaadin.data.fieldgroup.FieldGroup.FieldGroupInvalidValueException;
21
import com.vaadin.server.AbstractErrorMessage.ContentMode;
22
import com.vaadin.server.ErrorMessage.ErrorLevel;
23
import com.vaadin.server.FontAwesome;
24
import com.vaadin.server.UserError;
25
import com.vaadin.shared.ui.MarginInfo;
26
import com.vaadin.ui.AbstractField;
27
import com.vaadin.ui.AbstractLayout;
28
import com.vaadin.ui.AbstractOrderedLayout;
29
import com.vaadin.ui.Alignment;
30
import com.vaadin.ui.Button;
31
import com.vaadin.ui.CheckBox;
32
import com.vaadin.ui.Component;
33
import com.vaadin.ui.CssLayout;
34
import com.vaadin.ui.Field;
35
import com.vaadin.ui.GridLayout;
36
import com.vaadin.ui.GridLayout.OutOfBoundsException;
37
import com.vaadin.ui.GridLayout.OverlapsException;
38
import com.vaadin.ui.HorizontalLayout;
39
import com.vaadin.ui.Layout;
40
import com.vaadin.ui.Notification;
41
import com.vaadin.ui.Notification.Type;
42
import com.vaadin.ui.PopupDateField;
43
import com.vaadin.ui.TextField;
44
import com.vaadin.ui.VerticalLayout;
45
import com.vaadin.ui.themes.ValoTheme;
46

    
47
import eu.etaxonomy.cdm.database.PermissionDeniedException;
48
import eu.etaxonomy.cdm.vaadin.component.TextFieldNFix;
49
import eu.etaxonomy.vaadin.component.NestedFieldGroup;
50
import eu.etaxonomy.vaadin.component.SwitchableTextField;
51
import eu.etaxonomy.vaadin.mvp.event.EditorCancelEvent;
52
import eu.etaxonomy.vaadin.mvp.event.EditorDeleteEvent;
53
import eu.etaxonomy.vaadin.mvp.event.EditorPreSaveEvent;
54
import eu.etaxonomy.vaadin.mvp.event.EditorSaveEvent;
55
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent;
56
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent.Reason;
57

    
58
/**
59
 *
60
 * Optional with a delete button which can be enabled with {@link #withDeleteButton(boolean)}
61
 *
62
 * @author a.kohlbecker
63
 * @since Apr 5, 2017
64
 *
65
 */
66
public abstract class AbstractPopupEditor<DTO extends Object, P extends AbstractEditorPresenter<DTO, ? extends ApplicationView>>
67
    extends AbstractPopupView<P> {
68

    
69
    public static final Logger logger = Logger.getLogger(AbstractPopupEditor.class);
70

    
71
    private BeanFieldGroup<DTO> fieldGroup;
72

    
73
    private VerticalLayout mainLayout;
74

    
75
    private Layout fieldLayout;
76

    
77
    private HorizontalLayout buttonLayout;
78

    
79
    private Button save;
80

    
81
    private Button cancel;
82

    
83
    private Button delete;
84

    
85
    private CssLayout toolBar = new CssLayout();
86

    
87
    private CssLayout toolBarButtonGroup = new CssLayout();
88

    
89
    private GridLayout _gridLayoutCache;
90

    
91
    private boolean isBeanLoaded;
92

    
93
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
94

    
95
        mainLayout = new VerticalLayout();
96
        // IMPORTANT: mainLayout must be set to full size otherwise the
97
        // popup window may have problems with automatic resizing of its
98
        // content.
99
        mainLayout.setSizeFull();
100
        setCompositionRoot(mainLayout);
101

    
102
        fieldGroup = new BeanFieldGroup<>(dtoType);
103
        fieldGroup.addCommitHandler(new SaveHandler());
104

    
105
        toolBar.addStyleName(ValoTheme.WINDOW_TOP_TOOLBAR);
106
        toolBar.setWidth(100, Unit.PERCENTAGE);
107
        toolBarButtonGroup.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
108
        toolBarButtonGroup.setWidthUndefined();
109
        toolBar.addComponent(toolBarButtonGroup);
110
        toolBar.setVisible(false);
111

    
112
        fieldLayout = layout;
113
        fieldLayout.setWidthUndefined();
114
        if(fieldLayout instanceof AbstractOrderedLayout){
115
            ((AbstractOrderedLayout)fieldLayout).setSpacing(true);
116
        }
117

    
118
        buttonLayout = new HorizontalLayout();
119
        buttonLayout.setStyleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR);
120
        buttonLayout.setWidth(100, Unit.PERCENTAGE);
121
        buttonLayout.setSpacing(true);
122

    
123
        save = new Button("Save", FontAwesome.SAVE);
124
        save.setStyleName(ValoTheme.BUTTON_PRIMARY);
125
        save.addClickListener(e -> save());
126

    
127
        cancel = new Button("Cancel", FontAwesome.TRASH);
128
        cancel.addClickListener(e -> cancel());
129

    
130
        delete = new Button("Delete", FontAwesome.REMOVE);
131
        delete.setStyleName(ValoTheme.BUTTON_DANGER);
132
        delete.addClickListener(e -> delete());
133
        delete.setVisible(false);
134

    
135
        buttonLayout.addComponents(delete, save, cancel);
136
        // delete is initially invisible, let save take all space
137
        buttonLayout.setExpandRatio(save, 1);
138
        buttonLayout.setComponentAlignment(delete, Alignment.TOP_RIGHT);
139
        buttonLayout.setComponentAlignment(save, Alignment.TOP_RIGHT);
140
        buttonLayout.setComponentAlignment(cancel, Alignment.TOP_RIGHT);
141

    
142
        mainLayout.addComponents(toolBar, fieldLayout, buttonLayout);
143
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
144

    
145
        updateToolBarVisibility();
146
    }
147

    
148
    protected VerticalLayout getMainLayout() {
149
        return mainLayout;
150
    }
151

    
152
    protected Layout getFieldLayout() {
153
        return fieldLayout;
154
    }
155

    
156
    /**
157
     * @return
158
     */
159
    private GridLayout gridLayout() {
160
        if(_gridLayoutCache == null){
161
            if(fieldLayout instanceof GridLayout){
162
                _gridLayoutCache = (GridLayout)fieldLayout;
163
            } else {
164
                throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
165
            }
166
        }
167
        return _gridLayoutCache;
168
    }
169

    
170
    @Override
171
    public void setReadOnly(boolean readOnly) {
172
        super.setReadOnly(readOnly);
173
        save.setVisible(!readOnly);
174
        delete.setVisible(!readOnly);
175
        cancel.setCaption(readOnly ? "Close" : "Cancel");
176
    }
177

    
178
    /**
179
     * @return
180
     * @return
181
     */
182
    protected AbstractLayout getToolBar() {
183
        return toolBar;
184
    }
185

    
186
    /**
187
     * @return
188
     * @return
189
     */
190
    protected void toolBarAdd(Component c) {
191
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
192
        updateToolBarVisibility();
193
    }
194

    
195
    /**
196
     * @return
197
     * @return
198
     */
199
    protected void toolBarButtonGroupAdd(Component c) {
200
        toolBarButtonGroup.addComponent(c);
201
        updateToolBarVisibility();
202
    }
203

    
204
    /**
205
     * @return
206
     * @return
207
     */
208
    protected void toolBarButtonGroupRemove(Component c) {
209
        toolBarButtonGroup.removeComponent(c);
210
        updateToolBarVisibility();
211
    }
212

    
213
    /**
214
     *
215
     */
216
    private void updateToolBarVisibility() {
217
        boolean showToolbar = toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1;
218
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
219
        if(!showToolbar){
220
            mainLayout.setMargin(new MarginInfo(true, false, false, false));
221
        } else {
222
            mainLayout.setMargin(false);
223
        }
224

    
225
    }
226

    
227
    /**
228
     * The top tool-bar is initially invisible.
229
     *
230
     * @param visible
231
     */
232
    protected void setToolBarVisible(boolean visible){
233
        toolBar.setVisible(true);
234
    }
235

    
236

    
237

    
238
    // ------------------------ event handler ------------------------ //
239

    
240
    private class SaveHandler implements CommitHandler {
241

    
242
        private static final long serialVersionUID = 2047223089707080659L;
243

    
244
        @Override
245
        public void preCommit(CommitEvent commitEvent) throws CommitException {
246
            logger.debug("preCommit(), publishing EditorPreSaveEvent");
247
            // notify the presenter to start a transaction
248
            eventBus.publishEvent(new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
249
        }
250

    
251
        @Override
252
        public void postCommit(CommitEvent commitEvent) throws CommitException {
253
            try {
254
                if(logger.isTraceEnabled()){
255
                    logger.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
256
                }
257
                // notify the presenter to persist the bean and to commit the transaction
258
                eventBus.publishEvent(new EditorSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
259
                if(logger.isTraceEnabled()){
260
                    logger.trace("postCommit() publishing DoneWithPopupEvent");
261
                }
262
                // notify the NavigationManagerBean to close the window and to dispose the view
263
                eventBus.publishEvent(new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
264
            } catch (Exception e) {
265
                logger.error(e);
266
                throw new CommitException("Failed to store data to backend", e);
267
            }
268
        }
269
    }
270

    
271
    protected void addCommitHandler(CommitHandler commitHandler) {
272
        fieldGroup.addCommitHandler(commitHandler);
273
    }
274

    
275

    
276
    /**
277
     * Cancel editing and discard all modifications.
278
     */
279
    @Override
280
    public void cancel() {
281
        fieldGroup.discard();
282
        eventBus.publishEvent(new EditorCancelEvent<DTO>(this, getBean()));
283
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.CANCEL));
284
    }
285

    
286
    /**
287
     * @return
288
     */
289
    private void delete() {
290
        eventBus.publishEvent(new EditorDeleteEvent(this, fieldGroup.getItemDataSource().getBean()));
291
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.DELETE));
292
    }
293

    
294
    /**
295
     * Save the changes made in the editor.
296
     */
297
    private void save() {
298
        try {
299
            fieldGroup.commit();
300
        } catch (CommitException e) {
301
            fieldGroup.getFields().forEach(f -> ((AbstractField<?>)f).setValidationVisible(true));
302
            if(e.getCause() != null && e.getCause() instanceof FieldGroupInvalidValueException){
303
                FieldGroupInvalidValueException invalidValueException = (FieldGroupInvalidValueException)e.getCause();
304
                updateFieldNotifications(invalidValueException.getInvalidFields());
305
                Notification.show("The entered data in " + invalidValueException.getInvalidFields().size() + " fields is incomplete or invalid.");
306
            } else if(e.getCause() != null && e.getCause().getCause() != null && e.getCause().getCause() instanceof PermissionDeniedException){
307
                PermissionDeniedException permissionDeniedException = (PermissionDeniedException)e.getCause().getCause();
308
                Notification.show("Permission denied", permissionDeniedException.getMessage(), Type.ERROR_MESSAGE);
309
            } else {
310
//                Logger.getLogger(this.getClass()).error("Error saving", e);
311
//                Notification.show("Error saving", Type.ERROR_MESSAGE);
312
                throw new RuntimeException("Error saving", e);
313
            }
314
        }
315
    }
316

    
317
    /**
318
     * @param invalidFields
319
     */
320
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
321
        for(Field<?> f : invalidFields.keySet()){
322
            if(f instanceof AbstractField){
323
                String message = invalidFields.get(f).getHtmlMessage();
324
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
325
            }
326
        }
327

    
328
    }
329

    
330
    // ------------------------ field adding methods ------------------------ //
331

    
332

    
333
    protected TextField addTextField(String caption, String propertyId) {
334
        return addField(new TextFieldNFix(caption), propertyId);
335
    }
336

    
337
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
338
            int column2, int row2)
339
            throws OverlapsException, OutOfBoundsException {
340
        return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
341
    }
342

    
343
    protected TextField addTextField(String caption, String propertyId, int column, int row)
344
            throws OverlapsException, OutOfBoundsException {
345
        return addField(new TextFieldNFix(caption), propertyId, column, row);
346
    }
347

    
348
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
349
            int column2, int row2)
350
            throws OverlapsException, OutOfBoundsException {
351

    
352
        SwitchableTextField field = new SwitchableTextField(caption);
353
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
354
        addComponent(field, column1, row1, column2, row2);
355
        return field;
356
    }
357

    
358
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
359
            throws OverlapsException, OutOfBoundsException {
360

    
361
        SwitchableTextField field = new SwitchableTextField(caption);
362
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
363
        addComponent(field, column, row);
364
        return field;
365
    }
366

    
367
    protected PopupDateField addDateField(String caption, String propertyId) {
368
        return addField(new PopupDateField(caption), propertyId);
369
    }
370

    
371
    protected CheckBox addCheckBox(String caption, String propertyId) {
372
        return addField(new CheckBox(caption), propertyId);
373
    }
374

    
375
    protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
376
        return addField(new CheckBox(caption), propertyId, column, row);
377
    }
378

    
379
    protected <T extends Field> T addField(T field, String propertyId) {
380
        fieldGroup.bind(field, propertyId);
381
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
382
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
383
        }
384
        addComponent(field);
385
        return field;
386
    }
387

    
388
    /**
389
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
390
     *
391
     * @param field
392
     *            the field to be added, not <code>null</code>.
393
     * @param propertyId
394
     * @param column
395
     *            the column index, starting from 0.
396
     * @param row
397
     *            the row index, starting from 0.
398
     * @throws OverlapsException
399
     *             if the new component overlaps with any of the components
400
     *             already in the grid.
401
     * @throws OutOfBoundsException
402
     *             if the cell is outside the grid area.
403
     */
404
    protected <T extends Field> T addField(T field, String propertyId, int column, int row)
405
            throws OverlapsException, OutOfBoundsException {
406
        fieldGroup.bind(field, propertyId);
407
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
408
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
409
        }
410
        addComponent(field, column, row);
411
        return field;
412
    }
413

    
414
    /**
415
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
416
     *
417
     * @param field
418
     * @param propertyId
419
     * @param column1
420
     * @param row1
421
     * @param column2
422
     * @param row2
423
     * @return
424
     * @throws OverlapsException
425
     * @throws OutOfBoundsException
426
     */
427
    protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
428
            int column2, int row2)
429
            throws OverlapsException, OutOfBoundsException {
430
        if(propertyId != null){
431
            fieldGroup.bind(field, propertyId);
432
            if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
433
                ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
434
            }
435
        }
436
        addComponent(field, column1, row1, column2, row2);
437
        return field;
438
    }
439

    
440
    protected Field<?> getField(Object propertyId){
441
        return fieldGroup.getField(propertyId);
442
    }
443

    
444
    protected void addComponent(Component component) {
445
        fieldLayout.addComponent(component);
446
        applyDefaultComponentStyles(component);
447
    }
448

    
449
    protected void bindField(Field field, String propertyId){
450
        fieldGroup.bind(field, propertyId);
451
    }
452

    
453
    /**
454
     * @param component
455
     */
456
    public void applyDefaultComponentStyles(Component component) {
457
        component.addStyleName(getDefaultComponentStyles());
458
    }
459

    
460
    protected abstract String getDefaultComponentStyles();
461

    
462
    /**
463
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
464
     * <p>
465
     * Adds the component to the grid in cells column1,row1 (NortWest corner of
466
     * the area.) End coordinates (SouthEast corner of the area) are the same as
467
     * column1,row1. The coordinates are zero-based. Component width and height
468
     * is 1.
469
     *
470
     * @param component
471
     *            the component to be added, not <code>null</code>.
472
     * @param column
473
     *            the column index, starting from 0.
474
     * @param row
475
     *            the row index, starting from 0.
476
     * @throws OverlapsException
477
     *             if the new component overlaps with any of the components
478
     *             already in the grid.
479
     * @throws OutOfBoundsException
480
     *             if the cell is outside the grid area.
481
     */
482
    public void addComponent(Component component, int column, int row)
483
            throws OverlapsException, OutOfBoundsException {
484
        applyDefaultComponentStyles(component);
485
        gridLayout().addComponent(component, column, row, column, row);
486
    }
487

    
488
    /**
489
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
490
     * <p>
491
     * Adds a component to the grid in the specified area. The area is defined
492
     * by specifying the upper left corner (column1, row1) and the lower right
493
     * corner (column2, row2) of the area. The coordinates are zero-based.
494
     * </p>
495
     *
496
     * <p>
497
     * If the area overlaps with any of the existing components already present
498
     * in the grid, the operation will fail and an {@link OverlapsException} is
499
     * thrown.
500
     * </p>
501
     *
502
     * @param component
503
     *            the component to be added, not <code>null</code>.
504
     * @param column1
505
     *            the column of the upper left corner of the area <code>c</code>
506
     *            is supposed to occupy. The leftmost column has index 0.
507
     * @param row1
508
     *            the row of the upper left corner of the area <code>c</code> is
509
     *            supposed to occupy. The topmost row has index 0.
510
     * @param column2
511
     *            the column of the lower right corner of the area
512
     *            <code>c</code> is supposed to occupy.
513
     * @param row2
514
     *            the row of the lower right corner of the area <code>c</code>
515
     *            is supposed to occupy.
516
     * @throws OverlapsException
517
     *             if the new component overlaps with any of the components
518
     *             already in the grid.
519
     * @throws OutOfBoundsException
520
     *             if the cells are outside the grid area.
521
     */
522
    public void addComponent(Component component, int column1, int row1,
523
            int column2, int row2)
524
            throws OverlapsException, OutOfBoundsException {
525
        applyDefaultComponentStyles(component);
526
        gridLayout().addComponent(component, column1, row1, column2, row2);
527
    }
528

    
529

    
530
    public void withDeleteButton(boolean withDelete){
531

    
532
        if(withDelete){
533
            buttonLayout.setExpandRatio(save, 0);
534
            buttonLayout.setExpandRatio(delete, 1);
535
        } else {
536
            buttonLayout.setExpandRatio(save, 1);
537
            buttonLayout.setExpandRatio(delete, 0);
538
        }
539
        delete.setVisible(withDelete);
540
    }
541

    
542

    
543
    // ------------------------ data binding ------------------------ //
544

    
545
    protected void bindDesign(Component component) {
546
        fieldLayout.removeAllComponents();
547
        fieldGroup.bindMemberFields(component);
548
        fieldLayout.addComponent(component);
549
    }
550

    
551

    
552
    public final void loadInEditor(Object identifier) {
553

    
554
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
555
        fieldGroup.setItemDataSource(beanToEdit);
556
        afterItemDataSourceSet();
557
        isBeanLoaded = true;
558
    }
559

    
560
    /**
561
     * Passes the beanInstantiator to the presenter method {@link AbstractEditorPresenter#setBeanInstantiator(BeanInstantiator)}
562
     *
563
     * @param beanInstantiator
564
     */
565
    public final void setBeanInstantiator(BeanInstantiator<DTO> beanInstantiator) {
566
        getPresenter().setBeanInstantiator(beanInstantiator);
567
    }
568

    
569
    /**
570
     * Returns the bean contained in the itemDatasource of the fieldGroup.
571
     *
572
     * @return
573
     */
574
    public DTO getBean() {
575
        if(fieldGroup.getItemDataSource() != null){
576
            return fieldGroup.getItemDataSource().getBean();
577

    
578
        }
579
        return null;
580
    }
581

    
582
    /**
583
     * @return true once the bean has been loaded indicating that all fields have
584
     *   been setup configured so that the editor is ready for use.
585
     */
586
    public boolean isBeanLoaded() {
587
        return isBeanLoaded;
588
    }
589

    
590
    /**
591
     * This method should only be used by the presenter of this view
592
     *
593
     * @param bean
594
     */
595
    protected void updateItemDataSource(DTO bean) {
596
        fieldGroup.getItemDataSource().setBean(bean);
597
    }
598

    
599
    /**
600
     * This method is called after setting the item data source whereby the
601
     * {@link FieldGroup#configureField(Field<?> field)} method will be called.
602
     * In this method all fields are set to default states defined for the fieldGroup.
603
     * <p>
604
     * You can now implement this method if you need to modify the state or value of individual fields.
605
     */
606
    protected void afterItemDataSourceSet() {
607
    }
608

    
609
    // ------------------------ issue related temporary solutions --------------------- //
610
    /**
611
     * Publicly accessible equivalent to getPreseneter(), needed for
612
     * managing the presenter listeners.
613
     * <p>
614
     * TODO: refactor the presenter listeners management to get rid of this method
615
     *
616
     * @return
617
     * @deprecated marked deprecated to emphasize on the special character of this method
618
     *    which should only be used internally see #6673
619
     */
620
    @Deprecated
621
    public P presenter() {
622
        return getPresenter();
623
    }
624

    
625
}
(4-4/9)