Project

General

Profile

Download (19 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.ui.AbstractField;
26
import com.vaadin.ui.AbstractLayout;
27
import com.vaadin.ui.AbstractOrderedLayout;
28
import com.vaadin.ui.Alignment;
29
import com.vaadin.ui.Button;
30
import com.vaadin.ui.CheckBox;
31
import com.vaadin.ui.Component;
32
import com.vaadin.ui.CssLayout;
33
import com.vaadin.ui.Field;
34
import com.vaadin.ui.GridLayout;
35
import com.vaadin.ui.GridLayout.OutOfBoundsException;
36
import com.vaadin.ui.GridLayout.OverlapsException;
37
import com.vaadin.ui.HorizontalLayout;
38
import com.vaadin.ui.Layout;
39
import com.vaadin.ui.Notification;
40
import com.vaadin.ui.Notification.Type;
41
import com.vaadin.ui.PopupDateField;
42
import com.vaadin.ui.TextField;
43
import com.vaadin.ui.VerticalLayout;
44
import com.vaadin.ui.themes.ValoTheme;
45

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

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

    
66
    private BeanFieldGroup<DTO> fieldGroup;
67

    
68
    private VerticalLayout mainLayout;
69

    
70
    private Layout fieldLayout;
71

    
72
    private HorizontalLayout buttonLayout;
73

    
74
    private Button save;
75

    
76
    private Button cancel;
77

    
78
    private Button delete;
79

    
80
    private CssLayout toolBar = new CssLayout();
81

    
82
    private CssLayout toolBarButtonGroup = new CssLayout();
83

    
84
    private GridLayout _gridLayoutCache;
85

    
86
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
87

    
88
        setWidthUndefined();
89

    
90
        mainLayout = new VerticalLayout();
91
        mainLayout.setWidthUndefined();
92

    
93
        fieldGroup = new BeanFieldGroup<>(dtoType);
94
        fieldGroup.addCommitHandler(new SaveHandler());
95

    
96
        setCompositionRoot(mainLayout);
97

    
98
        toolBar.addStyleName(ValoTheme.WINDOW_TOP_TOOLBAR);
99
        toolBar.setWidth(100, Unit.PERCENTAGE);
100
        toolBarButtonGroup.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
101
        toolBarButtonGroup.setWidthUndefined();
102
        toolBar.addComponent(toolBarButtonGroup);
103
        toolBar.setVisible(false);
104

    
105
        fieldLayout = layout;
106
        fieldLayout.setWidthUndefined();
107
        if(fieldLayout instanceof AbstractOrderedLayout){
108
            ((AbstractOrderedLayout)fieldLayout).setSpacing(true);
109
        }
110

    
111
        buttonLayout = new HorizontalLayout();
112
        buttonLayout.setStyleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR);
113
        buttonLayout.setWidth(100, Unit.PERCENTAGE);
114
        buttonLayout.setSpacing(true);
115

    
116
        save = new Button("Save", FontAwesome.SAVE);
117
        save.setStyleName(ValoTheme.BUTTON_PRIMARY);
118
        save.addClickListener(e -> save());
119

    
120
        cancel = new Button("Cancel", FontAwesome.TRASH);
121
        cancel.addClickListener(e -> cancel());
122

    
123
        delete = new Button("Delete", FontAwesome.REMOVE);
124
        delete.setStyleName(ValoTheme.BUTTON_DANGER);
125
        delete.addClickListener(e -> delete());
126
        delete.setVisible(false);
127

    
128
        buttonLayout.addComponents(delete, save, cancel);
129
        // delete is initially invisible, let save take all space
130
        buttonLayout.setExpandRatio(save, 1);
131
        buttonLayout.setComponentAlignment(delete, Alignment.TOP_RIGHT);
132
        buttonLayout.setComponentAlignment(save, Alignment.TOP_RIGHT);
133
        buttonLayout.setComponentAlignment(cancel, Alignment.TOP_RIGHT);
134

    
135
        mainLayout.addComponents(toolBar, fieldLayout, buttonLayout);
136
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
137
    }
138

    
139
    protected VerticalLayout getMainLayout() {
140
        return mainLayout;
141
    }
142

    
143
    protected Layout getFieldLayout() {
144
        return fieldLayout;
145
    }
146

    
147
    /**
148
     * @return
149
     */
150
    private GridLayout gridLayout() {
151
        if(_gridLayoutCache == null){
152
            if(fieldLayout instanceof GridLayout){
153
                _gridLayoutCache = (GridLayout)fieldLayout;
154
            } else {
155
                throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
156
            }
157
        }
158
        return _gridLayoutCache;
159
    }
160

    
161
    @Override
162
    public void setReadOnly(boolean readOnly) {
163
        super.setReadOnly(readOnly);
164
        save.setVisible(!readOnly);
165
        cancel.setCaption(readOnly ? "Close" : "Cancel");
166
    }
167

    
168
    /**
169
     * @return
170
     * @return
171
     */
172
    protected AbstractLayout getToolBar() {
173
        return toolBar;
174
    }
175

    
176
    /**
177
     * @return
178
     * @return
179
     */
180
    protected void toolBarAdd(Component c) {
181
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
182
        updateToolBarVisibility();
183
    }
184

    
185
    /**
186
     * @return
187
     * @return
188
     */
189
    protected void toolBarButtonGroupAdd(Component c) {
190
        toolBarButtonGroup.addComponent(c);
191
        updateToolBarVisibility();
192
    }
193

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

    
203
    /**
204
     *
205
     */
206
    private void updateToolBarVisibility() {
207
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
208

    
209
    }
210

    
211
    /**
212
     * The top tool-bar is initially invisible.
213
     *
214
     * @param visible
215
     */
216
    protected void setToolBarVisible(boolean visible){
217
        toolBar.setVisible(true);
218
    }
219

    
220

    
221

    
222
    // ------------------------ event handler ------------------------ //
223

    
224
    private class SaveHandler implements CommitHandler {
225

    
226
        private static final long serialVersionUID = 2047223089707080659L;
227

    
228
        @Override
229
        public void preCommit(CommitEvent commitEvent) throws CommitException {
230
            logger.debug("preCommit");
231
            // notify the presenter to start a transaction
232
            eventBus.publishEvent(new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, fieldGroup.getItemDataSource().getBean()));
233
        }
234

    
235
        @Override
236
        public void postCommit(CommitEvent commitEvent) throws CommitException {
237
            try {
238
                // notify the presenter to persist the bean and to commit the transaction
239
                eventBus.publishEvent(new EditorSaveEvent<DTO>(AbstractPopupEditor.this, fieldGroup.getItemDataSource().getBean()));
240

    
241
                // notify the NavigationManagerBean to close the window and to dispose the view
242
                eventBus.publishEvent(new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
243
            } catch (Exception e) {
244
                throw new CommitException("Failed to store data to backend", e);
245
            }
246
        }
247
    }
248

    
249
    protected void addCommitHandler(CommitHandler commitHandler) {
250
        fieldGroup.addCommitHandler(commitHandler);
251
    }
252

    
253

    
254
    /**
255
     * Cancel editing and discard all modifications.
256
     */
257
    @Override
258
    public void cancel() {
259
        fieldGroup.discard();
260
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.CANCEL));
261
    }
262

    
263
    /**
264
     * @return
265
     */
266
    private void delete() {
267
        eventBus.publishEvent(new EditorDeleteEvent(this, fieldGroup.getItemDataSource().getBean()));
268
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.DELETE));
269
    }
270

    
271
    /**
272
     * Save the changes made in the editor.
273
     */
274
    private void save() {
275
        try {
276
            fieldGroup.commit();
277
        } catch (CommitException e) {
278
            fieldGroup.getFields().forEach(f -> ((AbstractField<?>)f).setValidationVisible(true));
279
            if(e.getCause() != null && e.getCause() instanceof FieldGroupInvalidValueException){
280
                FieldGroupInvalidValueException invalidValueException = (FieldGroupInvalidValueException)e.getCause();
281
                updateFieldNotifications(invalidValueException.getInvalidFields());
282
                Notification.show("The entered data in " + invalidValueException.getInvalidFields().size() + " fields is incomplete or invalid.");
283
            } else if(e.getCause() != null && e.getCause().getCause() != null && e.getCause().getCause() instanceof PermissionDeniedException){
284
                PermissionDeniedException permissionDeniedException = (PermissionDeniedException)e.getCause().getCause();
285
                Notification.show("Permission denied", permissionDeniedException.getMessage(), Type.ERROR_MESSAGE);
286
            }
287
            else {
288
                Logger.getLogger(this.getClass()).error("Error saving", e);
289
                Notification.show("Error saving", Type.ERROR_MESSAGE);
290
            }
291
        }
292
    }
293

    
294
    /**
295
     * @param invalidFields
296
     */
297
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
298
        for(Field<?> f : invalidFields.keySet()){
299
            if(f instanceof AbstractField){
300
                String message = invalidFields.get(f).getHtmlMessage();
301
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
302
            }
303
        }
304

    
305
    }
306

    
307
    // ------------------------ field adding methods ------------------------ //
308

    
309

    
310
    protected TextField addTextField(String caption, String propertyId) {
311
        return addField(new TextField(caption), propertyId);
312
    }
313

    
314
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
315
            int column2, int row2)
316
            throws OverlapsException, OutOfBoundsException {
317
        return addField(new TextField(caption), propertyId, column1, row1, column2, row2);
318
    }
319

    
320
    protected TextField addTextField(String caption, String propertyId, int column, int row)
321
            throws OverlapsException, OutOfBoundsException {
322
        return addField(new TextField(caption), propertyId, column, row);
323
    }
324

    
325
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
326
            int column2, int row2)
327
            throws OverlapsException, OutOfBoundsException {
328

    
329
        SwitchableTextField field = new SwitchableTextField(caption);
330
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
331
        addComponent(field, column1, row1, column2, row2);
332
        return field;
333
    }
334

    
335
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
336
            throws OverlapsException, OutOfBoundsException {
337

    
338
        SwitchableTextField field = new SwitchableTextField(caption);
339
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
340
        addComponent(field, column, row);
341
        return field;
342
    }
343

    
344
    protected PopupDateField addDateField(String caption, String propertyId) {
345
        return addField(new PopupDateField(caption), propertyId);
346
    }
347

    
348
    protected CheckBox addCheckBox(String caption, String propertyId) {
349
        return addField(new CheckBox(caption), propertyId);
350
    }
351

    
352
    protected <T extends Field> T addField(T field, String propertyId) {
353
        fieldGroup.bind(field, propertyId);
354
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
355
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
356
        }
357
        addComponent(field);
358
        return field;
359
    }
360

    
361
    /**
362
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
363
     *
364
     * @param field
365
     *            the field to be added, not <code>null</code>.
366
     * @param propertyId
367
     * @param column
368
     *            the column index, starting from 0.
369
     * @param row
370
     *            the row index, starting from 0.
371
     * @throws OverlapsException
372
     *             if the new component overlaps with any of the components
373
     *             already in the grid.
374
     * @throws OutOfBoundsException
375
     *             if the cell is outside the grid area.
376
     */
377
    protected <T extends Field> T addField(T field, String propertyId, int column, int row)
378
            throws OverlapsException, OutOfBoundsException {
379
        fieldGroup.bind(field, propertyId);
380
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
381
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
382
        }
383
        addComponent(field, column, row);
384
        return field;
385
    }
386

    
387
    /**
388
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
389
     *
390
     * @param field
391
     * @param propertyId
392
     * @param column1
393
     * @param row1
394
     * @param column2
395
     * @param row2
396
     * @return
397
     * @throws OverlapsException
398
     * @throws OutOfBoundsException
399
     */
400
    protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
401
            int column2, int row2)
402
            throws OverlapsException, OutOfBoundsException {
403
        if(propertyId != null){
404
            fieldGroup.bind(field, propertyId);
405
            if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
406
                ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
407
            }
408
        }
409
        addComponent(field, column1, row1, column2, row2);
410
        return field;
411
    }
412

    
413
    protected Field<?> getField(Object propertyId){
414
        return fieldGroup.getField(propertyId);
415
    }
416

    
417
    protected void addComponent(Component component) {
418
        fieldLayout.addComponent(component);
419
        applyDefaultComponentStyles(component);
420
    }
421

    
422
    /**
423
     * @param component
424
     */
425
    public void applyDefaultComponentStyles(Component component) {
426
        component.addStyleName(getDefaultComponentStyles());
427
    }
428

    
429
    protected abstract String getDefaultComponentStyles();
430

    
431
    /**
432
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
433
     * <p>
434
     * Adds the component to the grid in cells column1,row1 (NortWest corner of
435
     * the area.) End coordinates (SouthEast corner of the area) are the same as
436
     * column1,row1. The coordinates are zero-based. Component width and height
437
     * is 1.
438
     *
439
     * @param component
440
     *            the component to be added, not <code>null</code>.
441
     * @param column
442
     *            the column index, starting from 0.
443
     * @param row
444
     *            the row index, starting from 0.
445
     * @throws OverlapsException
446
     *             if the new component overlaps with any of the components
447
     *             already in the grid.
448
     * @throws OutOfBoundsException
449
     *             if the cell is outside the grid area.
450
     */
451
    public void addComponent(Component component, int column, int row)
452
            throws OverlapsException, OutOfBoundsException {
453
        applyDefaultComponentStyles(component);
454
        gridLayout().addComponent(component, column, row, column, row);
455
    }
456

    
457
    /**
458
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
459
     * <p>
460
     * Adds a component to the grid in the specified area. The area is defined
461
     * by specifying the upper left corner (column1, row1) and the lower right
462
     * corner (column2, row2) of the area. The coordinates are zero-based.
463
     * </p>
464
     *
465
     * <p>
466
     * If the area overlaps with any of the existing components already present
467
     * in the grid, the operation will fail and an {@link OverlapsException} is
468
     * thrown.
469
     * </p>
470
     *
471
     * @param component
472
     *            the component to be added, not <code>null</code>.
473
     * @param column1
474
     *            the column of the upper left corner of the area <code>c</code>
475
     *            is supposed to occupy. The leftmost column has index 0.
476
     * @param row1
477
     *            the row of the upper left corner of the area <code>c</code> is
478
     *            supposed to occupy. The topmost row has index 0.
479
     * @param column2
480
     *            the column of the lower right corner of the area
481
     *            <code>c</code> is supposed to occupy.
482
     * @param row2
483
     *            the row of the lower right corner of the area <code>c</code>
484
     *            is supposed to occupy.
485
     * @throws OverlapsException
486
     *             if the new component overlaps with any of the components
487
     *             already in the grid.
488
     * @throws OutOfBoundsException
489
     *             if the cells are outside the grid area.
490
     */
491
    public void addComponent(Component component, int column1, int row1,
492
            int column2, int row2)
493
            throws OverlapsException, OutOfBoundsException {
494
        applyDefaultComponentStyles(component);
495
        gridLayout().addComponent(component, column1, row1, column2, row2);
496
    }
497

    
498

    
499
    public void withDeleteButton(boolean withDelete){
500

    
501
        if(withDelete){
502
            buttonLayout.setExpandRatio(save, 0);
503
            buttonLayout.setExpandRatio(delete, 1);
504
        } else {
505
            buttonLayout.setExpandRatio(save, 1);
506
            buttonLayout.setExpandRatio(delete, 0);
507
        }
508
        delete.setVisible(withDelete);
509
    }
510

    
511

    
512
    // ------------------------ data binding ------------------------ //
513

    
514
    protected void bindDesign(Component component) {
515
        fieldLayout.removeAllComponents();
516
        fieldGroup.bindMemberFields(component);
517
        fieldLayout.addComponent(component);
518
    }
519

    
520
    public void showInEditor(DTO beanToEdit) {
521

    
522
        fieldGroup.setItemDataSource(beanToEdit);
523
        afterItemDataSourceSet();
524
    }
525

    
526
    /**
527
     * Returns the bean to be edited.
528
     *
529
     * @return
530
     */
531
    public DTO getBean() {
532
        return fieldGroup.getItemDataSource().getBean();
533
    }
534

    
535
    /**
536
     * This method is called after setting the item data source whereby the {@link FieldGroup#configureField(Field<?> field)} method will be called.
537
     * In this method all fields are set to default states defined for the fieldGroup.
538
     * <p>
539
     * You can now implement this method if you need to configure the enable state of fields
540
     * individually.
541
     */
542
    protected void afterItemDataSourceSet() {
543

    
544
    }
545

    
546
    // ------------------------ issue related temporary solutions --------------------- //
547
    /**
548
     *
549
     * @return
550
     * @deprecated see #6673
551
     */
552
    @Deprecated
553
    public P presenter() {
554
        return getPresenter();
555
    }
556
}
(4-4/8)