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
        mainLayout = new VerticalLayout();
91
        // IMPORTANT: mainLayout must be set to full size otherwise the
92
        // popup window may have problems with automatic resizing of its
93
        // content.
94
        mainLayout.setSizeFull();
95
        setCompositionRoot(mainLayout);
96

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

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

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

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

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

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

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

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

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

    
141
    protected VerticalLayout getMainLayout() {
142
        return mainLayout;
143
    }
144

    
145
    protected Layout getFieldLayout() {
146
        return fieldLayout;
147
    }
148

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

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

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

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

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

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

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

    
211
    }
212

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

    
222

    
223

    
224
    // ------------------------ event handler ------------------------ //
225

    
226
    private class SaveHandler implements CommitHandler {
227

    
228
        private static final long serialVersionUID = 2047223089707080659L;
229

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

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

    
257
    protected void addCommitHandler(CommitHandler commitHandler) {
258
        fieldGroup.addCommitHandler(commitHandler);
259
    }
260

    
261

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

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

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

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

    
313
    }
314

    
315
    // ------------------------ field adding methods ------------------------ //
316

    
317

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
441
    protected abstract String getDefaultComponentStyles();
442

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

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

    
510

    
511
    public void withDeleteButton(boolean withDelete){
512

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

    
523

    
524
    // ------------------------ data binding ------------------------ //
525

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

    
532

    
533
    public final void loadInEditor(Object identifier) {
534

    
535
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
536

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

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

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

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

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