Project

General

Profile

Download (36.1 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.ArrayList;
12
import java.util.Arrays;
13
import java.util.HashSet;
14
import java.util.List;
15
import java.util.Map;
16
import java.util.Optional;
17
import java.util.Set;
18
import java.util.Stack;
19

    
20
import org.apache.log4j.Level;
21
import org.apache.log4j.Logger;
22
import org.vaadin.spring.events.EventScope;
23

    
24
import com.vaadin.data.Validator.InvalidValueException;
25
import com.vaadin.data.fieldgroup.BeanFieldGroup;
26
import com.vaadin.data.fieldgroup.FieldGroup;
27
import com.vaadin.data.fieldgroup.FieldGroup.CommitEvent;
28
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
29
import com.vaadin.data.fieldgroup.FieldGroup.CommitHandler;
30
import com.vaadin.data.fieldgroup.FieldGroup.FieldGroupInvalidValueException;
31
import com.vaadin.server.AbstractErrorMessage.ContentMode;
32
import com.vaadin.server.ErrorMessage.ErrorLevel;
33
import com.vaadin.server.FontAwesome;
34
import com.vaadin.server.UserError;
35
import com.vaadin.shared.ui.MarginInfo;
36
import com.vaadin.ui.AbstractComponentContainer;
37
import com.vaadin.ui.AbstractField;
38
import com.vaadin.ui.AbstractLayout;
39
import com.vaadin.ui.AbstractOrderedLayout;
40
import com.vaadin.ui.Alignment;
41
import com.vaadin.ui.Button;
42
import com.vaadin.ui.Button.ClickListener;
43
import com.vaadin.ui.CheckBox;
44
import com.vaadin.ui.Component;
45
import com.vaadin.ui.CssLayout;
46
import com.vaadin.ui.Field;
47
import com.vaadin.ui.GridLayout;
48
import com.vaadin.ui.GridLayout.OutOfBoundsException;
49
import com.vaadin.ui.GridLayout.OverlapsException;
50
import com.vaadin.ui.HasComponents;
51
import com.vaadin.ui.HorizontalLayout;
52
import com.vaadin.ui.Label;
53
import com.vaadin.ui.Layout;
54
import com.vaadin.ui.Layout.MarginHandler;
55
import com.vaadin.ui.Notification;
56
import com.vaadin.ui.Notification.Type;
57
import com.vaadin.ui.PopupDateField;
58
import com.vaadin.ui.TextField;
59
import com.vaadin.ui.UI;
60
import com.vaadin.ui.VerticalLayout;
61
import com.vaadin.ui.themes.ValoTheme;
62

    
63
import eu.etaxonomy.cdm.database.PermissionDeniedException;
64
import eu.etaxonomy.cdm.vaadin.component.TextFieldNFix;
65
import eu.etaxonomy.cdm.vaadin.component.dialog.ContinueAlternativeCancelDialog;
66
import eu.etaxonomy.cdm.vaadin.event.EditorActionContext;
67
import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormat;
68
import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormatter;
69
import eu.etaxonomy.cdm.vaadin.ui.PopupEditorDefaultStatusMessageSource;
70
import eu.etaxonomy.vaadin.component.NestedFieldGroup;
71
import eu.etaxonomy.vaadin.component.SwitchableTextField;
72
import eu.etaxonomy.vaadin.event.FieldReplaceEvent;
73
import eu.etaxonomy.vaadin.mvp.event.EditorDeleteEvent;
74
import eu.etaxonomy.vaadin.mvp.event.EditorPreSaveEvent;
75
import eu.etaxonomy.vaadin.mvp.event.EditorSaveEvent;
76
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent;
77
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent.Reason;
78
import eu.etaxonomy.vaadin.util.PropertyIdPath;
79

    
80
/**
81
 *
82
 * Optional with a delete button which can be enabled with {@link #withDeleteButton(boolean)}
83
 *
84
 * @author a.kohlbecker
85
 * @since Apr 5, 2017
86
 *
87
 */
88
public abstract class AbstractPopupEditor<DTO extends Object, P extends AbstractEditorPresenter<DTO, ? extends ApplicationView>>
89
    extends AbstractPopupView<P> {
90

    
91
    /**
92
     *
93
     */
94
    private static final String READ_ONLY_MESSAGE_TEXT = "The editor is in read-only mode. Your authorities are not sufficient to edit this data.";
95

    
96
    public static final Logger logger = Logger.getLogger(AbstractPopupEditor.class);
97

    
98
    private BeanFieldGroup<DTO> fieldGroup;
99

    
100
    private VerticalLayout mainLayout;
101

    
102
    private Layout fieldLayout;
103

    
104
    private HorizontalLayout buttonLayout;
105

    
106
    private Button save;
107

    
108
    private Button cancel;
109

    
110
    private Button delete;
111

    
112
    private CssLayout toolBar = new CssLayout();
113

    
114
    private CssLayout toolBarButtonGroup = new CssLayout();
115

    
116
    private Label contextBreadcrumbsLabel = new Label();
117

    
118
    private Label statusMessageLabel = new Label();
119

    
120
    Set<String> statusMessages = new HashSet<>();
121

    
122
    private GridLayout _gridLayoutCache;
123

    
124
    private boolean isBeanLoaded;
125

    
126
    private Stack<EditorActionContext> context = new Stack<EditorActionContext>();
127

    
128
    private boolean isContextUpdated;
129

    
130
    private boolean isAdvancedMode = false;
131

    
132
    protected List<Component> advancedModeComponents = new ArrayList<>();
133

    
134
    private Button advancedModeButton;
135

    
136
    private EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator;
137

    
138
    private boolean withDeleteButton;
139

    
140
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
141

    
142
        mainLayout = new VerticalLayout();
143
        // IMPORTANT: mainLayout must be set to full size otherwise the
144
        // popup window may have problems with automatic resizing of its
145
        // content.
146
        mainLayout.setSizeFull();
147

    
148
        setCompositionRoot(mainLayout);
149

    
150
        fieldGroup = new BeanFieldGroup<>(dtoType);
151
        fieldGroup.addCommitHandler(new SaveHandler());
152

    
153
        toolBar.addStyleName(ValoTheme.WINDOW_TOP_TOOLBAR);
154
        toolBar.setWidth(100, Unit.PERCENTAGE);
155
        contextBreadcrumbsLabel.setId("context-breadcrumbs");
156
        contextBreadcrumbsLabel.setWidthUndefined();
157
        contextBreadcrumbsLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
158
        toolBar.addComponent(contextBreadcrumbsLabel);
159
        toolBarButtonGroup.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
160
        toolBarButtonGroup.setWidthUndefined();
161
        toolBar.addComponent(toolBarButtonGroup);
162
        toolBar.setVisible(false);
163

    
164
        fieldLayout = layout;
165
        fieldLayout.setWidthUndefined();
166
        if(fieldLayout instanceof AbstractOrderedLayout){
167
            ((AbstractOrderedLayout)fieldLayout).setSpacing(true);
168
        }
169
        if(MarginHandler.class.isAssignableFrom(fieldLayout.getClass())){
170
            ((MarginHandler)fieldLayout).setMargin(new MarginInfo(false, true, true, true));
171
        }
172

    
173
        buttonLayout = new HorizontalLayout();
174
        buttonLayout.setStyleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR);
175
        buttonLayout.setWidth(100, Unit.PERCENTAGE);
176
        buttonLayout.setSpacing(true);
177

    
178
        save = new Button("Save", FontAwesome.SAVE);
179
        save.setStyleName(ValoTheme.BUTTON_PRIMARY);
180
        save.addClickListener(e -> save());
181

    
182
        cancel = new Button("Cancel", FontAwesome.REMOVE);
183
        cancel.addClickListener(e -> cancelEditorDialog());
184

    
185
        delete = new Button("Delete", FontAwesome.TRASH);
186
        delete.setStyleName(ValoTheme.BUTTON_DANGER);
187
        delete.addClickListener(e -> delete());
188
        delete.setVisible(false);
189

    
190
        buttonLayout.addComponents(delete, save, cancel);
191
        // delete is initially invisible, let save take all space
192
        buttonLayout.setExpandRatio(save, 1);
193
        buttonLayout.setComponentAlignment(delete, Alignment.TOP_RIGHT);
194
        buttonLayout.setComponentAlignment(save, Alignment.TOP_RIGHT);
195
        buttonLayout.setComponentAlignment(cancel, Alignment.TOP_RIGHT);
196

    
197
        statusMessageLabel.setSizeFull();
198
        statusMessageLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
199

    
200
        HorizontalLayout statusMessageLayout = new HorizontalLayout();
201
        statusMessageLayout.setSizeFull();
202
        statusMessageLayout.addComponent(statusMessageLabel);
203
        statusMessageLayout.setMargin(new MarginInfo(false, true, false, true));
204

    
205
        mainLayout.addComponents(toolBar, fieldLayout, statusMessageLayout, buttonLayout);
206
        mainLayout.setComponentAlignment(statusMessageLayout, Alignment.BOTTOM_RIGHT);
207
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
208

    
209
        updateToolBarVisibility();
210

    
211
        UI currentUI = UI.getCurrent();
212
        if(PopupEditorDefaultStatusMessageSource.class.isAssignableFrom(currentUI.getClass())){
213
            String message = ((PopupEditorDefaultStatusMessageSource)currentUI).defaultStatusMarkup(this.getClass());
214
            addStatusMessage(message);
215
        }
216
    }
217

    
218
    protected VerticalLayout getMainLayout() {
219
        return mainLayout;
220
    }
221

    
222
    protected Layout getFieldLayout() {
223
        return fieldLayout;
224
    }
225

    
226
    /**
227
     * @return
228
     */
229
    private GridLayout gridLayout() {
230
        if(_gridLayoutCache == null){
231
            if(fieldLayout instanceof GridLayout){
232
                _gridLayoutCache = (GridLayout)fieldLayout;
233
            } else {
234
                throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
235
            }
236
        }
237
        return _gridLayoutCache;
238
    }
239

    
240
    @Override
241
    public void setReadOnly(boolean readOnly) {
242
        super.setReadOnly(readOnly);
243
        if(readOnly){
244
            statusMessageLabel.setValue(READ_ONLY_MESSAGE_TEXT);
245
            statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
246
        } else {
247
            statusMessageLabel.setValue(null);
248
        }
249
        statusMessageLabel.setVisible(readOnly);
250
        save.setVisible(!readOnly);
251
        updateDeleteButtonState();
252
        cancel.setCaption(readOnly ? "Close" : "Cancel");
253
        recursiveReadonly(readOnly, (AbstractComponentContainer)getFieldLayout());
254
    }
255

    
256
    /**
257
     * @param readOnly
258
     * @param layout
259
     */
260
    protected void recursiveReadonly(boolean readOnly, AbstractComponentContainer layout) {
261
        for(Component c : layout){
262
            c.setReadOnly(readOnly);
263
            if(c instanceof AbstractComponentContainer){
264
                recursiveReadonly(readOnly, (AbstractComponentContainer)c);
265
            }
266
        }
267
    }
268

    
269
    /**
270
     * @return
271
     * @return
272
     */
273
    protected AbstractLayout getToolBar() {
274
        return toolBar;
275
    }
276

    
277
    /**
278
     * @return
279
     * @return
280
     */
281
    protected void toolBarAdd(Component c) {
282
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
283
        updateToolBarVisibility();
284
    }
285

    
286
    /**
287
     * @return
288
     * @return
289
     */
290
    protected void toolBarButtonGroupAdd(Component c) {
291
        toolBarButtonGroup.addComponent(c);
292
        updateToolBarVisibility();
293
    }
294

    
295
    /**
296
     * @return
297
     * @return
298
     */
299
    protected void toolBarButtonGroupRemove(Component c) {
300
        toolBarButtonGroup.removeComponent(c);
301
        updateToolBarVisibility();
302
    }
303

    
304
    /**
305
     *
306
     */
307
    private void updateToolBarVisibility() {
308
        boolean showToolbar = toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1;
309
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
310
        if(!showToolbar){
311
            mainLayout.setMargin(new MarginInfo(true, false, false, false));
312
        } else {
313
            mainLayout.setMargin(false);
314
        }
315

    
316
    }
317

    
318
    /**
319
     * The top tool-bar is initially invisible.
320
     *
321
     * @param visible
322
     */
323
    protected void setToolBarVisible(boolean visible){
324
        toolBar.setVisible(true);
325
    }
326

    
327
    /**
328
     * @return the isAdvancedMode
329
     */
330
    public boolean isAdvancedMode() {
331
        return isAdvancedMode;
332
    }
333

    
334
    /**
335
     * @param isAdvancedMode the isAdvancedMode to set
336
     */
337
    public void setAdvancedMode(boolean isAdvancedMode) {
338
        this.isAdvancedMode = isAdvancedMode;
339
        advancedModeComponents.forEach(c -> c.setVisible(isAdvancedMode));
340
    }
341

    
342
    public void setAdvancedModeEnabled(boolean activate){
343
        if(activate && advancedModeButton == null){
344
            advancedModeButton = new Button(FontAwesome.WRENCH); // FontAwesome.FLASK
345
            advancedModeButton.setIconAlternateText("Advanced mode");
346
            advancedModeButton.addStyleName(ValoTheme.BUTTON_TINY);
347
            toolBarButtonGroupAdd(advancedModeButton);
348
            advancedModeButton.addClickListener(e -> {
349
                setAdvancedMode(!isAdvancedMode);
350
                }
351
            );
352

    
353
        } else if(advancedModeButton != null) {
354
            toolBarButtonGroupRemove(advancedModeButton);
355
            advancedModeButton = null;
356
        }
357
    }
358

    
359
    public void registerAdvancedModeComponents(Component ... c){
360
        advancedModeComponents.addAll(Arrays.asList(c));
361
    }
362

    
363

    
364
    // ------------------------ event handler ------------------------ //
365

    
366
    private class SaveHandler implements CommitHandler {
367

    
368
        private static final long serialVersionUID = 2047223089707080659L;
369

    
370
        @Override
371
        public void preCommit(CommitEvent commitEvent) throws CommitException {
372
            logger.debug("preCommit(), publishing EditorPreSaveEvent");
373
            // notify the presenter to start a transaction
374
            viewEventBus.publish(this, new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
375
        }
376

    
377
        @Override
378
        public void postCommit(CommitEvent commitEvent) throws CommitException {
379
            try {
380
                if(logger.isTraceEnabled()){
381
                    logger.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
382
                }
383
                // notify the presenter to persist the bean and to commit the transaction
384
                viewEventBus.publish(this, new EditorSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
385
                if(logger.isTraceEnabled()){
386
                    logger.trace("postCommit() publishing DoneWithPopupEvent");
387
                }
388
                // notify the NavigationManagerBean to close the window and to dispose the view
389
                viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
390
            } catch (Exception e) {
391
                logger.error(e);
392
                throw new CommitException("Failed to store data to backend", e);
393
            }
394
        }
395
    }
396

    
397
    protected void addCommitHandler(CommitHandler commitHandler) {
398
        fieldGroup.addCommitHandler(commitHandler);
399
    }
400

    
401
    protected void cancelEditorDialog(){
402

    
403
        if(fieldGroup.isModified()){
404

    
405
            ContinueAlternativeCancelDialog editorModifiedDialog = new ContinueAlternativeCancelDialog(
406
                    "Cancel editor",
407
                    "<p>The editor has been modified.<br>Do you want to save your changes or discard them?<p>",
408
                    "Discard",
409
                    "Save");
410
            ClickListener saveListener = e -> {editorModifiedDialog.close(); save();};
411
            ClickListener discardListener = e -> {editorModifiedDialog.close(); cancel();};
412
            ClickListener cancelListener = e -> editorModifiedDialog.close();
413
            editorModifiedDialog.addAlternativeClickListener(saveListener);
414
            editorModifiedDialog.addContinueClickListener(discardListener);
415
            editorModifiedDialog.addCancelClickListener(cancelListener);
416

    
417
            UI.getCurrent().addWindow(editorModifiedDialog);
418
        } else {
419
            cancel();
420
        }
421
    }
422

    
423

    
424
    /**
425
     * Cancel editing and discard all modifications.
426
     */
427
    @Override
428
    public void cancel() {
429
        fieldGroup.discard();
430
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.CANCEL));
431
    }
432

    
433
    /**
434
     * @return
435
     */
436
    private void delete() {
437
        viewEventBus.publish(this, new EditorDeleteEvent<DTO>(this, fieldGroup.getItemDataSource().getBean()));
438
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.DELETE));
439
    }
440

    
441
    /**
442
     * Save the changes made in the editor.
443
     */
444
    private void save() {
445
        try {
446
            fieldGroup.commit();
447
        } catch (CommitException e) {
448
            fieldGroup.getFields().forEach(f -> ((AbstractField<?>)f).setValidationVisible(true));
449
            Throwable cause = e.getCause();
450
            while(cause != null) {
451
                if(cause instanceof FieldGroupInvalidValueException){
452
                    FieldGroupInvalidValueException invalidValueException = (FieldGroupInvalidValueException)cause;
453
                    updateFieldNotifications(invalidValueException.getInvalidFields());
454
                    int invalidFieldsCount = invalidValueException.getInvalidFields().size();
455
                    Notification.show("The entered data in " + invalidFieldsCount + " field" + (invalidFieldsCount > 1 ? "s": "") + " is incomplete or invalid.");
456
                    break;
457
                } else if(cause instanceof PermissionDeniedException){
458
                    PermissionDeniedException permissionDeniedException = (PermissionDeniedException)cause;
459
                    Notification.show("Permission denied", permissionDeniedException.getMessage(), Type.ERROR_MESSAGE);
460
                    break;
461
                }
462
                cause = cause.getCause();
463
            }
464
            if(cause == null){
465
                // no known exception type found
466
                logger.error(e);
467
                PopupEditorException pee = null;
468
                try {
469
                    pee  = new PopupEditorException("Error saving popup editor", this, e);
470
                } catch (Throwable t) {
471
                    /* IGORE errors which happen during the construction of the PopupEditorException */
472
                }
473
                if(pee != null){
474
                    throw pee;
475
                }
476
                throw new RuntimeException(e);
477
            }
478

    
479
        }
480
    }
481

    
482
    /**
483
     * @param invalidFields
484
     */
485
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
486
        for(Field<?> f : invalidFields.keySet()){
487
            if(f instanceof AbstractField){
488
                String message = invalidFields.get(f).getHtmlMessage();
489
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
490
            }
491
        }
492

    
493
    }
494

    
495
    // ------------------------ field adding methods ------------------------ //
496

    
497

    
498
    protected TextField addTextField(String caption, String propertyId) {
499
        return addField(new TextFieldNFix(caption), propertyId);
500
    }
501

    
502
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
503
            int column2, int row2)
504
            throws OverlapsException, OutOfBoundsException {
505
        return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
506
    }
507

    
508
    protected TextField addTextField(String caption, String propertyId, int column, int row)
509
            throws OverlapsException, OutOfBoundsException {
510
        return addField(new TextFieldNFix(caption), propertyId, column, row);
511
    }
512

    
513
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
514
            int column2, int row2)
515
            throws OverlapsException, OutOfBoundsException {
516

    
517
        SwitchableTextField field = new SwitchableTextField(caption);
518
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
519
        addComponent(field, column1, row1, column2, row2);
520
        return field;
521
    }
522

    
523
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
524
            throws OverlapsException, OutOfBoundsException {
525

    
526
        SwitchableTextField field = new SwitchableTextField(caption);
527
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
528
        addComponent(field, column, row);
529
        return field;
530
    }
531

    
532
    protected PopupDateField addDateField(String caption, String propertyId) {
533
        return addField(new PopupDateField(caption), propertyId);
534
    }
535

    
536
    protected CheckBox addCheckBox(String caption, String propertyId) {
537
        return addField(new CheckBox(caption), propertyId);
538
    }
539

    
540
    protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
541
        return addField(new CheckBox(caption), propertyId, column, row);
542
    }
543

    
544
    protected <T extends Field> T addField(T field, String propertyId) {
545
        fieldGroup.bind(field, propertyId);
546
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
547
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
548
        }
549
        addComponent(field);
550
        return field;
551
    }
552

    
553
    /**
554
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
555
     *
556
     * @param field
557
     *            the field to be added, not <code>null</code>.
558
     * @param propertyId
559
     * @param column
560
     *            the column index, starting from 0.
561
     * @param row
562
     *            the row index, starting from 0.
563
     * @throws OverlapsException
564
     *             if the new component overlaps with any of the components
565
     *             already in the grid.
566
     * @throws OutOfBoundsException
567
     *             if the cell is outside the grid area.
568
     */
569
    protected <T extends Field> T addField(T field, String propertyId, int column, int row)
570
            throws OverlapsException, OutOfBoundsException {
571
        fieldGroup.bind(field, propertyId);
572
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
573
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
574
        }
575
        addComponent(field, column, row);
576
        return field;
577
    }
578

    
579
    /**
580
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
581
     *
582
     * @param field
583
     * @param propertyId
584
     * @param column1
585
     * @param row1
586
     * @param column2
587
     * @param row2
588
     * @return
589
     * @throws OverlapsException
590
     * @throws OutOfBoundsException
591
     */
592
    protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
593
            int column2, int row2)
594
            throws OverlapsException, OutOfBoundsException {
595
        if(propertyId != null){
596
            fieldGroup.bind(field, propertyId);
597
            if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
598
                ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
599
            }
600
        }
601
        addComponent(field, column1, row1, column2, row2);
602
        return field;
603
    }
604

    
605
    protected Field<?> getField(Object propertyId){
606
        return fieldGroup.getField(propertyId);
607
    }
608

    
609
    public PropertyIdPath boundPropertyIdPath(Field<?> field){
610

    
611
        PropertyIdPath propertyIdPath = null;
612
        Object propertyId = fieldGroup.getPropertyId(field);
613

    
614
        if(propertyId == null){
615
            // not found in the editor field group. Maybe the field is bound to a nested fieldgroup?
616
            // 1. find the NestedFieldGroup implementations from the field up to the editor
617
            logger.setLevel(Level.DEBUG);
618
            PropertyIdPath nestedPropertyIds = new PropertyIdPath();
619
            Field parentField = field;
620
            HasComponents parentComponent = parentField.getParent();
621
            logger.debug("field: " + parentField.getClass().getSimpleName());
622
            while(parentComponent != null){
623
                logger.debug("parentComponent: " + parentComponent.getClass().getSimpleName());
624
                if(NestedFieldGroup.class.isAssignableFrom(parentComponent.getClass()) && AbstractField.class.isAssignableFrom(parentComponent.getClass())){
625
                    Optional<FieldGroup> parentFieldGroup = ((NestedFieldGroup)parentComponent).getFieldGroup();
626
                    if(parentFieldGroup.isPresent()){
627
                        Object propId = parentFieldGroup.get().getPropertyId(parentField);
628
                        if(propId != null){
629
                            logger.debug("propId: " + propId.toString());
630
                            nestedPropertyIds.addParent(propId);
631
                        }
632
                        logger.debug("parentField: " + parentField.getClass().getSimpleName());
633
                        parentField = (Field)parentComponent;
634
                    } else {
635
                        logger.debug("parentFieldGroup is null, continuing ...");
636
                    }
637
                } else if(parentComponent == this) {
638
                    // we reached the editor itself
639
                    Object propId = fieldGroup.getPropertyId(parentField);
640
                    if(propId != null){
641
                        logger.debug("propId: " + propId.toString());
642
                        nestedPropertyIds.addParent(propId);
643
                    }
644
                    propertyIdPath = nestedPropertyIds;
645
                    break;
646
                }
647
                parentComponent = parentComponent.getParent();
648
            }
649
            // 2. check the NestedFieldGroup binding the field is direct or indirect child component of the editor
650
//            NO lONGER NEEDED
651
//            parentComponent = parentField.getParent(); // get component containing the last parent field found
652
//            while(true){
653
//                if(parentComponent == getFieldLayout()){
654
//                    propertyIdPath = nestedPropertyIds;
655
//                    break;
656
//                }
657
//                parentComponent = parentComponent.getParent();
658
//            }
659
        } else {
660
            propertyIdPath = new PropertyIdPath(propertyId);
661
        }
662
        return propertyIdPath;
663
    }
664

    
665
    protected void addComponent(Component component) {
666
        fieldLayout.addComponent(component);
667
        applyDefaultComponentStyles(component);
668
    }
669

    
670
    protected void bindField(Field field, String propertyId){
671
        fieldGroup.bind(field, propertyId);
672
    }
673

    
674
    protected void unbindField(Field field){
675
        fieldGroup.unbind(field);
676
    }
677

    
678
    /**
679
     * @param component
680
     */
681
    public void applyDefaultComponentStyles(Component component) {
682
        component.addStyleName(getDefaultComponentStyles());
683
    }
684

    
685
    protected abstract String getDefaultComponentStyles();
686

    
687
    /**
688
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
689
     * <p>
690
     * Adds the component to the grid in cells column1,row1 (NortWest corner of
691
     * the area.) End coordinates (SouthEast corner of the area) are the same as
692
     * column1,row1. The coordinates are zero-based. Component width and height
693
     * is 1.
694
     *
695
     * @param component
696
     *            the component to be added, not <code>null</code>.
697
     * @param column
698
     *            the column index, starting from 0.
699
     * @param row
700
     *            the row index, starting from 0.
701
     * @throws OverlapsException
702
     *             if the new component overlaps with any of the components
703
     *             already in the grid.
704
     * @throws OutOfBoundsException
705
     *             if the cell is outside the grid area.
706
     */
707
    public void addComponent(Component component, int column, int row)
708
            throws OverlapsException, OutOfBoundsException {
709
        applyDefaultComponentStyles(component);
710
        gridLayout().addComponent(component, column, row, column, row);
711
    }
712

    
713
    /**
714
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
715
     * <p>
716
     * Adds a component to the grid in the specified area. The area is defined
717
     * by specifying the upper left corner (column1, row1) and the lower right
718
     * corner (column2, row2) of the area. The coordinates are zero-based.
719
     * </p>
720
     *
721
     * <p>
722
     * If the area overlaps with any of the existing components already present
723
     * in the grid, the operation will fail and an {@link OverlapsException} is
724
     * thrown.
725
     * </p>
726
     *
727
     * @param component
728
     *            the component to be added, not <code>null</code>.
729
     * @param column1
730
     *            the column of the upper left corner of the area <code>c</code>
731
     *            is supposed to occupy. The leftmost column has index 0.
732
     * @param row1
733
     *            the row of the upper left corner of the area <code>c</code> is
734
     *            supposed to occupy. The topmost row has index 0.
735
     * @param column2
736
     *            the column of the lower right corner of the area
737
     *            <code>c</code> is supposed to occupy.
738
     * @param row2
739
     *            the row of the lower right corner of the area <code>c</code>
740
     *            is supposed to occupy.
741
     * @throws OverlapsException
742
     *             if the new component overlaps with any of the components
743
     *             already in the grid.
744
     * @throws OutOfBoundsException
745
     *             if the cells are outside the grid area.
746
     */
747
    public void addComponent(Component component, int column1, int row1,
748
            int column2, int row2)
749
            throws OverlapsException, OutOfBoundsException {
750
        applyDefaultComponentStyles(component);
751
        gridLayout().addComponent(component, column1, row1, column2, row2);
752
    }
753

    
754
    public void setSaveButtonEnabled(boolean enabled){
755
        save.setEnabled(enabled);
756
    }
757

    
758
    public void withDeleteButton(boolean withDelete){
759

    
760
        this.withDeleteButton = withDelete;
761
        if(withDeleteButton){
762
            buttonLayout.setExpandRatio(save, 0);
763
            buttonLayout.setExpandRatio(delete, 1);
764
        } else {
765
            buttonLayout.setExpandRatio(save, 1);
766
            buttonLayout.setExpandRatio(delete, 0);
767
        }
768
        updateDeleteButtonState();
769
    }
770

    
771
    /**
772
     * @param withDelete
773
     */
774
    private void updateDeleteButtonState() {
775
        delete.setVisible(withDeleteButton && !isReadOnly());
776
    }
777

    
778
    public boolean addStatusMessage(String message){
779
        boolean returnVal = statusMessages.add(message);
780
        updateStatusLabel();
781
        return returnVal;
782
    }
783

    
784
    public boolean removeStatusMessage(String message){
785
        boolean returnVal = statusMessages.remove(message);
786
        updateStatusLabel();
787
        return returnVal;
788
    }
789

    
790
    /**
791
     *
792
     */
793
    private void updateStatusLabel() {
794
        String text = "";
795
        for(String s : statusMessages){
796
            text += s + "</br>";
797
        }
798
        statusMessageLabel.setValue(text);
799
        statusMessageLabel.setVisible(!text.isEmpty());
800
        statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
801
    }
802

    
803
    private void updateContextBreadcrumbs() {
804

    
805
        List<EditorActionContext> contextInfo = new ArrayList<>(getEditorActionContext());
806
        String breadcrumbs = "";
807
        EditorActionContextFormatter formatter = new EditorActionContextFormatter();
808

    
809
        int cnt = 0;
810
        for(EditorActionContext cntxt : contextInfo){
811
            cnt++;
812
            boolean isLast = cnt == contextInfo.size();
813
            boolean isFirst = cnt == 1;
814

    
815
            boolean doClass = false; // will be removed in future
816
            boolean classNameForMissingPropertyPath = true; // !doClass;
817
            boolean doProperties = true;
818
            boolean doCreateOrNew = !isFirst;
819
            String contextmarkup = formatter.format(
820
                    cntxt,
821
                    new EditorActionContextFormat(doClass, doProperties, classNameForMissingPropertyPath, doCreateOrNew,
822
                            EditorActionContextFormat.TargetInfoType.FIELD_CAPTION, (isLast ? "active" : ""))
823
                    );
824
//            if(!isLast){
825
//                contextmarkup += " " + FontAwesome.ANGLE_RIGHT.getHtml() + " ";
826
//            }
827
            if(isLast){
828
                contextmarkup = "<li><span class=\"crumb active\">" + contextmarkup + "</span></li>";
829
            } else {
830
                contextmarkup = "<li><span class=\"crumb\">" + contextmarkup + "</span></li>";
831
            }
832
            breadcrumbs += contextmarkup;
833
        }
834
        contextBreadcrumbsLabel.setValue("<ul class=\"breadcrumbs\">" + breadcrumbs + "</ul>");
835
    }
836

    
837
    // ------------------------ data binding ------------------------ //
838

    
839
    protected void bindDesign(Component component) {
840
        fieldLayout.removeAllComponents();
841
        fieldGroup.bindMemberFields(component);
842
        fieldLayout.addComponent(component);
843
    }
844

    
845

    
846
    public final void loadInEditor(Object identifier) {
847

    
848
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
849
        fieldGroup.setItemDataSource(beanToEdit);
850
        afterItemDataSourceSet();
851
        getPresenter().onViewFormReady(beanToEdit);
852
        updateContextBreadcrumbs();
853
        isBeanLoaded = true;
854
    }
855

    
856
    /**
857
     * Passes the beanInstantiator to the presenter method {@link AbstractEditorPresenter#setBeanInstantiator(BeanInstantiator)}
858
     *
859
     * @param beanInstantiator
860
     */
861
    public final void setBeanInstantiator(BeanInstantiator<DTO> beanInstantiator) {
862
        if(AbstractCdmEditorPresenter.class.isAssignableFrom(getPresenter().getClass())){
863
            ((CdmEditorPresenterBase)getPresenter()).setBeanInstantiator(beanInstantiator);
864
        } else {
865
            throw new RuntimeException("BeanInstantiator can only be set for popup editors with a peresenter of the type CdmEditorPresenterBase");
866
        }
867
    }
868

    
869
    /**
870
     * Returns the bean contained in the itemDatasource of the fieldGroup.
871
     *
872
     * @return
873
     */
874
    public DTO getBean() {
875
        if(fieldGroup.getItemDataSource() != null){
876
            return fieldGroup.getItemDataSource().getBean();
877

    
878
        }
879
        return null;
880
    }
881

    
882
    /**
883
     * @return true once the bean has been loaded indicating that all fields have
884
     *   been setup configured so that the editor is ready for use.
885
     */
886
    public boolean isBeanLoaded() {
887
        return isBeanLoaded;
888
    }
889

    
890
    /**
891
     * This method should only be used by the presenter of this view
892
     *
893
     * @param bean
894
     */
895
    protected void updateItemDataSource(DTO bean) {
896
        fieldGroup.getItemDataSource().setBean(bean);
897
    }
898

    
899
    /**
900
     * This method is called after setting the item data source whereby the
901
     * {@link FieldGroup#configureField(Field<?> field)} method will be called.
902
     * In this method all fields are set to default states defined for the fieldGroup.
903
     * <p>
904
     * You can now implement this method if you need to modify the state or value of individual fields.
905
     */
906
    protected void afterItemDataSourceSet() {
907
        if(editorComponentsConfigurator != null){
908
            editorComponentsConfigurator.updateComponentStates(this);
909
        }
910
    }
911

    
912

    
913
    // ------------------------ issue related temporary solutions --------------------- //
914
    /**
915
     * Publicly accessible equivalent to getPreseneter(), needed for
916
     * managing the presenter listeners.
917
     * <p>
918
     * TODO: refactor the presenter listeners management to get rid of this method
919
     *
920
     * @return
921
     * @deprecated marked deprecated to emphasize on the special character of this method
922
     *    which should only be used internally see #6673
923
     */
924
    @Deprecated
925
    public P presenter() {
926
        return getPresenter();
927
    }
928

    
929
    /**
930
     * Returns the context of editor actions for this editor.
931
     * The context submitted with {@link #setParentContext(Stack)} will be updated
932
     * to represent the current context.
933
     *
934
     * @return the context
935
     */
936
    public Stack<EditorActionContext> getEditorActionContext() {
937
        if(!isContextUpdated){
938
            if(getBean() == null){
939
                throw new RuntimeException("getContext() is only possible after the bean is loaded");
940
            }
941
            context.push(new EditorActionContext(getBean(), this));
942
            isContextUpdated = true;
943
        }
944
        return context;
945
    }
946

    
947
    /**
948
     * Set the context of editor actions parent to this editor
949
     *
950
     * @param context the context to set
951
     */
952
    public void setParentEditorActionContext(Stack<EditorActionContext> context, Field<?> targetField) {
953
        if(context != null){
954
            this.context.addAll(context);
955
        }
956
        if(targetField != null){
957
            this.context.get(context.size() - 1).setTargetField(targetField);
958
        }
959
    }
960

    
961
    protected AbstractField<String> replaceComponent(String propertyId, AbstractField<String> oldField, AbstractField<String> newField, int column1, int row1, int column2,
962
            int row2) {
963
                String value = oldField.getValue();
964
                newField.setCaption(oldField.getCaption());
965
                GridLayout grid = (GridLayout)getFieldLayout();
966
                grid.removeComponent(oldField);
967

    
968
                unbindField(oldField);
969
                addField(newField, propertyId, column1, row1, column2, row2);
970
                getViewEventBus().publish(this, new FieldReplaceEvent(this, oldField, newField));
971
                // important: set newField value at last!
972
                newField.setValue(value);
973
                return newField;
974
            }
975

    
976
    public EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> getEditorComponentsConfigurator() {
977
        return editorComponentsConfigurator;
978
    }
979

    
980
    public void setEditorComponentsConfigurator(
981
            EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator) {
982
        this.editorComponentsConfigurator = editorComponentsConfigurator;
983
    }
984

    
985
}
(6-6/15)