Project

General

Profile

Download (35.4 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.vaadin.component.NestedFieldGroup;
70
import eu.etaxonomy.vaadin.component.SwitchableTextField;
71
import eu.etaxonomy.vaadin.event.FieldReplaceEvent;
72
import eu.etaxonomy.vaadin.mvp.event.EditorDeleteEvent;
73
import eu.etaxonomy.vaadin.mvp.event.EditorPreSaveEvent;
74
import eu.etaxonomy.vaadin.mvp.event.EditorSaveEvent;
75
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent;
76
import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent.Reason;
77
import eu.etaxonomy.vaadin.util.PropertyIdPath;
78

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

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

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

    
97
    private BeanFieldGroup<DTO> fieldGroup;
98

    
99
    private VerticalLayout mainLayout;
100

    
101
    private Layout fieldLayout;
102

    
103
    private HorizontalLayout buttonLayout;
104

    
105
    private Button save;
106

    
107
    private Button cancel;
108

    
109
    private Button delete;
110

    
111
    private CssLayout toolBar = new CssLayout();
112

    
113
    private CssLayout toolBarButtonGroup = new CssLayout();
114

    
115
    private Label contextBreadcrumbsLabel = new Label();
116

    
117
    private Label statusMessageLabel = new Label();
118

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

    
121
    private GridLayout _gridLayoutCache;
122

    
123
    private boolean isBeanLoaded;
124

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

    
127
    private boolean isContextUpdated;
128

    
129
    private boolean isAdvancedMode = false;
130

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

    
133
    private Button advancedModeButton;
134

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

    
137
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
138

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

    
145
        setCompositionRoot(mainLayout);
146

    
147
        fieldGroup = new BeanFieldGroup<>(dtoType);
148
        fieldGroup.addCommitHandler(new SaveHandler());
149

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

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

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

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

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

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

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

    
194
        statusMessageLabel.setWidthUndefined();
195

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

    
200
        updateToolBarVisibility();
201
    }
202

    
203
    protected VerticalLayout getMainLayout() {
204
        return mainLayout;
205
    }
206

    
207
    protected Layout getFieldLayout() {
208
        return fieldLayout;
209
    }
210

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

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

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

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

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

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

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

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

    
301
    }
302

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

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

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

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

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

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

    
348

    
349
    // ------------------------ event handler ------------------------ //
350

    
351
    private class SaveHandler implements CommitHandler {
352

    
353
        private static final long serialVersionUID = 2047223089707080659L;
354

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

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

    
382
    protected void addCommitHandler(CommitHandler commitHandler) {
383
        fieldGroup.addCommitHandler(commitHandler);
384
    }
385

    
386
    protected void cancelEditorDialog(){
387

    
388
        if(fieldGroup.isModified()){
389

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

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

    
408

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

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

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

    
464
        }
465
    }
466

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

    
478
    }
479

    
480
    // ------------------------ field adding methods ------------------------ //
481

    
482

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
594
    public PropertyIdPath boundPropertyIdPath(Field<?> field){
595

    
596
        PropertyIdPath propertyIdPath = null;
597
        Object propertyId = fieldGroup.getPropertyId(field);
598

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

    
654
    protected void addComponent(Component component) {
655
        fieldLayout.addComponent(component);
656
        applyDefaultComponentStyles(component);
657
    }
658

    
659
    protected void bindField(Field field, String propertyId){
660
        fieldGroup.bind(field, propertyId);
661
    }
662

    
663
    protected void unbindField(Field field){
664
        fieldGroup.unbind(field);
665
    }
666

    
667
    /**
668
     * @param component
669
     */
670
    public void applyDefaultComponentStyles(Component component) {
671
        component.addStyleName(getDefaultComponentStyles());
672
    }
673

    
674
    protected abstract String getDefaultComponentStyles();
675

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

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

    
743
    public void setSaveButtonEnabled(boolean enabled){
744
        save.setEnabled(enabled);
745
    }
746

    
747
    public void withDeleteButton(boolean withDelete){
748

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

    
759
    public boolean addStatusMessage(String message){
760
        boolean returnVal = statusMessages.add(message);
761
        updateStatusLabel();
762
        return returnVal;
763
    }
764

    
765
    public boolean removeStatusMessage(String message){
766
        boolean returnVal = statusMessages.remove(message);
767
        updateStatusLabel();
768
        return returnVal;
769
    }
770

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

    
784
    private void updateContextBreadcrumbs() {
785

    
786
        List<EditorActionContext> contextInfo = new ArrayList<>(getEditorActionContext());
787
        String breadcrumbs = "";
788
        EditorActionContextFormatter formatter = new EditorActionContextFormatter();
789

    
790
        int cnt = 0;
791
        for(EditorActionContext cntxt : contextInfo){
792
            cnt++;
793
            boolean isLast = cnt == contextInfo.size();
794
            boolean isFirst = cnt == 1;
795

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

    
818
    // ------------------------ data binding ------------------------ //
819

    
820
    protected void bindDesign(Component component) {
821
        fieldLayout.removeAllComponents();
822
        fieldGroup.bindMemberFields(component);
823
        fieldLayout.addComponent(component);
824
    }
825

    
826

    
827
    public final void loadInEditor(Object identifier) {
828

    
829
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
830
        fieldGroup.setItemDataSource(beanToEdit);
831
        afterItemDataSourceSet();
832
        getPresenter().onViewFormReady(beanToEdit);
833
        updateContextBreadcrumbs();
834
        isBeanLoaded = true;
835
    }
836

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

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

    
859
        }
860
        return null;
861
    }
862

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

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

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

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

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

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

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

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

    
956
    public EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> getEditorComponentsConfigurator() {
957
        return editorComponentsConfigurator;
958
    }
959

    
960
    public void setEditorComponentsConfigurator(
961
            EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator) {
962
        this.editorComponentsConfigurator = editorComponentsConfigurator;
963
    }
964

    
965
}
(6-6/15)