Project

General

Profile

Download (20.6 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.cdm.vaadin.component.TextFieldNFix;
48
import eu.etaxonomy.vaadin.component.NestedFieldGroup;
49
import eu.etaxonomy.vaadin.component.SwitchableTextField;
50
import eu.etaxonomy.vaadin.mvp.event.EditorDeleteEvent;
51
import eu.etaxonomy.vaadin.mvp.event.EditorPreSaveEvent;
52
import eu.etaxonomy.vaadin.mvp.event.EditorSaveEvent;
53
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent;
54
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent.Reason;
55

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

    
67
    public static final Logger logger = Logger.getLogger(AbstractPopupEditor.class);
68

    
69
    private BeanFieldGroup<DTO> fieldGroup;
70

    
71
    private VerticalLayout mainLayout;
72

    
73
    private Layout fieldLayout;
74

    
75
    private HorizontalLayout buttonLayout;
76

    
77
    private Button save;
78

    
79
    private Button cancel;
80

    
81
    private Button delete;
82

    
83
    private CssLayout toolBar = new CssLayout();
84

    
85
    private CssLayout toolBarButtonGroup = new CssLayout();
86

    
87
    private GridLayout _gridLayoutCache;
88

    
89
    private boolean isBeanLoaded;
90

    
91
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
92

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

    
100
        fieldGroup = new BeanFieldGroup<>(dtoType);
101
        fieldGroup.addCommitHandler(new SaveHandler());
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
        delete.setVisible(!readOnly);
171
        cancel.setCaption(readOnly ? "Close" : "Cancel");
172
    }
173

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

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

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

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

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

    
215
    }
216

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

    
226

    
227

    
228
    // ------------------------ event handler ------------------------ //
229

    
230
    private class SaveHandler implements CommitHandler {
231

    
232
        private static final long serialVersionUID = 2047223089707080659L;
233

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

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

    
261
    protected void addCommitHandler(CommitHandler commitHandler) {
262
        fieldGroup.addCommitHandler(commitHandler);
263
    }
264

    
265

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

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

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

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

    
317
    }
318

    
319
    // ------------------------ field adding methods ------------------------ //
320

    
321

    
322
    protected TextField addTextField(String caption, String propertyId) {
323
        return addField(new TextFieldNFix(caption), propertyId);
324
    }
325

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
445
    protected abstract String getDefaultComponentStyles();
446

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

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

    
514

    
515
    public void withDeleteButton(boolean withDelete){
516

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

    
527

    
528
    // ------------------------ data binding ------------------------ //
529

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

    
536

    
537
    public final void loadInEditor(Object identifier) {
538

    
539
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
540
        fieldGroup.setItemDataSource(beanToEdit);
541
        afterItemDataSourceSet();
542
        isBeanLoaded = true;
543
    }
544

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

    
554
    /**
555
     * @return true once the bean has been loaded indicating that all fields have
556
     *   been setup configured so that the editor is ready for use.
557
     */
558
    public boolean isBeanLoaded() {
559
        return isBeanLoaded;
560
    }
561

    
562
    /**
563
     * This method should only be used by the presenter of this view
564
     *
565
     * @param bean
566
     */
567
    protected void updateItemDataSource(DTO bean) {
568
        fieldGroup.getItemDataSource().setBean(bean);
569
    }
570

    
571
    /**
572
     * This method is called after setting the item data source whereby the
573
     * {@link FieldGroup#configureField(Field<?> field)} method will be called.
574
     * In this method all fields are set to default states defined for the fieldGroup.
575
     * <p>
576
     * You can now implement this method if you need to modify the state or value of individual fields.
577
     */
578
    protected void afterItemDataSourceSet() {
579
    }
580

    
581
    // ------------------------ issue related temporary solutions --------------------- //
582
    /**
583
     * Publicly accessible equivalent to getPreseneter(), needed for
584
     * managing the presenter listeners.
585
     * <p>
586
     * TODO: refactor the presenter listeners management to get rid of this method
587
     *
588
     * @return
589
     * @deprecated marked deprecated to emphasize on the special character of this method
590
     *    which should only be used internally see #6673
591
     */
592
    @Deprecated
593
    public P presenter() {
594
        return getPresenter();
595
    }
596

    
597
}
(4-4/8)