Project

General

Profile

Download (20.2 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
        mainLayout.setWidthUndefined();
94

    
95
        fieldGroup = new BeanFieldGroup<>(dtoType);
96
        fieldGroup.addCommitHandler(new SaveHandler());
97

    
98
        setCompositionRoot(mainLayout);
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
                throw new CommitException("Failed to store data to backend", e);
252
            }
253
        }
254
    }
255

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

    
260

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

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

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

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

    
312
    }
313

    
314
    // ------------------------ field adding methods ------------------------ //
315

    
316

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
440
    protected abstract String getDefaultComponentStyles();
441

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

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

    
509

    
510
    public void withDeleteButton(boolean withDelete){
511

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

    
522

    
523
    // ------------------------ data binding ------------------------ //
524

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

    
531

    
532
    public final void loadInEditor(Object identifier) {
533

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

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

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

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

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

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