Project

General

Profile

Download (35.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.GenericFontIcon;
35
import com.vaadin.server.UserError;
36
import com.vaadin.shared.ui.MarginInfo;
37
import com.vaadin.ui.AbstractComponentContainer;
38
import com.vaadin.ui.AbstractField;
39
import com.vaadin.ui.AbstractLayout;
40
import com.vaadin.ui.AbstractOrderedLayout;
41
import com.vaadin.ui.Alignment;
42
import com.vaadin.ui.Button;
43
import com.vaadin.ui.Button.ClickListener;
44
import com.vaadin.ui.CheckBox;
45
import com.vaadin.ui.Component;
46
import com.vaadin.ui.CssLayout;
47
import com.vaadin.ui.Field;
48
import com.vaadin.ui.GridLayout;
49
import com.vaadin.ui.GridLayout.OutOfBoundsException;
50
import com.vaadin.ui.GridLayout.OverlapsException;
51
import com.vaadin.ui.HasComponents;
52
import com.vaadin.ui.HorizontalLayout;
53
import com.vaadin.ui.Label;
54
import com.vaadin.ui.Layout;
55
import com.vaadin.ui.Layout.MarginHandler;
56
import com.vaadin.ui.Notification;
57
import com.vaadin.ui.Notification.Type;
58
import com.vaadin.ui.PopupDateField;
59
import com.vaadin.ui.TextField;
60
import com.vaadin.ui.UI;
61
import com.vaadin.ui.VerticalLayout;
62
import com.vaadin.ui.themes.ValoTheme;
63

    
64
import eu.etaxonomy.cdm.database.PermissionDeniedException;
65
import eu.etaxonomy.cdm.vaadin.component.TextFieldNFix;
66
import eu.etaxonomy.cdm.vaadin.component.dialog.ContinueAlternativeCancelDialog;
67
import eu.etaxonomy.cdm.vaadin.event.EditorActionContext;
68
import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormat;
69
import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormatter;
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
    public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
139

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

    
146
        setCompositionRoot(mainLayout);
147

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

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

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

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

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

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

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

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

    
195
        statusMessageLabel.setWidthUndefined();
196

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

    
201
        updateToolBarVisibility();
202
    }
203

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

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

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

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

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

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

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

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

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

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

    
302
    }
303

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

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

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

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

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

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

    
349

    
350
    // ------------------------ event handler ------------------------ //
351

    
352
    private class SaveHandler implements CommitHandler {
353

    
354
        private static final long serialVersionUID = 2047223089707080659L;
355

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

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

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

    
387
    protected void cancelEditorDialog(){
388

    
389
        if(fieldGroup.isModified()){
390

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

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

    
409

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

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

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

    
456
        }
457
    }
458

    
459
    /**
460
     * @param invalidFields
461
     */
462
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
463
        for(Field<?> f : invalidFields.keySet()){
464
            if(f instanceof AbstractField){
465
                String message = invalidFields.get(f).getHtmlMessage();
466
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
467
            }
468
        }
469

    
470
    }
471

    
472
    // ------------------------ field adding methods ------------------------ //
473

    
474

    
475
    protected TextField addTextField(String caption, String propertyId) {
476
        return addField(new TextFieldNFix(caption), propertyId);
477
    }
478

    
479
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
480
            int column2, int row2)
481
            throws OverlapsException, OutOfBoundsException {
482
        return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
483
    }
484

    
485
    protected TextField addTextField(String caption, String propertyId, int column, int row)
486
            throws OverlapsException, OutOfBoundsException {
487
        return addField(new TextFieldNFix(caption), propertyId, column, row);
488
    }
489

    
490
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
491
            int column2, int row2)
492
            throws OverlapsException, OutOfBoundsException {
493

    
494
        SwitchableTextField field = new SwitchableTextField(caption);
495
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
496
        addComponent(field, column1, row1, column2, row2);
497
        return field;
498
    }
499

    
500
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
501
            throws OverlapsException, OutOfBoundsException {
502

    
503
        SwitchableTextField field = new SwitchableTextField(caption);
504
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
505
        addComponent(field, column, row);
506
        return field;
507
    }
508

    
509
    protected PopupDateField addDateField(String caption, String propertyId) {
510
        return addField(new PopupDateField(caption), propertyId);
511
    }
512

    
513
    protected CheckBox addCheckBox(String caption, String propertyId) {
514
        return addField(new CheckBox(caption), propertyId);
515
    }
516

    
517
    protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
518
        return addField(new CheckBox(caption), propertyId, column, row);
519
    }
520

    
521
    protected <T extends Field> T addField(T field, String propertyId) {
522
        fieldGroup.bind(field, propertyId);
523
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
524
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
525
        }
526
        addComponent(field);
527
        return field;
528
    }
529

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

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

    
582
    protected Field<?> getField(Object propertyId){
583
        return fieldGroup.getField(propertyId);
584
    }
585

    
586
    public PropertyIdPath boundPropertyIdPath(Field<?> field){
587

    
588
        PropertyIdPath propertyIdPath = null;
589
        Object propertyId = fieldGroup.getPropertyId(field);
590

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

    
642
    protected void addComponent(Component component) {
643
        fieldLayout.addComponent(component);
644
        applyDefaultComponentStyles(component);
645
    }
646

    
647
    protected void bindField(Field field, String propertyId){
648
        fieldGroup.bind(field, propertyId);
649
    }
650

    
651
    protected void unbindField(Field field){
652
        fieldGroup.unbind(field);
653
    }
654

    
655
    /**
656
     * @param component
657
     */
658
    public void applyDefaultComponentStyles(Component component) {
659
        component.addStyleName(getDefaultComponentStyles());
660
    }
661

    
662
    protected abstract String getDefaultComponentStyles();
663

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

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

    
731
    public void setSaveButtonEnabled(boolean enabled){
732
        save.setEnabled(enabled);
733
    }
734

    
735
    public void withDeleteButton(boolean withDelete){
736

    
737
        if(withDelete){
738
            buttonLayout.setExpandRatio(save, 0);
739
            buttonLayout.setExpandRatio(delete, 1);
740
        } else {
741
            buttonLayout.setExpandRatio(save, 1);
742
            buttonLayout.setExpandRatio(delete, 0);
743
        }
744
        delete.setVisible(withDelete);
745
    }
746

    
747
    public boolean addStatusMessage(String message){
748
        boolean returnVal = statusMessages.add(message);
749
        updateStatusLabel();
750
        return returnVal;
751
    }
752

    
753
    public boolean removeStatusMessage(String message){
754
        boolean returnVal = statusMessages.remove(message);
755
        updateStatusLabel();
756
        return returnVal;
757
    }
758

    
759
    /**
760
     *
761
     */
762
    private void updateStatusLabel() {
763
        String text = "";
764
        for(String s : statusMessages){
765
            text += s + " ";
766
        }
767
        statusMessageLabel.setValue(text);
768
        statusMessageLabel.setVisible(!text.isEmpty());
769
        statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
770
    }
771

    
772
    private void updateContextBreadcrumbs() {
773

    
774
        List<EditorActionContext> contextInfo = new ArrayList<>(getEditorActionContext());
775
        String breadcrumbs = "";
776
        EditorActionContextFormatter formatter = new EditorActionContextFormatter();
777

    
778
        GenericFontIcon operationPrefixIcon = new GenericFontIcon("IcoMoon", 0xe902);
779
        GenericFontIcon operationSuffxIcon = new GenericFontIcon("IcoMoon", 0xe901);
780

    
781
        int cnt = 0;
782
        for(EditorActionContext cntxt : contextInfo){
783
            cnt++;
784
            boolean isLast = cnt == contextInfo.size();
785
            boolean isFirst = cnt == 1;
786

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

    
809
    // ------------------------ data binding ------------------------ //
810

    
811
    protected void bindDesign(Component component) {
812
        fieldLayout.removeAllComponents();
813
        fieldGroup.bindMemberFields(component);
814
        fieldLayout.addComponent(component);
815
    }
816

    
817

    
818
    public final void loadInEditor(Object identifier) {
819

    
820
        DTO beanToEdit = getPresenter().loadBeanById(identifier);
821
        fieldGroup.setItemDataSource(beanToEdit);
822
        afterItemDataSourceSet();
823
        getPresenter().onViewFormReady(beanToEdit);
824
        updateContextBreadcrumbs();
825
        isBeanLoaded = true;
826
    }
827

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

    
841
    /**
842
     * Returns the bean contained in the itemDatasource of the fieldGroup.
843
     *
844
     * @return
845
     */
846
    public DTO getBean() {
847
        if(fieldGroup.getItemDataSource() != null){
848
            return fieldGroup.getItemDataSource().getBean();
849

    
850
        }
851
        return null;
852
    }
853

    
854
    /**
855
     * @return true once the bean has been loaded indicating that all fields have
856
     *   been setup configured so that the editor is ready for use.
857
     */
858
    public boolean isBeanLoaded() {
859
        return isBeanLoaded;
860
    }
861

    
862
    /**
863
     * This method should only be used by the presenter of this view
864
     *
865
     * @param bean
866
     */
867
    protected void updateItemDataSource(DTO bean) {
868
        fieldGroup.getItemDataSource().setBean(bean);
869
    }
870

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

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

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

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

    
932
    protected AbstractField<String> replaceComponent(String propertyId, AbstractField<String> oldField, AbstractField<String> newField, int column1, int row1, int column2,
933
            int row2) {
934
                String value = oldField.getValue();
935
                newField.setCaption(oldField.getCaption());
936
                GridLayout grid = (GridLayout)getFieldLayout();
937
                grid.removeComponent(oldField);
938

    
939
                unbindField(oldField);
940
                addField(newField, propertyId, column1, row1, column2, row2);
941
                getViewEventBus().publish(this, new FieldReplaceEvent(this, oldField, newField));
942
                // important: set newField value at last!
943
                newField.setValue(value);
944
                return newField;
945
            }
946

    
947
    public EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> getEditorComponentsConfigurator() {
948
        return editorComponentsConfigurator;
949
    }
950

    
951
    public void setEditorComponentsConfigurator(
952
            EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator) {
953
        this.editorComponentsConfigurator = editorComponentsConfigurator;
954
    }
955

    
956
}
(6-6/15)