Project

General

Profile

Download (35.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.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.Set;
17
import java.util.Stack;
18

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

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

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

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

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

    
93
    public static final Logger logger = Logger.getLogger(AbstractPopupEditor.class);
94

    
95
    private BeanFieldGroup<DTO> fieldGroup;
96

    
97
    private VerticalLayout mainLayout;
98

    
99
    private Layout fieldLayout;
100

    
101
    private HorizontalLayout buttonLayout;
102

    
103
    private Button save;
104

    
105
    private Button cancel;
106

    
107
    private Button delete;
108

    
109
    private CssLayout toolBar = new CssLayout();
110

    
111
    private CssLayout toolBarButtonGroup = new CssLayout();
112

    
113
    private Label contextBreadcrumbsLabel = new Label();
114

    
115
    private Label statusMessageLabel = new Label();
116

    
117
    Set<String> statusMessages = new HashSet<>();
118

    
119
    private GridLayout _gridLayoutCache;
120

    
121
    private boolean isBeanLoaded;
122

    
123
    private Stack<EditorActionContext> context = new Stack<EditorActionContext>();
124

    
125
    private boolean isContextUpdated;
126

    
127
    private boolean isAdvancedMode = false;
128

    
129
    protected List<Component> advancedModeComponents = new ArrayList<>();
130

    
131
    private Button advancedModeButton;
132

    
133
    private EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator;
134

    
135
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
136

    
137
        mainLayout = new VerticalLayout();
138
        // IMPORTANT: mainLayout must be set to full size otherwise the
139
        // popup window may have problems with automatic resizing of its
140
        // content.
141
        mainLayout.setSizeFull();
142

    
143
        setCompositionRoot(mainLayout);
144

    
145
        fieldGroup = new BeanFieldGroup<>(dtoType);
146
        fieldGroup.addCommitHandler(new SaveHandler());
147

    
148
        toolBar.addStyleName(ValoTheme.WINDOW_TOP_TOOLBAR);
149
        toolBar.setWidth(100, Unit.PERCENTAGE);
150
        contextBreadcrumbsLabel.setId("context-breadcrumbs");
151
        contextBreadcrumbsLabel.setWidthUndefined();
152
        contextBreadcrumbsLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
153
        toolBar.addComponent(contextBreadcrumbsLabel);
154
        toolBarButtonGroup.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
155
        toolBarButtonGroup.setWidthUndefined();
156
        toolBar.addComponent(toolBarButtonGroup);
157
        toolBar.setVisible(false);
158

    
159
        fieldLayout = layout;
160
        fieldLayout.setWidthUndefined();
161
        if(fieldLayout instanceof AbstractOrderedLayout){
162
            ((AbstractOrderedLayout)fieldLayout).setSpacing(true);
163
        }
164
        if(MarginHandler.class.isAssignableFrom(fieldLayout.getClass())){
165
            ((MarginHandler)fieldLayout).setMargin(new MarginInfo(false, true, true, true));
166
        }
167

    
168
        buttonLayout = new HorizontalLayout();
169
        buttonLayout.setStyleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR);
170
        buttonLayout.setWidth(100, Unit.PERCENTAGE);
171
        buttonLayout.setSpacing(true);
172

    
173
        save = new Button("Save", FontAwesome.SAVE);
174
        save.setStyleName(ValoTheme.BUTTON_PRIMARY);
175
        save.addClickListener(e -> save());
176

    
177
        cancel = new Button("Cancel", FontAwesome.REMOVE);
178
        cancel.addClickListener(e -> cancelEditorDialog());
179

    
180
        delete = new Button("Delete", FontAwesome.TRASH);
181
        delete.setStyleName(ValoTheme.BUTTON_DANGER);
182
        delete.addClickListener(e -> delete());
183
        delete.setVisible(false);
184

    
185
        buttonLayout.addComponents(delete, save, cancel);
186
        // delete is initially invisible, let save take all space
187
        buttonLayout.setExpandRatio(save, 1);
188
        buttonLayout.setComponentAlignment(delete, Alignment.TOP_RIGHT);
189
        buttonLayout.setComponentAlignment(save, Alignment.TOP_RIGHT);
190
        buttonLayout.setComponentAlignment(cancel, Alignment.TOP_RIGHT);
191

    
192
        statusMessageLabel.setWidthUndefined();
193

    
194
        mainLayout.addComponents(toolBar, fieldLayout, statusMessageLabel, buttonLayout);
195
        mainLayout.setComponentAlignment(statusMessageLabel, Alignment.BOTTOM_RIGHT);
196
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
197

    
198
        updateToolBarVisibility();
199
    }
200

    
201
    protected VerticalLayout getMainLayout() {
202
        return mainLayout;
203
    }
204

    
205
    protected Layout getFieldLayout() {
206
        return fieldLayout;
207
    }
208

    
209
    /**
210
     * @return
211
     */
212
    private GridLayout gridLayout() {
213
        if(_gridLayoutCache == null){
214
            if(fieldLayout instanceof GridLayout){
215
                _gridLayoutCache = (GridLayout)fieldLayout;
216
            } else {
217
                throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
218
            }
219
        }
220
        return _gridLayoutCache;
221
    }
222

    
223
    @Override
224
    public void setReadOnly(boolean readOnly) {
225
        super.setReadOnly(readOnly);
226
        if(readOnly){
227
            statusMessageLabel.setValue(READ_ONLY_MESSAGE_TEXT);
228
            statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
229
        } else {
230
            statusMessageLabel.setValue(null);
231
        }
232
        statusMessageLabel.setVisible(readOnly);
233
        save.setVisible(!readOnly);
234
        delete.setVisible(!readOnly);
235
        cancel.setCaption(readOnly ? "Close" : "Cancel");
236
        recursiveReadonly(readOnly, (AbstractComponentContainer)getFieldLayout());
237
    }
238

    
239
    /**
240
     * @param readOnly
241
     * @param layout
242
     */
243
    protected void recursiveReadonly(boolean readOnly, AbstractComponentContainer layout) {
244
        for(Component c : layout){
245
            c.setReadOnly(readOnly);
246
            if(c instanceof AbstractComponentContainer){
247
                recursiveReadonly(readOnly, (AbstractComponentContainer)c);
248
            }
249
        }
250
    }
251

    
252
    /**
253
     * @return
254
     * @return
255
     */
256
    protected AbstractLayout getToolBar() {
257
        return toolBar;
258
    }
259

    
260
    /**
261
     * @return
262
     * @return
263
     */
264
    protected void toolBarAdd(Component c) {
265
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
266
        updateToolBarVisibility();
267
    }
268

    
269
    /**
270
     * @return
271
     * @return
272
     */
273
    protected void toolBarButtonGroupAdd(Component c) {
274
        toolBarButtonGroup.addComponent(c);
275
        updateToolBarVisibility();
276
    }
277

    
278
    /**
279
     * @return
280
     * @return
281
     */
282
    protected void toolBarButtonGroupRemove(Component c) {
283
        toolBarButtonGroup.removeComponent(c);
284
        updateToolBarVisibility();
285
    }
286

    
287
    /**
288
     *
289
     */
290
    private void updateToolBarVisibility() {
291
        boolean showToolbar = toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1;
292
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
293
        if(!showToolbar){
294
            mainLayout.setMargin(new MarginInfo(true, false, false, false));
295
        } else {
296
            mainLayout.setMargin(false);
297
        }
298

    
299
    }
300

    
301
    /**
302
     * The top tool-bar is initially invisible.
303
     *
304
     * @param visible
305
     */
306
    protected void setToolBarVisible(boolean visible){
307
        toolBar.setVisible(true);
308
    }
309

    
310
    /**
311
     * @return the isAdvancedMode
312
     */
313
    public boolean isAdvancedMode() {
314
        return isAdvancedMode;
315
    }
316

    
317
    /**
318
     * @param isAdvancedMode the isAdvancedMode to set
319
     */
320
    public void setAdvancedMode(boolean isAdvancedMode) {
321
        this.isAdvancedMode = isAdvancedMode;
322
        advancedModeComponents.forEach(c -> c.setVisible(isAdvancedMode));
323
    }
324

    
325
    public void setAdvancedModeEnabled(boolean activate){
326
        if(activate && advancedModeButton == null){
327
            advancedModeButton = new Button(FontAwesome.WRENCH); // FontAwesome.FLASK
328
            advancedModeButton.setIconAlternateText("Advanced mode");
329
            advancedModeButton.addStyleName(ValoTheme.BUTTON_TINY);
330
            toolBarButtonGroupAdd(advancedModeButton);
331
            advancedModeButton.addClickListener(e -> {
332
                setAdvancedMode(!isAdvancedMode);
333
                }
334
            );
335

    
336
        } else if(advancedModeButton != null) {
337
            toolBarButtonGroupRemove(advancedModeButton);
338
            advancedModeButton = null;
339
        }
340
    }
341

    
342
    public void registerAdvancedModeComponents(Component ... c){
343
        advancedModeComponents.addAll(Arrays.asList(c));
344
    }
345

    
346

    
347
    // ------------------------ event handler ------------------------ //
348

    
349
    private class SaveHandler implements CommitHandler {
350

    
351
        private static final long serialVersionUID = 2047223089707080659L;
352

    
353
        @Override
354
        public void preCommit(CommitEvent commitEvent) throws CommitException {
355
            logger.debug("preCommit(), publishing EditorPreSaveEvent");
356
            // notify the presenter to start a transaction
357
            viewEventBus.publish(this, new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
358
        }
359

    
360
        @Override
361
        public void postCommit(CommitEvent commitEvent) throws CommitException {
362
            try {
363
                if(logger.isTraceEnabled()){
364
                    logger.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
365
                }
366
                // notify the presenter to persist the bean and to commit the transaction
367
                viewEventBus.publish(this, new EditorSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
368
                if(logger.isTraceEnabled()){
369
                    logger.trace("postCommit() publishing DoneWithPopupEvent");
370
                }
371
                // notify the NavigationManagerBean to close the window and to dispose the view
372
                viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
373
            } catch (Exception e) {
374
                logger.error(e);
375
                throw new CommitException("Failed to store data to backend", e);
376
            }
377
        }
378
    }
379

    
380
    protected void addCommitHandler(CommitHandler commitHandler) {
381
        fieldGroup.addCommitHandler(commitHandler);
382
    }
383

    
384
    protected void cancelEditorDialog(){
385

    
386
        if(fieldGroup.isModified()){
387

    
388
            ContinueAlternativeCancelDialog editorModifiedDialog = new ContinueAlternativeCancelDialog(
389
                    "Cancel editor",
390
                    "<p>The editor has been modified.<br>Do you want to save your changes or discard them?<p>",
391
                    "Discard",
392
                    "Save");
393
            ClickListener saveListener = e -> {editorModifiedDialog.close(); save();};
394
            ClickListener discardListener = e -> {editorModifiedDialog.close(); cancel();};
395
            ClickListener cancelListener = e -> editorModifiedDialog.close();
396
            editorModifiedDialog.addAlternativeClickListener(saveListener);
397
            editorModifiedDialog.addContinueClickListener(discardListener);
398
            editorModifiedDialog.addCancelClickListener(cancelListener);
399

    
400
            UI.getCurrent().addWindow(editorModifiedDialog);
401
        } else {
402
            cancel();
403
        }
404
    }
405

    
406

    
407
    /**
408
     * Cancel editing and discard all modifications.
409
     */
410
    @Override
411
    public void cancel() {
412
        fieldGroup.discard();
413
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.CANCEL));
414
    }
415

    
416
    /**
417
     * @return
418
     */
419
    private void delete() {
420
        viewEventBus.publish(this, new EditorDeleteEvent<DTO>(this, fieldGroup.getItemDataSource().getBean()));
421
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.DELETE));
422
    }
423

    
424
    /**
425
     * Save the changes made in the editor.
426
     */
427
    private void save() {
428
        try {
429
            fieldGroup.commit();
430
        } catch (CommitException e) {
431
            fieldGroup.getFields().forEach(f -> ((AbstractField<?>)f).setValidationVisible(true));
432
            Throwable cause = e.getCause();
433
            while(cause != null) {
434
                if(cause instanceof FieldGroupInvalidValueException){
435
                    FieldGroupInvalidValueException invalidValueException = (FieldGroupInvalidValueException)cause;
436
                    updateFieldNotifications(invalidValueException.getInvalidFields());
437
                    int invalidFieldsCount = invalidValueException.getInvalidFields().size();
438
                    Notification.show("The entered data in " + invalidFieldsCount + " field" + (invalidFieldsCount > 1 ? "s": "") + " is incomplete or invalid.");
439
                    break;
440
                } else if(cause instanceof PermissionDeniedException){
441
                    PermissionDeniedException permissionDeniedException = (PermissionDeniedException)cause;
442
                    Notification.show("Permission denied", permissionDeniedException.getMessage(), Type.ERROR_MESSAGE);
443
                    break;
444
                }
445
                cause = cause.getCause();
446
            }
447
            if(cause == null){
448
                // no known exception type found
449
                logger.error(e);
450
                PopupEditorException pee = null;
451
                try {
452
                    pee  = new PopupEditorException("Error saving popup editor", this, e);
453
                } catch (Throwable t) {
454
                    /* IGORE errors which happen during the construction of the PopupEditorException */
455
                }
456
                if(pee != null){
457
                    throw pee;
458
                }
459
                throw new RuntimeException(e);
460
            }
461

    
462
        }
463
    }
464

    
465
    /**
466
     * @param invalidFields
467
     */
468
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
469
        for(Field<?> f : invalidFields.keySet()){
470
            if(f instanceof AbstractField){
471
                String message = invalidFields.get(f).getHtmlMessage();
472
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
473
            }
474
        }
475

    
476
    }
477

    
478
    // ------------------------ field adding methods ------------------------ //
479

    
480

    
481
    protected TextField addTextField(String caption, String propertyId) {
482
        return addField(new TextFieldNFix(caption), propertyId);
483
    }
484

    
485
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
486
            int column2, int row2)
487
            throws OverlapsException, OutOfBoundsException {
488
        return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
489
    }
490

    
491
    protected TextField addTextField(String caption, String propertyId, int column, int row)
492
            throws OverlapsException, OutOfBoundsException {
493
        return addField(new TextFieldNFix(caption), propertyId, column, row);
494
    }
495

    
496
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
497
            int column2, int row2)
498
            throws OverlapsException, OutOfBoundsException {
499

    
500
        SwitchableTextField field = new SwitchableTextField(caption);
501
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
502
        addComponent(field, column1, row1, column2, row2);
503
        return field;
504
    }
505

    
506
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
507
            throws OverlapsException, OutOfBoundsException {
508

    
509
        SwitchableTextField field = new SwitchableTextField(caption);
510
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
511
        addComponent(field, column, row);
512
        return field;
513
    }
514

    
515
    protected PopupDateField addDateField(String caption, String propertyId) {
516
        return addField(new PopupDateField(caption), propertyId);
517
    }
518

    
519
    protected CheckBox addCheckBox(String caption, String propertyId) {
520
        return addField(new CheckBox(caption), propertyId);
521
    }
522

    
523
    protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
524
        return addField(new CheckBox(caption), propertyId, column, row);
525
    }
526

    
527
    protected <T extends Field> T addField(T field, String propertyId) {
528
        fieldGroup.bind(field, propertyId);
529
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
530
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
531
        }
532
        addComponent(field);
533
        return field;
534
    }
535

    
536
    /**
537
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
538
     *
539
     * @param field
540
     *            the field to be added, not <code>null</code>.
541
     * @param propertyId
542
     * @param column
543
     *            the column index, starting from 0.
544
     * @param row
545
     *            the row index, starting from 0.
546
     * @throws OverlapsException
547
     *             if the new component overlaps with any of the components
548
     *             already in the grid.
549
     * @throws OutOfBoundsException
550
     *             if the cell is outside the grid area.
551
     */
552
    protected <T extends Field> T addField(T field, String propertyId, int column, int row)
553
            throws OverlapsException, OutOfBoundsException {
554
        fieldGroup.bind(field, propertyId);
555
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
556
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
557
        }
558
        addComponent(field, column, row);
559
        return field;
560
    }
561

    
562
    /**
563
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
564
     *
565
     * @param field
566
     * @param propertyId
567
     * @param column1
568
     * @param row1
569
     * @param column2
570
     * @param row2
571
     * @return
572
     * @throws OverlapsException
573
     * @throws OutOfBoundsException
574
     */
575
    protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
576
            int column2, int row2)
577
            throws OverlapsException, OutOfBoundsException {
578
        if(propertyId != null){
579
            fieldGroup.bind(field, propertyId);
580
            if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
581
                ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
582
            }
583
        }
584
        addComponent(field, column1, row1, column2, row2);
585
        return field;
586
    }
587

    
588
    protected Field<?> getField(Object propertyId){
589
        return fieldGroup.getField(propertyId);
590
    }
591

    
592
    public PropertyIdPath boundPropertyIdPath(Field<?> field){
593

    
594
        PropertyIdPath propertyIdPath = null;
595
        Object propertyId = fieldGroup.getPropertyId(field);
596

    
597
        if(propertyId == null){
598
            // not found in the editor field group. Maybe the field is bound to a nested fieldgroup?
599
            // 1. find the NestedFieldGroup implementations from the field up to the editor
600
            logger.setLevel(Level.DEBUG);
601
            PropertyIdPath nestedPropertyIds = new PropertyIdPath();
602
            Field parentField = field;
603
            HasComponents parentComponent = parentField.getParent();
604
            logger.debug("field: " + parentField.getClass().getSimpleName());
605
            while(parentComponent != null){
606
                logger.debug("parentComponent: " + parentComponent.getClass().getSimpleName());
607
                if(NestedFieldGroup.class.isAssignableFrom(parentComponent.getClass()) && AbstractField.class.isAssignableFrom(parentComponent.getClass())){
608
                    try {
609
                    Object propId = ((NestedFieldGroup)parentComponent).getFieldGroup().getPropertyId(parentField);
610
                    if(propId != null){
611
                        logger.debug("propId: " + propId.toString());
612
                        nestedPropertyIds.addParent(propId);
613
                    }
614
                    logger.debug("parentField: " + parentField.getClass().getSimpleName());
615
                    parentField = (Field)parentComponent;
616
                    } catch (NullPointerException e){
617
                        String causeDetail = "NullPointerException";
618
                        if(((NestedFieldGroup)parentComponent).getFieldGroup() == null){
619
                            causeDetail = "parentComponent.getFieldGroup() is NULL, this should not happen.";
620
                        }
621
                        logger.error(causeDetail, e);
622
                    }
623
                } else if(parentComponent == this) {
624
                    // we reached the editor itself
625
                    Object propId = fieldGroup.getPropertyId(parentField);
626
                    if(propId != null){
627
                        logger.debug("propId: " + propId.toString());
628
                        nestedPropertyIds.addParent(propId);
629
                    }
630
                    propertyIdPath = nestedPropertyIds;
631
                    break;
632
                }
633
                parentComponent = parentComponent.getParent();
634
            }
635
            // 2. check the NestedFieldGroup binding the field is direct or indirect child component of the editor
636
//            NO lONGER NEEDED
637
//            parentComponent = parentField.getParent(); // get component containing the last parent field found
638
//            while(true){
639
//                if(parentComponent == getFieldLayout()){
640
//                    propertyIdPath = nestedPropertyIds;
641
//                    break;
642
//                }
643
//                parentComponent = parentComponent.getParent();
644
//            }
645
        } else {
646
            propertyIdPath = new PropertyIdPath(propertyId);
647
        }
648
        return propertyIdPath;
649
    }
650

    
651
    protected void addComponent(Component component) {
652
        fieldLayout.addComponent(component);
653
        applyDefaultComponentStyles(component);
654
    }
655

    
656
    protected void bindField(Field field, String propertyId){
657
        fieldGroup.bind(field, propertyId);
658
    }
659

    
660
    protected void unbindField(Field field){
661
        fieldGroup.unbind(field);
662
    }
663

    
664
    /**
665
     * @param component
666
     */
667
    public void applyDefaultComponentStyles(Component component) {
668
        component.addStyleName(getDefaultComponentStyles());
669
    }
670

    
671
    protected abstract String getDefaultComponentStyles();
672

    
673
    /**
674
     * Can only be used if the <code>fieldlayout</code> is a GridLayout.
675
     * <p>
676
     * Adds the component to the grid in cells column1,row1 (NortWest corner of
677
     * the area.) End coordinates (SouthEast corner of the area) are the same as
678
     * column1,row1. The coordinates are zero-based. Component width and height
679
     * is 1.
680
     *
681
     * @param component
682
     *            the component to be added, not <code>null</code>.
683
     * @param column
684
     *            the column index, starting from 0.
685
     * @param row
686
     *            the row index, starting from 0.
687
     * @throws OverlapsException
688
     *             if the new component overlaps with any of the components
689
     *             already in the grid.
690
     * @throws OutOfBoundsException
691
     *             if the cell is outside the grid area.
692
     */
693
    public void addComponent(Component component, int column, int row)
694
            throws OverlapsException, OutOfBoundsException {
695
        applyDefaultComponentStyles(component);
696
        gridLayout().addComponent(component, column, row, column, row);
697
    }
698

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

    
740
    public void setSaveButtonEnabled(boolean enabled){
741
        save.setEnabled(enabled);
742
    }
743

    
744
    public void withDeleteButton(boolean withDelete){
745

    
746
        if(withDelete){
747
            buttonLayout.setExpandRatio(save, 0);
748
            buttonLayout.setExpandRatio(delete, 1);
749
        } else {
750
            buttonLayout.setExpandRatio(save, 1);
751
            buttonLayout.setExpandRatio(delete, 0);
752
        }
753
        delete.setVisible(withDelete);
754
    }
755

    
756
    public boolean addStatusMessage(String message){
757
        boolean returnVal = statusMessages.add(message);
758
        updateStatusLabel();
759
        return returnVal;
760
    }
761

    
762
    public boolean removeStatusMessage(String message){
763
        boolean returnVal = statusMessages.remove(message);
764
        updateStatusLabel();
765
        return returnVal;
766
    }
767

    
768
    /**
769
     *
770
     */
771
    private void updateStatusLabel() {
772
        String text = "";
773
        for(String s : statusMessages){
774
            text += s + " ";
775
        }
776
        statusMessageLabel.setValue(text);
777
        statusMessageLabel.setVisible(!text.isEmpty());
778
        statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
779
    }
780

    
781
    private void updateContextBreadcrumbs() {
782

    
783
        List<EditorActionContext> contextInfo = new ArrayList<>(getEditorActionContext());
784
        String breadcrumbs = "";
785
        EditorActionContextFormatter formatter = new EditorActionContextFormatter();
786

    
787
        int cnt = 0;
788
        for(EditorActionContext cntxt : contextInfo){
789
            cnt++;
790
            boolean isLast = cnt == contextInfo.size();
791
            boolean isFirst = cnt == 1;
792

    
793
            boolean doClass = false; // will be removed in future
794
            boolean classNameForMissingPropertyPath = true; // !doClass;
795
            boolean doProperties = true;
796
            boolean doCreateOrNew = !isFirst;
797
            String contextmarkup = formatter.format(
798
                    cntxt,
799
                    new EditorActionContextFormat(doClass, doProperties, classNameForMissingPropertyPath, doCreateOrNew,
800
                            EditorActionContextFormat.TargetInfoType.FIELD_CAPTION, (isLast ? "active" : ""))
801
                    );
802
//            if(!isLast){
803
//                contextmarkup += " " + FontAwesome.ANGLE_RIGHT.getHtml() + " ";
804
//            }
805
            if(isLast){
806
                contextmarkup = "<li><span class=\"crumb active\">" + contextmarkup + "</span></li>";
807
            } else {
808
                contextmarkup = "<li><span class=\"crumb\">" + contextmarkup + "</span></li>";
809
            }
810
            breadcrumbs += contextmarkup;
811
        }
812
        contextBreadcrumbsLabel.setValue("<ul class=\"breadcrumbs\">" + breadcrumbs + "</ul>");
813
    }
814

    
815
    // ------------------------ data binding ------------------------ //
816

    
817
    protected void bindDesign(Component component) {
818
        fieldLayout.removeAllComponents();
819
        fieldGroup.bindMemberFields(component);
820
        fieldLayout.addComponent(component);
821
    }
822

    
823

    
824
    public final void loadInEditor(Object identifier) {
825

    
826
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
827
        fieldGroup.setItemDataSource(beanToEdit);
828
        afterItemDataSourceSet();
829
        getPresenter().onViewFormReady(beanToEdit);
830
        updateContextBreadcrumbs();
831
        isBeanLoaded = true;
832
    }
833

    
834
    /**
835
     * Passes the beanInstantiator to the presenter method {@link AbstractEditorPresenter#setBeanInstantiator(BeanInstantiator)}
836
     *
837
     * @param beanInstantiator
838
     */
839
    public final void setBeanInstantiator(BeanInstantiator<DTO> beanInstantiator) {
840
        if(AbstractCdmEditorPresenter.class.isAssignableFrom(getPresenter().getClass())){
841
            ((CdmEditorPresenterBase)getPresenter()).setBeanInstantiator(beanInstantiator);
842
        } else {
843
            throw new RuntimeException("BeanInstantiator can only be set for popup editors with a peresenter of the type CdmEditorPresenterBase");
844
        }
845
    }
846

    
847
    /**
848
     * Returns the bean contained in the itemDatasource of the fieldGroup.
849
     *
850
     * @return
851
     */
852
    public DTO getBean() {
853
        if(fieldGroup.getItemDataSource() != null){
854
            return fieldGroup.getItemDataSource().getBean();
855

    
856
        }
857
        return null;
858
    }
859

    
860
    /**
861
     * @return true once the bean has been loaded indicating that all fields have
862
     *   been setup configured so that the editor is ready for use.
863
     */
864
    public boolean isBeanLoaded() {
865
        return isBeanLoaded;
866
    }
867

    
868
    /**
869
     * This method should only be used by the presenter of this view
870
     *
871
     * @param bean
872
     */
873
    protected void updateItemDataSource(DTO bean) {
874
        fieldGroup.getItemDataSource().setBean(bean);
875
    }
876

    
877
    /**
878
     * This method is called after setting the item data source whereby the
879
     * {@link FieldGroup#configureField(Field<?> field)} method will be called.
880
     * In this method all fields are set to default states defined for the fieldGroup.
881
     * <p>
882
     * You can now implement this method if you need to modify the state or value of individual fields.
883
     */
884
    protected void afterItemDataSourceSet() {
885
        if(editorComponentsConfigurator != null){
886
            editorComponentsConfigurator.updateComponentStates(this);
887
        }
888
    }
889

    
890
    // ------------------------ issue related temporary solutions --------------------- //
891
    /**
892
     * Publicly accessible equivalent to getPreseneter(), needed for
893
     * managing the presenter listeners.
894
     * <p>
895
     * TODO: refactor the presenter listeners management to get rid of this method
896
     *
897
     * @return
898
     * @deprecated marked deprecated to emphasize on the special character of this method
899
     *    which should only be used internally see #6673
900
     */
901
    @Deprecated
902
    public P presenter() {
903
        return getPresenter();
904
    }
905

    
906
    /**
907
     * Returns the context of editor actions for this editor.
908
     * The context submitted with {@link #setParentContext(Stack)} will be updated
909
     * to represent the current context.
910
     *
911
     * @return the context
912
     */
913
    public Stack<EditorActionContext> getEditorActionContext() {
914
        if(!isContextUpdated){
915
            if(getBean() == null){
916
                throw new RuntimeException("getContext() is only possible after the bean is loaded");
917
            }
918
            context.push(new EditorActionContext(getBean(), this));
919
            isContextUpdated = true;
920
        }
921
        return context;
922
    }
923

    
924
    /**
925
     * Set the context of editor actions parent to this editor
926
     *
927
     * @param context the context to set
928
     */
929
    public void setParentEditorActionContext(Stack<EditorActionContext> context, Field<?> targetField) {
930
        if(context != null){
931
            this.context.addAll(context);
932
        }
933
        if(targetField != null){
934
            this.context.get(context.size() - 1).setTargetField(targetField);
935
        }
936
    }
937

    
938
    protected AbstractField<String> replaceComponent(String propertyId, AbstractField<String> oldField, AbstractField<String> newField, int column1, int row1, int column2,
939
            int row2) {
940
                String value = oldField.getValue();
941
                newField.setCaption(oldField.getCaption());
942
                GridLayout grid = (GridLayout)getFieldLayout();
943
                grid.removeComponent(oldField);
944

    
945
                unbindField(oldField);
946
                addField(newField, propertyId, column1, row1, column2, row2);
947
                getViewEventBus().publish(this, new FieldReplaceEvent(this, oldField, newField));
948
                // important: set newField value at last!
949
                newField.setValue(value);
950
                return newField;
951
            }
952

    
953
    public EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> getEditorComponentsConfigurator() {
954
        return editorComponentsConfigurator;
955
    }
956

    
957
    public void setEditorComponentsConfigurator(
958
            EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator) {
959
        this.editorComponentsConfigurator = editorComponentsConfigurator;
960
    }
961

    
962
}
(6-6/15)