fix exception caused by PermissionDeniedException moved (in vaadin)
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / vaadin / mvp / AbstractPopupEditor.java
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.logging.log4j.LogManager;
21 import org.apache.logging.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.persistence.permission.PermissionDeniedException;
64 import eu.etaxonomy.cdm.vaadin.component.TextFieldNFix;
65 import eu.etaxonomy.cdm.vaadin.component.dialog.ContinueAlternativeCancelDialog;
66 import eu.etaxonomy.cdm.vaadin.event.EditorActionContext;
67 import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormat;
68 import eu.etaxonomy.cdm.vaadin.event.EditorActionContextFormatter;
69 import eu.etaxonomy.cdm.vaadin.ui.PopupEditorDefaultStatusMessageSource;
70 import eu.etaxonomy.vaadin.component.NestedFieldGroup;
71 import eu.etaxonomy.vaadin.component.SwitchableTextField;
72 import eu.etaxonomy.vaadin.event.FieldReplaceEvent;
73 import eu.etaxonomy.vaadin.mvp.event.EditorDeleteEvent;
74 import eu.etaxonomy.vaadin.mvp.event.EditorPreSaveEvent;
75 import eu.etaxonomy.vaadin.mvp.event.EditorSaveEvent;
76 import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent;
77 import eu.etaxonomy.vaadin.ui.view.DoneWithPopupEvent.Reason;
78 import eu.etaxonomy.vaadin.util.PropertyIdPath;
79
80 /**
81 * Optional with a deleteBtn button which can be enabled with {@link #withDeleteButton(boolean)}
82 *
83 * @author a.kohlbecker
84 * @since Apr 5, 2017
85 */
86 public abstract class AbstractPopupEditor<DTO extends Object, P extends AbstractEditorPresenter<DTO,P,V>, V extends ApplicationView<V,P>>
87 extends AbstractPopupView<V,P> {
88
89 private static final long serialVersionUID = 5944874629527570061L;
90 private static final Logger logger = LogManager.getLogger();
91
92 private static final String READ_ONLY_MESSAGE_TEXT = "The editor is in read-only mode. You do not have authority to edit this data.";
93
94 private BeanFieldGroup<DTO> fieldGroup;
95
96 private VerticalLayout mainLayout;
97
98 private Layout fieldLayout;
99
100 private HorizontalLayout buttonLayout;
101
102 private Button saveBtn;
103
104 private Button cancelBtn;
105
106 private Button deleteBtn;
107
108 private CssLayout toolBar = new CssLayout();
109
110 private CssLayout toolBarButtonGroup = new CssLayout();
111
112 private Label contextBreadcrumbsLabel = new Label();
113
114 private Label statusMessageLabel = new Label();
115
116 private Set<String> statusMessages = new HashSet<>();
117
118 private GridLayout gridLayoutCache;
119
120 private boolean isBeanLoaded;
121
122 private Stack<EditorActionContext> context = new Stack<>();
123
124 private boolean isContextUpdated;
125
126 private boolean isAdvancedMode = false;
127
128 protected List<Component> advancedModeComponents = new ArrayList<>();
129
130 private Button advancedModeButton;
131
132 private EditorFormConfigurator<? extends AbstractPopupEditor<DTO, P,V>> editorComponentsConfigurator;
133
134 private boolean withDeleteButton;
135
136 public AbstractPopupEditor(Layout layout, Class<DTO> dtoType) {
137
138 mainLayout = new VerticalLayout();
139 // IMPORTANT: mainLayout must be set to full size otherwise the
140 // popup window may have problems with automatic resizing of its
141 // content.
142 mainLayout.setSizeFull();
143
144 setCompositionRoot(mainLayout);
145
146 fieldGroup = new BeanFieldGroup<>(dtoType);
147 fieldGroup.addCommitHandler(new SaveHandler());
148
149 toolBar.addStyleName(ValoTheme.WINDOW_TOP_TOOLBAR);
150 toolBar.setWidth(100, Unit.PERCENTAGE);
151 contextBreadcrumbsLabel.setId("context-breadcrumbs");
152 contextBreadcrumbsLabel.setWidthUndefined();
153 contextBreadcrumbsLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
154 toolBar.addComponent(contextBreadcrumbsLabel);
155 toolBarButtonGroup.addStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
156 toolBarButtonGroup.setWidthUndefined();
157 toolBar.addComponent(toolBarButtonGroup);
158 toolBar.setVisible(false);
159
160 fieldLayout = layout;
161 fieldLayout.setWidthUndefined();
162 if(fieldLayout instanceof AbstractOrderedLayout){
163 ((AbstractOrderedLayout)fieldLayout).setSpacing(true);
164 }
165 if(MarginHandler.class.isAssignableFrom(fieldLayout.getClass())){
166 ((MarginHandler)fieldLayout).setMargin(new MarginInfo(false, true, true, true));
167 }
168
169 buttonLayout = new HorizontalLayout();
170 buttonLayout.setStyleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR);
171 buttonLayout.setWidth(100, Unit.PERCENTAGE);
172 buttonLayout.setSpacing(true);
173
174 saveBtn = new Button("Save", FontAwesome.SAVE);
175 saveBtn.setStyleName(ValoTheme.BUTTON_PRIMARY);
176 saveBtn.addClickListener(e -> save());
177
178 cancelBtn = new Button("Cancel", FontAwesome.REMOVE);
179 cancelBtn.addClickListener(e -> cancelEditorDialog());
180
181 deleteBtn = new Button("Delete", FontAwesome.TRASH);
182 deleteBtn.setStyleName(ValoTheme.BUTTON_DANGER);
183 deleteBtn.addClickListener(e -> delete());
184 deleteBtn.setVisible(false);
185
186 buttonLayout.addComponents(deleteBtn, saveBtn, cancelBtn);
187 // deleteBtn is initially invisible, let saveBtn take all space
188 buttonLayout.setExpandRatio(saveBtn, 1);
189 buttonLayout.setComponentAlignment(deleteBtn, Alignment.TOP_RIGHT);
190 buttonLayout.setComponentAlignment(saveBtn, Alignment.TOP_RIGHT);
191 buttonLayout.setComponentAlignment(cancelBtn, Alignment.TOP_RIGHT);
192
193 statusMessageLabel.setSizeFull();
194 statusMessageLabel.setContentMode(com.vaadin.shared.ui.label.ContentMode.HTML);
195
196 HorizontalLayout statusMessageLayout = new HorizontalLayout();
197 statusMessageLayout.setSizeFull();
198 statusMessageLayout.addComponent(statusMessageLabel);
199 statusMessageLayout.setMargin(new MarginInfo(false, true, false, true));
200
201 mainLayout.addComponents(toolBar, fieldLayout, statusMessageLayout, buttonLayout);
202 mainLayout.setComponentAlignment(statusMessageLayout, Alignment.BOTTOM_RIGHT);
203 mainLayout.setComponentAlignment(toolBar, Alignment.TOP_RIGHT);
204
205 updateToolBarVisibility();
206
207 UI currentUI = UI.getCurrent();
208 //Note AM: why not "currentUI instanceof PopupEditorDefaultStatusMessageSource"
209 if(PopupEditorDefaultStatusMessageSource.class.isAssignableFrom(currentUI.getClass())){
210 String message = ((PopupEditorDefaultStatusMessageSource)currentUI).defaultStatusMarkup(this.getClass());
211 addStatusMessage(message);
212 }
213 }
214
215 protected VerticalLayout getMainLayout() {
216 return mainLayout;
217 }
218
219 protected Layout getFieldLayout() {
220 return fieldLayout;
221 }
222
223 private GridLayout gridLayout() {
224 if(gridLayoutCache == null){
225 if(fieldLayout instanceof GridLayout){
226 gridLayoutCache = (GridLayout)fieldLayout;
227 } else {
228 throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
229 }
230 }
231 return gridLayoutCache;
232 }
233
234 @Override
235 public void setReadOnly(boolean readOnly) {
236 super.setReadOnly(readOnly);
237 if(readOnly){
238 statusMessageLabel.setValue(READ_ONLY_MESSAGE_TEXT);
239 statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
240 } else {
241 statusMessageLabel.setValue(null);
242 }
243 statusMessageLabel.setVisible(readOnly);
244 logger.info("Set saveBtn.visible to " + !readOnly);
245 saveBtn.setVisible(!readOnly);
246 updateDeleteButtonState();
247 cancelBtn.setCaption(readOnly ? "Close" : "Cancel");
248 recursiveReadonly(readOnly, (AbstractComponentContainer)getFieldLayout());
249 }
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 protected AbstractLayout getToolBar() {
261 return toolBar;
262 }
263
264 protected void toolBarAdd(Component c) {
265 toolBar.addComponent(c, toolBar.getComponentIndex(toolBarButtonGroup) - 1);
266 updateToolBarVisibility();
267 }
268
269 protected void toolBarButtonGroupAdd(Component c) {
270 toolBarButtonGroup.addComponent(c);
271 updateToolBarVisibility();
272 }
273
274 protected void toolBarButtonGroupRemove(Component c) {
275 toolBarButtonGroup.removeComponent(c);
276 updateToolBarVisibility();
277 }
278
279 private void updateToolBarVisibility() {
280 boolean showToolbar = toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1;
281 toolBar.setVisible(toolBarButtonGroup.getComponentCount() + toolBar.getComponentCount() > 1);
282 if(!showToolbar){
283 mainLayout.setMargin(new MarginInfo(true, false, false, false));
284 } else {
285 mainLayout.setMargin(false);
286 }
287 }
288
289 /**
290 * The top tool-bar is initially invisible.
291 */
292 protected void setToolBarVisible(boolean visible){
293 toolBar.setVisible(true);
294 }
295
296 public boolean isAdvancedMode() {
297 return isAdvancedMode;
298 }
299
300 public void setAdvancedMode(boolean isAdvancedMode) {
301 this.isAdvancedMode = isAdvancedMode;
302 advancedModeComponents.forEach(c -> c.setVisible(isAdvancedMode));
303 }
304
305 public void setAdvancedModeEnabled(boolean activate){
306 if(activate && advancedModeButton == null){
307 advancedModeButton = new Button(FontAwesome.WRENCH); // FontAwesome.FLASK
308 advancedModeButton.setIconAlternateText("Advanced mode");
309 advancedModeButton.addStyleName(ValoTheme.BUTTON_TINY);
310 toolBarButtonGroupAdd(advancedModeButton);
311 advancedModeButton.addClickListener(e -> {
312 setAdvancedMode(!isAdvancedMode);
313 }
314 );
315
316 } else if(advancedModeButton != null) {
317 toolBarButtonGroupRemove(advancedModeButton);
318 advancedModeButton = null;
319 }
320 }
321
322 public void registerAdvancedModeComponents(Component ... c){
323 advancedModeComponents.addAll(Arrays.asList(c));
324 }
325
326 // ------------------------ event handler ------------------------ //
327
328 private class SaveHandler implements CommitHandler {
329
330 private static final long serialVersionUID = 2047223089707080659L;
331
332 @Override
333 public void preCommit(CommitEvent commitEvent) throws CommitException {
334 logger.debug("preCommit(), publishing EditorPreSaveEvent");
335 // notify the presenter to start a transaction
336 viewEventBus.publish(this, new EditorPreSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
337 }
338
339 @Override
340 public void postCommit(CommitEvent commitEvent) throws CommitException {
341 try {
342 if(logger.isTraceEnabled()){
343 logger.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
344 }
345 // notify the presenter to persist the bean and to commit the transaction
346 viewEventBus.publish(this, new EditorSaveEvent<DTO>(AbstractPopupEditor.this, getBean()));
347 if(logger.isTraceEnabled()){
348 logger.trace("postCommit() publishing DoneWithPopupEvent");
349 }
350 // notify the NavigationManagerBean to close the window and to dispose the view
351 viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(AbstractPopupEditor.this, Reason.SAVE));
352 } catch (Exception e) {
353 logger.error(e);
354 throw new CommitException("Failed to store data to backend", e);
355 }
356 }
357 }
358
359 protected void addCommitHandler(CommitHandler commitHandler) {
360 fieldGroup.addCommitHandler(commitHandler);
361 }
362
363 protected void cancelEditorDialog(){
364
365 if(fieldGroup.isModified()){
366
367 ContinueAlternativeCancelDialog editorModifiedDialog = new ContinueAlternativeCancelDialog(
368 "Cancel editor",
369 "<p>The editor has been modified.<br>Do you want to save your changes or discard them?<p>",
370 "Discard",
371 "Save");
372 ClickListener saveListener = e -> {editorModifiedDialog.close(); save();};
373 ClickListener discardListener = e -> {editorModifiedDialog.close(); cancel();};
374 ClickListener cancelListener = e -> editorModifiedDialog.close();
375 editorModifiedDialog.addAlternativeClickListener(saveListener);
376 editorModifiedDialog.addContinueClickListener(discardListener);
377 editorModifiedDialog.addCancelClickListener(cancelListener);
378
379 UI.getCurrent().addWindow(editorModifiedDialog);
380 } else {
381 cancel();
382 }
383 }
384
385 /**
386 * Cancel editing and discard all modifications.
387 */
388 @Override
389 public void cancel() {
390 fieldGroup.discard();
391 viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.CANCEL));
392 }
393
394 private void delete() {
395 viewEventBus.publish(this, new EditorDeleteEvent<DTO>(this, fieldGroup.getItemDataSource().getBean()));
396 viewEventBus.publish(EventScope.UI, this, new DoneWithPopupEvent(this, Reason.DELETE));
397 }
398
399 /**
400 * Save the changes made in the editor.
401 */
402 private void save() {
403 try {
404 fieldGroup.commit();
405 } catch (CommitException e) {
406 fieldGroup.getFields().forEach(f -> ((AbstractField<?>)f).setValidationVisible(true));
407 Throwable cause = e.getCause();
408 while(cause != null) {
409 if(cause instanceof FieldGroupInvalidValueException){
410 FieldGroupInvalidValueException invalidValueException = (FieldGroupInvalidValueException)cause;
411 updateFieldNotifications(invalidValueException.getInvalidFields());
412 int invalidFieldsCount = invalidValueException.getInvalidFields().size();
413 Notification.show("The entered data in " + invalidFieldsCount + " field" + (invalidFieldsCount > 1 ? "s": "") + " is incomplete or invalid.");
414 break;
415 } else if(cause instanceof PermissionDeniedException){
416 PermissionDeniedException permissionDeniedException = (PermissionDeniedException)cause;
417 Notification.show("Permission denied", permissionDeniedException.getMessage(), Type.ERROR_MESSAGE);
418 break;
419 }
420 cause = cause.getCause();
421 }
422 if(cause == null){
423 // no known exception type found
424 logger.error(e);
425 PopupEditorException pee = null;
426 try {
427 pee = new PopupEditorException("Error saving popup editor", this, e);
428 } catch (Throwable t) {
429 /* IGORE errors which happen during the construction of the PopupEditorException */
430 }
431 if(pee != null){
432 throw pee;
433 }
434 throw new RuntimeException(e);
435 }
436 }
437 }
438
439 private void updateFieldNotifications(Map<Field<?>, InvalidValueException> invalidFields) {
440 for(Field<?> f : invalidFields.keySet()){
441 if(f instanceof AbstractField){
442 String message = invalidFields.get(f).getHtmlMessage();
443 ((AbstractField<?>)f).setComponentError(new UserError(message, ContentMode.HTML, ErrorLevel.ERROR));
444 }
445 }
446 }
447
448 // ------------------------ field adding methods ------------------------ //
449
450 protected TextField addTextField(String caption, String propertyId) {
451 return addField(new TextFieldNFix(caption), propertyId);
452 }
453
454 protected TextField addTextField(String caption, String propertyId, int column1, int row1,
455 int column2, int row2)
456 throws OverlapsException, OutOfBoundsException {
457 return addField(new TextFieldNFix(caption), propertyId, column1, row1, column2, row2);
458 }
459
460 protected TextField addTextField(String caption, String propertyId, int column, int row)
461 throws OverlapsException, OutOfBoundsException {
462 return addField(new TextFieldNFix(caption), propertyId, column, row);
463 }
464
465 protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column1, int row1,
466 int column2, int row2)
467 throws OverlapsException, OutOfBoundsException {
468
469 SwitchableTextField field = new SwitchableTextField(caption);
470 field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
471 addComponent(field, column1, row1, column2, row2);
472 return field;
473 }
474
475 protected SwitchableTextField addSwitchableTextField(String caption, String textPropertyId, String switchPropertyId, int column, int row)
476 throws OverlapsException, OutOfBoundsException {
477
478 SwitchableTextField field = new SwitchableTextField(caption);
479 field.bindTo(fieldGroup, textPropertyId, switchPropertyId);
480 addComponent(field, column, row);
481 return field;
482 }
483
484 protected PopupDateField addDateField(String caption, String propertyId) {
485 return addField(new PopupDateField(caption), propertyId);
486 }
487
488 protected CheckBox addCheckBox(String caption, String propertyId) {
489 return addField(new CheckBox(caption), propertyId);
490 }
491
492 protected CheckBox addCheckBox(String caption, String propertyId, int column, int row){
493 return addField(new CheckBox(caption), propertyId, column, row);
494 }
495
496 protected <T extends Field> T addField(T field, String propertyId) {
497 fieldGroup.bind(field, propertyId);
498 if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
499 ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
500 }
501 addComponent(field);
502 return field;
503 }
504
505 /**
506 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
507 *
508 * @param field
509 * the field to be added, not <code>null</code>.
510 * @param propertyId
511 * @param column
512 * the column index, starting from 0.
513 * @param row
514 * the row index, starting from 0.
515 * @throws OverlapsException
516 * if the new component overlaps with any of the components
517 * already in the grid.
518 * @throws OutOfBoundsException
519 * if the cell is outside the grid area.
520 */
521 protected <T extends Field> T addField(T field, String propertyId, int column, int row)
522 throws OverlapsException, OutOfBoundsException {
523 fieldGroup.bind(field, propertyId);
524 if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
525 ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
526 }
527 addComponent(field, column, row);
528 return field;
529 }
530
531 /**
532 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
533 */
534 protected <T extends Field> T addField(T field, String propertyId, int column1, int row1,
535 int column2, int row2)
536 throws OverlapsException, OutOfBoundsException {
537 if(propertyId != null){
538 fieldGroup.bind(field, propertyId);
539 if(NestedFieldGroup.class.isAssignableFrom(field.getClass())){
540 ((NestedFieldGroup)field).registerParentFieldGroup(fieldGroup);
541 }
542 }
543 addComponent(field, column1, row1, column2, row2);
544 return field;
545 }
546
547 protected Field<?> getField(Object propertyId){
548 return fieldGroup.getField(propertyId);
549 }
550
551 public PropertyIdPath boundPropertyIdPath(Field<?> field){
552
553 PropertyIdPath propertyIdPath = null;
554 Object propertyId = fieldGroup.getPropertyId(field);
555
556 if(propertyId == null){
557 // not found in the editor field group. Maybe the field is bound to a nested fieldgroup?
558 // 1. find the NestedFieldGroup implementations from the field up to the editor
559 PropertyIdPath nestedPropertyIds = new PropertyIdPath();
560 Field<?> parentField = field;
561 HasComponents parentComponent = parentField.getParent();
562 logger.debug("field: " + parentField.getClass().getSimpleName());
563 while(parentComponent != null){
564 if (logger.isDebugEnabled()){logger.debug("parentComponent: " + parentComponent.getClass().getSimpleName());}
565 if(NestedFieldGroup.class.isAssignableFrom(parentComponent.getClass()) && AbstractField.class.isAssignableFrom(parentComponent.getClass())){
566 Optional<FieldGroup> parentFieldGroup = ((NestedFieldGroup)parentComponent).getFieldGroup();
567 if(parentFieldGroup.isPresent()){
568 Object propId = parentFieldGroup.get().getPropertyId(parentField);
569 if(propId != null){
570 if (logger.isDebugEnabled()){logger.debug("propId: " + propId.toString());}
571 nestedPropertyIds.addParent(propId);
572 }
573 if (logger.isDebugEnabled()){logger.debug("parentField: " + parentField.getClass().getSimpleName());}
574 parentField = (Field<?>)parentComponent;
575 } else {
576 if (logger.isDebugEnabled()){logger.debug("parentFieldGroup is null, continuing ...");}
577 }
578 } else if(parentComponent == this) {
579 // we reached the editor itself
580 Object propId = fieldGroup.getPropertyId(parentField);
581 if(propId != null){
582 if (logger.isDebugEnabled()){logger.debug("propId: " + propId.toString());}
583 nestedPropertyIds.addParent(propId);
584 }
585 propertyIdPath = nestedPropertyIds;
586 break;
587 }
588 parentComponent = parentComponent.getParent();
589 }
590 // 2. check the NestedFieldGroup binding the field is direct or indirect child component of the editor
591 // NO lONGER NEEDED
592 // parentComponent = parentField.getParent(); // get component containing the last parent field found
593 // while(true){
594 // if(parentComponent == getFieldLayout()){
595 // propertyIdPath = nestedPropertyIds;
596 // break;
597 // }
598 // parentComponent = parentComponent.getParent();
599 // }
600 } else {
601 propertyIdPath = new PropertyIdPath(propertyId);
602 }
603 return propertyIdPath;
604 }
605
606 protected void addComponent(Component component) {
607 fieldLayout.addComponent(component);
608 applyDefaultComponentStyles(component);
609 }
610
611 protected void bindField(Field field, String propertyId){
612 fieldGroup.bind(field, propertyId);
613 }
614
615 protected void unbindField(Field field){
616 fieldGroup.unbind(field);
617 }
618
619 public void applyDefaultComponentStyles(Component component) {
620 component.addStyleName(getDefaultComponentStyles());
621 }
622
623 protected abstract String getDefaultComponentStyles();
624
625 /**
626 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
627 * <p>
628 * Adds the component to the grid in cells column1,row1 (NortWest corner of
629 * the area.) End coordinates (SouthEast corner of the area) are the same as
630 * column1,row1. The coordinates are zero-based. Component width and height
631 * is 1.
632 *
633 * @param component
634 * the component to be added, not <code>null</code>.
635 * @param column
636 * the column index, starting from 0.
637 * @param row
638 * the row index, starting from 0.
639 * @throws OverlapsException
640 * if the new component overlaps with any of the components
641 * already in the grid.
642 * @throws OutOfBoundsException
643 * if the cell is outside the grid area.
644 */
645 public void addComponent(Component component, int column, int row)
646 throws OverlapsException, OutOfBoundsException {
647 applyDefaultComponentStyles(component);
648 gridLayout().addComponent(component, column, row, column, row);
649 }
650
651 /**
652 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
653 * <p>
654 * Adds a component to the grid in the specified area. The area is defined
655 * by specifying the upper left corner (column1, row1) and the lower right
656 * corner (column2, row2) of the area. The coordinates are zero-based.
657 * </p>
658 *
659 * <p>
660 * If the area overlaps with any of the existing components already present
661 * in the grid, the operation will fail and an {@link OverlapsException} is
662 * thrown.
663 * </p>
664 *
665 * @param component
666 * the component to be added, not <code>null</code>.
667 * @param column1
668 * the column of the upper left corner of the area <code>c</code>
669 * is supposed to occupy. The leftmost column has index 0.
670 * @param row1
671 * the row of the upper left corner of the area <code>c</code> is
672 * supposed to occupy. The topmost row has index 0.
673 * @param column2
674 * the column of the lower right corner of the area
675 * <code>c</code> is supposed to occupy.
676 * @param row2
677 * the row of the lower right corner of the area <code>c</code>
678 * is supposed to occupy.
679 * @throws OverlapsException
680 * if the new component overlaps with any of the components
681 * already in the grid.
682 * @throws OutOfBoundsException
683 * if the cells are outside the grid area.
684 */
685 public void addComponent(Component component, int column1, int row1,
686 int column2, int row2)
687 throws OverlapsException, OutOfBoundsException {
688 applyDefaultComponentStyles(component);
689 gridLayout().addComponent(component, column1, row1, column2, row2);
690 }
691
692 public void setSaveButtonEnabled(boolean enabled){
693 saveBtn.setEnabled(enabled);
694 }
695
696 protected void setSaveButtonVisible(boolean enabled){
697 saveBtn.setVisible(enabled);
698 }
699
700 protected void setSaveButtonCaption(String caption) {
701 saveBtn.setCaption(caption);
702 }
703
704 public void withDeleteButton(boolean withDelete){
705
706 this.withDeleteButton = withDelete;
707 if(withDeleteButton){
708 buttonLayout.setExpandRatio(saveBtn, 0);
709 buttonLayout.setExpandRatio(deleteBtn, 1);
710 } else {
711 buttonLayout.setExpandRatio(saveBtn, 1);
712 buttonLayout.setExpandRatio(deleteBtn, 0);
713 }
714 updateDeleteButtonState();
715 }
716
717 private void updateDeleteButtonState() {
718 deleteBtn.setVisible(withDeleteButton && !isReadOnly());
719 }
720
721 public boolean addStatusMessage(String message){
722 boolean returnVal = statusMessages.add(message);
723 updateStatusLabel();
724 return returnVal;
725 }
726
727 public boolean removeStatusMessage(String message){
728 boolean returnVal = statusMessages.remove(message);
729 updateStatusLabel();
730 return returnVal;
731 }
732
733 private void updateStatusLabel() {
734 String text = "";
735 for(String s : statusMessages){
736 text += s + "</br>";
737 }
738 statusMessageLabel.setValue(text);
739 statusMessageLabel.setVisible(!text.isEmpty());
740 statusMessageLabel.addStyleName(ValoTheme.LABEL_COLORED);
741 }
742
743 private void updateContextBreadcrumbs() {
744
745 List<EditorActionContext> contextInfo = new ArrayList<>(getEditorActionContext());
746 String breadcrumbs = "";
747 EditorActionContextFormatter formatter = new EditorActionContextFormatter();
748
749 int cnt = 0;
750 for(EditorActionContext cntxt : contextInfo){
751 cnt++;
752 boolean isLast = cnt == contextInfo.size();
753 boolean isFirst = cnt == 1;
754
755 boolean doClass = false; // will be removed in future
756 boolean classNameForMissingPropertyPath = true; // !doClass;
757 boolean doProperties = true;
758 boolean doCreateOrNew = !isFirst;
759 String contextmarkup = formatter.format(
760 cntxt,
761 new EditorActionContextFormat(doClass, doProperties, classNameForMissingPropertyPath, doCreateOrNew,
762 EditorActionContextFormat.TargetInfoType.FIELD_CAPTION, (isLast ? "active" : ""))
763 );
764 // if(!isLast){
765 // contextmarkup += " " + FontAwesome.ANGLE_RIGHT.getHtml() + " ";
766 // }
767 if(isLast){
768 contextmarkup = "<li><span class=\"crumb active\">" + contextmarkup + "</span></li>";
769 } else {
770 contextmarkup = "<li><span class=\"crumb\">" + contextmarkup + "</span></li>";
771 }
772 breadcrumbs += contextmarkup;
773 }
774 contextBreadcrumbsLabel.setValue("<ul class=\"breadcrumbs\">" + breadcrumbs + "</ul>");
775 }
776
777 // ------------------------ data binding ------------------------ //
778
779 protected void bindDesign(Component component) {
780 fieldLayout.removeAllComponents();
781 fieldGroup.bindMemberFields(component);
782 fieldLayout.addComponent(component);
783 }
784
785
786 public final void loadInEditor(Object identifier) {
787
788 DTO beanToEdit = getPresenter().loadBeanById(identifier);
789 fieldGroup.setItemDataSource(beanToEdit);
790 afterItemDataSourceSet();
791 getPresenter().onViewFormReady(beanToEdit);
792 updateContextBreadcrumbs();
793 isBeanLoaded = true;
794 }
795
796 /**
797 * Passes the beanInstantiator to the presenter method {@link AbstractEditorPresenter#setBeanInstantiator(BeanInstantiator)}
798 *
799 * @param beanInstantiator
800 */
801 public final void setBeanInstantiator(BeanInstantiator<DTO> beanInstantiator) {
802 if(AbstractCdmEditorPresenter.class.isAssignableFrom(getPresenter().getClass())){
803 ((CdmEditorPresenterBase)getPresenter()).setBeanInstantiator(beanInstantiator);
804 } else {
805 throw new RuntimeException("BeanInstantiator can only be set for popup editors with a peresenter of the type CdmEditorPresenterBase");
806 }
807 }
808
809 /**
810 * Returns the bean contained in the itemDatasource of the fieldGroup.
811 */
812 public DTO getBean() {
813 if(fieldGroup.getItemDataSource() != null){
814 return fieldGroup.getItemDataSource().getBean();
815
816 }
817 return null;
818 }
819
820 /**
821 * @return true once the bean has been loaded indicating that all fields have
822 * been setup configured so that the editor is ready for use.
823 */
824 public boolean isBeanLoaded() {
825 return isBeanLoaded;
826 }
827
828 /**
829 * This method should only be used by the presenter of this view
830 *
831 * @param bean
832 */
833 protected void updateItemDataSource(DTO bean) {
834 fieldGroup.getItemDataSource().setBean(bean);
835 }
836
837 /**
838 * This method is called after setting the item data source whereby the
839 * {@link FieldGroup#configureField(Field<?> field)} method will be called.
840 * In this method all fields are set to default states defined for the fieldGroup.
841 * <p>
842 * You can now implement this method if you need to modify the state or value of individual fields.
843 */
844 protected void afterItemDataSourceSet() {
845 if(editorComponentsConfigurator != null){
846 editorComponentsConfigurator.updateComponentStates(this);
847 }
848 }
849
850
851 // ------------------------ issue related temporary solutions --------------------- //
852 /**
853 * Publicly accessible equivalent to getPreseneter(), needed for
854 * managing the presenter listeners.
855 * <p>
856 * TODO: refactor the presenter listeners management to get rid of this method
857 *
858 * @return
859 * @deprecated marked deprecated to emphasize on the special character of this method
860 * which should only be used internally see #6673
861 */
862 @Deprecated
863 public P presenter() {
864 return getPresenter();
865 }
866
867 /**
868 * Returns the context of editor actions for this editor.
869 * The context submitted with {@link #setParentContext(Stack)} will be updated
870 * to represent the current context.
871 *
872 * @return the context
873 */
874 public Stack<EditorActionContext> getEditorActionContext() {
875 if(!isContextUpdated){
876 if(getBean() == null){
877 throw new RuntimeException("getContext() is only possible after the bean is loaded");
878 }
879 context.push(new EditorActionContext(getBean(), this));
880 isContextUpdated = true;
881 }
882 return context;
883 }
884
885 /**
886 * Attempts to find an item in the context of editor actions for this editor,
887 * having a parentView matching the <code>viewType</code> by {@link Class#isAssignableFrom(Class)}.
888 */
889 @SuppressWarnings("unchecked")
890 public <VIEW extends AbstractView> Optional<VIEW> findViewInEditorActionContext(Class<VIEW> viewType) {
891 Stack<EditorActionContext> ctxt = getEditorActionContext();
892 for(int i = ctxt.size(); i > 0; --i) {
893 if(viewType.isAssignableFrom(ctxt.get(i).getParentView().getClass())){
894 return Optional.of((VIEW) ctxt.get(i).getParentView());
895 }
896 }
897 return Optional.empty();
898 }
899
900 /**
901 * Set the context of editor actions parent to this editor
902 *
903 * @param context the context to set
904 */
905 public void setParentEditorActionContext(Stack<EditorActionContext> context, Field<?> targetField) {
906 if(context != null){
907 this.context.addAll(context);
908 }
909 if(targetField != null){
910 this.context.get(context.size() - 1).setTargetField(targetField);
911 }
912 }
913
914 protected AbstractField<String> replaceComponent(String propertyId, AbstractField<String> oldField,
915 AbstractField<String> newField, int column1, int row1, int column2, int row2) {
916
917 String value = oldField.getValue();
918 newField.setCaption(oldField.getCaption());
919 GridLayout grid = (GridLayout)getFieldLayout();
920 grid.removeComponent(oldField);
921
922 unbindField(oldField);
923 addField(newField, propertyId, column1, row1, column2, row2);
924 getViewEventBus().publish(this, new FieldReplaceEvent(this, oldField, newField));
925 // important: set newField value at last!
926 newField.setValue(value);
927 return newField;
928 }
929
930 public EditorFormConfigurator<? extends AbstractPopupEditor<DTO,P,V>> getEditorComponentsConfigurator() {
931 return editorComponentsConfigurator;
932 }
933
934 public void setEditorComponentsConfigurator(
935 EditorFormConfigurator<? extends AbstractPopupEditor<DTO,P,V>> editorComponentsConfigurator) {
936 this.editorComponentsConfigurator = editorComponentsConfigurator;
937 }
938 }