Project

General

Profile

Download (35.5 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.setSizeFull();
195
        statusMessageLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
196

    
197
        HorizontalLayout statusMessageLayout = new HorizontalLayout();
198
        statusMessageLayout.setSizeFull();
199
        statusMessageLayout.addComponent(statusMessageLabel);
200
        statusMessageLayout.setMargin(new MarginInfo(false, true, false, true));
201

    
202
        mainLayout.addComponents(toolBar, fieldLayout, statusMessageLayout, buttonLayout);
203
        mainLayout.setComponentAlignment(statusMessageLayout, Alignment.BOTTOM_RIGHT);
204
        mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
205

    
206
        updateToolBarVisibility();
207
    }
208

    
209
    protected VerticalLayout getMainLayout() {
210
        return mainLayout;
211
    }
212

    
213
    protected Layout getFieldLayout() {
214
        return fieldLayout;
215
    }
216

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

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

    
247
    /**
248
     * @param readOnly
249
     * @param layout
250
     */
251
    protected void recursiveReadonly(boolean readOnly, AbstractComponentContainer layout) {
252
        for(Component c : layout){
253
            c.setReadOnly(readOnly);
254
            if(c instanceof AbstractComponentContainer){
255
                recursiveReadonly(readOnly, (AbstractComponentContainer)c);
256
            }
257
        }
258
    }
259

    
260
    /**
261
     * @return
262
     * @return
263
     */
264
    protected AbstractLayout getToolBar() {
265
        return toolBar;
266
    }
267

    
268
    /**
269
     * @return
270
     * @return
271
     */
272
    protected void toolBarAdd(Component c) {
273
        toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
274
        updateToolBarVisibility();
275
    }
276

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

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

    
295
    /**
296
     *
297
     */
298
    private void updateToolBarVisibility() {
299
        boolean showToolbar = toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1;
300
        toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
301
        if(!showToolbar){
302
            mainLayout.setMargin(new MarginInfo(true, false, false, false));
303
        } else {
304
            mainLayout.setMargin(false);
305
        }
306

    
307
    }
308

    
309
    /**
310
     * The top tool-bar is initially invisible.
311
     *
312
     * @param visible
313
     */
314
    protected void setToolBarVisible(boolean visible){
315
        toolBar.setVisible(true);
316
    }
317

    
318
    /**
319
     * @return the isAdvancedMode
320
     */
321
    public boolean isAdvancedMode() {
322
        return isAdvancedMode;
323
    }
324

    
325
    /**
326
     * @param isAdvancedMode the isAdvancedMode to set
327
     */
328
    public void setAdvancedMode(boolean isAdvancedMode) {
329
        this.isAdvancedMode = isAdvancedMode;
330
        advancedModeComponents.forEach(c -> c.setVisible(isAdvancedMode));
331
    }
332

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

    
344
        } else if(advancedModeButton != null) {
345
            toolBarButtonGroupRemove(advancedModeButton);
346
            advancedModeButton = null;
347
        }
348
    }
349

    
350
    public void registerAdvancedModeComponents(Component ... c){
351
        advancedModeComponents.addAll(Arrays.asList(c));
352
    }
353

    
354

    
355
    // ------------------------ event handler ------------------------ //
356

    
357
    private class SaveHandler implements CommitHandler {
358

    
359
        private static final long serialVersionUID = 2047223089707080659L;
360

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

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

    
388
    protected void addCommitHandler(CommitHandler commitHandler) {
389
        fieldGroup.addCommitHandler(commitHandler);
390
    }
391

    
392
    protected void cancelEditorDialog(){
393

    
394
        if(fieldGroup.isModified()){
395

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

    
408
            UI.getCurrent().addWindow(editorModifiedDialog);
409
        } else {
410
            cancel();
411
        }
412
    }
413

    
414

    
415
    /**
416
     * Cancel editing and discard all modifications.
417
     */
418
    @Override
419
    public void cancel() {
420
        fieldGroup.discard();
421
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.CANCEL));
422
    }
423

    
424
    /**
425
     * @return
426
     */
427
    private void delete() {
428
        viewEventBus.publish(this, new EditorDeleteEvent<DTO>(this, fieldGroup.getItemDataSource().getBean()));
429
        viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.DELETE));
430
    }
431

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

    
470
        }
471
    }
472

    
473
    /**
474
     * @param invalidFields
475
     */
476
    private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
477
        for(Field<?> f : invalidFields.keySet()){
478
            if(f instanceof AbstractField){
479
                String message = invalidFields.get(f).getHtmlMessage();
480
                ((AbstractField)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
481
            }
482
        }
483

    
484
    }
485

    
486
    // ------------------------ field adding methods ------------------------ //
487

    
488

    
489
    protected TextField addTextField(String caption, String propertyId) {
490
        return addField(new TextFieldNFix(caption), propertyId);
491
    }
492

    
493
    protected TextField addTextField(String caption, String propertyId, int column1, int row1,
494
            int column2, int row2)
495
            throws OverlapsException, OutOfBoundsException {
496
        return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
497
    }
498

    
499
    protected TextField addTextField(String caption, String propertyId, int column, int row)
500
            throws OverlapsException, OutOfBoundsException {
501
        return addField(new TextFieldNFix(caption), propertyId, column, row);
502
    }
503

    
504
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
505
            int column2, int row2)
506
            throws OverlapsException, OutOfBoundsException {
507

    
508
        SwitchableTextField field = new SwitchableTextField(caption);
509
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
510
        addComponent(field, column1, row1, column2, row2);
511
        return field;
512
    }
513

    
514
    protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
515
            throws OverlapsException, OutOfBoundsException {
516

    
517
        SwitchableTextField field = new SwitchableTextField(caption);
518
        field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
519
        addComponent(field, column, row);
520
        return field;
521
    }
522

    
523
    protected PopupDateField addDateField(String caption, String propertyId) {
524
        return addField(new PopupDateField(caption), propertyId);
525
    }
526

    
527
    protected CheckBox addCheckBox(String caption, String propertyId) {
528
        return addField(new CheckBox(caption), propertyId);
529
    }
530

    
531
    protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
532
        return addField(new CheckBox(caption), propertyId, column, row);
533
    }
534

    
535
    protected <T extends Field> T addField(T field, String propertyId) {
536
        fieldGroup.bind(field, propertyId);
537
        if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
538
            ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
539
        }
540
        addComponent(field);
541
        return field;
542
    }
543

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

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

    
596
    protected Field<?> getField(Object propertyId){
597
        return fieldGroup.getField(propertyId);
598
    }
599

    
600
    public PropertyIdPath boundPropertyIdPath(Field<?> field){
601

    
602
        PropertyIdPath propertyIdPath = null;
603
        Object propertyId = fieldGroup.getPropertyId(field);
604

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

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

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

    
665
    protected void unbindField(Field field){
666
        fieldGroup.unbind(field);
667
    }
668

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

    
676
    protected abstract String getDefaultComponentStyles();
677

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

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

    
745
    public void setSaveButtonEnabled(boolean enabled){
746
        save.setEnabled(enabled);
747
    }
748

    
749
    public void withDeleteButton(boolean withDelete){
750

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

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

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

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

    
786
    private void updateContextBreadcrumbs() {
787

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

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

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

    
820
    // ------------------------ data binding ------------------------ //
821

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

    
828

    
829
    public final void loadInEditor(Object identifier) {
830

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

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

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

    
861
        }
862
        return null;
863
    }
864

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

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

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

    
895

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

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

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

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

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

    
959
    public EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> getEditorComponentsConfigurator() {
960
        return editorComponentsConfigurator;
961
    }
962

    
963
    public void setEditorComponentsConfigurator(
964
            EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P>> editorComponentsConfigurator) {
965
        this.editorComponentsConfigurator = editorComponentsConfigurator;
966
    }
967

    
968
}
(6-6/15)