Project

General

Profile

Download (20.3 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
    public static final Logger logger = Logger.getLogger(AbstractPopupEditor.class);
67

    
68
    private BeanFieldGroup<DTO> fieldGroup;
69

    
70
    private VerticalLayout mainLayout;
71

    
72
    private Layout fieldLayout;
73

    
74
    private HorizontalLayout buttonLayout;
75

    
76
    private Button save;
77

    
78
    private Button cancel;
79

    
80
    private Button delete;
81

    
82
    private CssLayout toolBar = new CssLayout();
83

    
84
    private CssLayout toolBarButtonGroup = new CssLayout();
85

    
86
    private GridLayout _gridLayoutCache;
87

    
88
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
89

    
90
        setWidthUndefined();
91

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

    
98
        fieldGroup = new BeanFieldGroup<>(dtoType);
99
        fieldGroup.addCommitHandler(new SaveHandler());
100

    
101
        setCompositionRoot(mainLayout);
102

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

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

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

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

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

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

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

    
140
        mainLayout.addComponents(toolBar, fieldLayout, buttonLayout);
141
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
142
    }
143

    
144
    protected VerticalLayout getMainLayout() {
145
        return mainLayout;
146
    }
147

    
148
    protected Layout getFieldLayout() {
149
        return fieldLayout;
150
    }
151

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

    
166
    @Override
167
    public void setReadOnly(boolean readOnly) {
168
        super.setReadOnly(readOnly);
169
        save.setVisible(!readOnly);
170
        cancel.setCaption(readOnly ? "Close" : "Cancel");
171
    }
172

    
173
    /**
174
     * @return
175
     * @return
176
     */
177
    protected AbstractLayout getToolBar() {
178
        return toolBar;
179
    }
180

    
181
    /**
182
     * @return
183
     * @return
184
     */
185
    protected void toolBarAdd(Component c) {
186
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
187
        updateToolBarVisibility();
188
    }
189

    
190
    /**
191
     * @return
192
     * @return
193
     */
194
    protected void toolBarButtonGroupAdd(Component c) {
195
        toolBarButtonGroup.addComponent(c);
196
        updateToolBarVisibility();
197
    }
198

    
199
    /**
200
     * @return
201
     * @return
202
     */
203
    protected void toolBarButtonGroupRemove(Component c) {
204
        toolBarButtonGroup.removeComponent(c);
205
        updateToolBarVisibility();
206
    }
207

    
208
    /**
209
     *
210
     */
211
    private void updateToolBarVisibility() {
212
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
213

    
214
    }
215

    
216
    /**
217
     * The top tool-bar is initially invisible.
218
     *
219
     * @param visible
220
     */
221
    protected void setToolBarVisible(boolean visible){
222
        toolBar.setVisible(true);
223
    }
224

    
225

    
226

    
227
    // ------------------------ event handler ------------------------ //
228

    
229
    private class SaveHandler implements CommitHandler {
230

    
231
        private static final long serialVersionUID = 2047223089707080659L;
232

    
233
        @Override
234
        public void preCommit(CommitEvent commitEvent) throws CommitException {
235
            logger.debug("preCommit(), publishing EditorPreSaveEvent");
236
            // notify the presenter to start a transaction
237
            eventBus.publishEvent(new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
238
        }
239

    
240
        @Override
241
        public void postCommit(CommitEvent commitEvent) throws CommitException {
242
            try {
243
                if(logger.isTraceEnabled()){
244
                    logger.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
245
                }
246
                // notify the presenter to persist the bean and to commit the transaction
247
                eventBus.publishEvent(new EditorSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
248
                if(logger.isTraceEnabled()){
249
                    logger.trace("postCommit() publishing DoneWithPopupEvent");
250
                }
251
                // notify the NavigationManagerBean to close the window and to dispose the view
252
                eventBus.publishEvent(new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
253
            } catch (Exception e) {
254
                throw new CommitException("Failed to store data to backend", e);
255
            }
256
        }
257
    }
258

    
259
    protected void addCommitHandler(CommitHandler commitHandler) {
260
        fieldGroup.addCommitHandler(commitHandler);
261
    }
262

    
263

    
264
    /**
265
     * Cancel editing and discard all modifications.
266
     */
267
    @Override
268
    public void cancel() {
269
        fieldGroup.discard();
270
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.CANCEL));
271
    }
272

    
273
    /**
274
     * @return
275
     */
276
    private void delete() {
277
        eventBus.publishEvent(new EditorDeleteEvent(this, fieldGroup.getItemDataSource().getBean()));
278
        eventBus.publishEvent(new DoneWithPopupEvent(this, Reason.DELETE));
279
    }
280

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

    
304
    /**
305
     * @param invalidFields
306
     */
307
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
308
        for(Field<?> f : invalidFields.keySet()){
309
            if(f instanceof AbstractField){
310
                String message = invalidFields.get(f).getHtmlMessage();
311
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
312
            }
313
        }
314

    
315
    }
316

    
317
    // ------------------------ field adding methods ------------------------ //
318

    
319

    
320
    protected TextField addTextField(String caption, String propertyId) {
321
        return addField(new TextField(caption), propertyId);
322
    }
323

    
324
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
325
            int column2, int row2)
326
            throws OverlapsException, OutOfBoundsException {
327
        return addField(new TextField(caption), propertyId, column1, row1, column2, row2);
328
    }
329

    
330
    protected TextField addTextField(String caption, String propertyId, int column, int row)
331
            throws OverlapsException, OutOfBoundsException {
332
        return addField(new TextField(caption), propertyId, column, row);
333
    }
334

    
335
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
336
            int column2, int row2)
337
            throws OverlapsException, OutOfBoundsException {
338

    
339
        SwitchableTextField field = new SwitchableTextField(caption);
340
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
341
        addComponent(field, column1, row1, column2, row2);
342
        return field;
343
    }
344

    
345
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
346
            throws OverlapsException, OutOfBoundsException {
347

    
348
        SwitchableTextField field = new SwitchableTextField(caption);
349
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
350
        addComponent(field, column, row);
351
        return field;
352
    }
353

    
354
    protected PopupDateField addDateField(String caption, String propertyId) {
355
        return addField(new PopupDateField(caption), propertyId);
356
    }
357

    
358
    protected CheckBox addCheckBox(String caption, String propertyId) {
359
        return addField(new CheckBox(caption), propertyId);
360
    }
361

    
362
    protected <T extends Field> T addField(T field, String propertyId) {
363
        fieldGroup.bind(field, propertyId);
364
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
365
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
366
        }
367
        addComponent(field);
368
        return field;
369
    }
370

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

    
397
    /**
398
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
399
     *
400
     * @param field
401
     * @param propertyId
402
     * @param column1
403
     * @param row1
404
     * @param column2
405
     * @param row2
406
     * @return
407
     * @throws OverlapsException
408
     * @throws OutOfBoundsException
409
     */
410
    protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
411
            int column2, int row2)
412
            throws OverlapsException, OutOfBoundsException {
413
        if(propertyId != null){
414
            fieldGroup.bind(field, propertyId);
415
            if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
416
                ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
417
            }
418
        }
419
        addComponent(field, column1, row1, column2, row2);
420
        return field;
421
    }
422

    
423
    protected Field<?> getField(Object propertyId){
424
        return fieldGroup.getField(propertyId);
425
    }
426

    
427
    protected void addComponent(Component component) {
428
        fieldLayout.addComponent(component);
429
        applyDefaultComponentStyles(component);
430
    }
431

    
432
    protected void bindField(Field field, String propertyId){
433
        fieldGroup.bind(field, propertyId);
434
    }
435

    
436
    /**
437
     * @param component
438
     */
439
    public void applyDefaultComponentStyles(Component component) {
440
        component.addStyleName(getDefaultComponentStyles());
441
    }
442

    
443
    protected abstract String getDefaultComponentStyles();
444

    
445
    /**
446
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
447
     * <p>
448
     * Adds the component to the grid in cells column1,row1 (NortWest corner of
449
     * the area.) End coordinates (SouthEast corner of the area) are the same as
450
     * column1,row1. The coordinates are zero-based. Component width and height
451
     * is 1.
452
     *
453
     * @param component
454
     *            the component to be added, not <code>null</code>.
455
     * @param column
456
     *            the column index, starting from 0.
457
     * @param row
458
     *            the row index, starting from 0.
459
     * @throws OverlapsException
460
     *             if the new component overlaps with any of the components
461
     *             already in the grid.
462
     * @throws OutOfBoundsException
463
     *             if the cell is outside the grid area.
464
     */
465
    public void addComponent(Component component, int column, int row)
466
            throws OverlapsException, OutOfBoundsException {
467
        applyDefaultComponentStyles(component);
468
        gridLayout().addComponent(component, column, row, column, row);
469
    }
470

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

    
512

    
513
    public void withDeleteButton(boolean withDelete){
514

    
515
        if(withDelete){
516
            buttonLayout.setExpandRatio(save, 0);
517
            buttonLayout.setExpandRatio(delete, 1);
518
        } else {
519
            buttonLayout.setExpandRatio(save, 1);
520
            buttonLayout.setExpandRatio(delete, 0);
521
        }
522
        delete.setVisible(withDelete);
523
    }
524

    
525

    
526
    // ------------------------ data binding ------------------------ //
527

    
528
    protected void bindDesign(Component component) {
529
        fieldLayout.removeAllComponents();
530
        fieldGroup.bindMemberFields(component);
531
        fieldLayout.addComponent(component);
532
    }
533

    
534

    
535
    public final void loadInEditor(Object identifier) {
536

    
537
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
538

    
539
        DTO preparedBean = getPresenter().prepareAsFieldGroupDataSource(beanToEdit);
540
        fieldGroup.setItemDataSource(preparedBean);
541
        afterItemDataSourceSet();
542
    }
543

    
544
    /**
545
     * Returns the bean contained in the itemDatasource of the fieldGroup.
546
     *
547
     * @return
548
     */
549
    public DTO getBean() {
550
        return fieldGroup.getItemDataSource().getBean();
551
    }
552

    
553
    /**
554
     * This method should only be used by the presenter of this view
555
     *
556
     * @param bean
557
     */
558
    protected void updateItemDataSource(DTO bean) {
559
        fieldGroup.getItemDataSource().setBean(bean);
560
    }
561

    
562
    /**
563
     * This method is called after setting the item data source whereby the
564
     * {@link FieldGroup#configureField(Field<?> field)} method will be called.
565
     * In this method all fields are set to default states defined for the fieldGroup.
566
     * <p>
567
     * You can now implement this method if you need to modify the state or value of individual fields.
568
     */
569
    protected void afterItemDataSourceSet() {
570
    }
571

    
572
    // ------------------------ issue related temporary solutions --------------------- //
573
    /**
574
     * Publicly accessible equivalent to getPreseneter(), needed for
575
     * managing the presenter listeners.
576
     * <p>
577
     * TODO: refactor the presenter listeners management to get rid of this method
578
     *
579
     * @return
580
     * @deprecated marked deprecated to emphasize on the special character of this method
581
     *    which should only be used internally see #6673
582
     */
583
    @Deprecated
584
    public P presenter() {
585
        return getPresenter();
586
    }
587
}
(4-4/8)