8e7a63cf088dd0f29f6fc480c372f5ff3acf2d3a
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / vaadin / component / ToManyRelatedEntitiesListSelect.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.component;
10
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.Collections;
14 import java.util.HashSet;
15 import java.util.List;
16 import java.util.Optional;
17 import java.util.Set;
18
19 import org.apache.logging.log4j.LogManager;
20 import org.apache.logging.log4j.Logger;
21 import org.osgi.dto.DTO;
22
23 import com.vaadin.data.Property;
24 import com.vaadin.data.Validator;
25 import com.vaadin.data.Validator.InvalidValueException;
26 import com.vaadin.data.fieldgroup.FieldGroup;
27 import com.vaadin.server.FontAwesome;
28 import com.vaadin.ui.AbstractField;
29 import com.vaadin.ui.Button;
30 import com.vaadin.ui.Component;
31 import com.vaadin.ui.CssLayout;
32 import com.vaadin.ui.Field;
33 import com.vaadin.ui.GridLayout;
34 import com.vaadin.ui.themes.ValoTheme;
35
36 import eu.etaxonomy.cdm.vaadin.component.ButtonFactory;
37 import eu.etaxonomy.vaadin.event.EditorActionType;
38 import eu.etaxonomy.vaadin.event.EntityEditorActionEvent;
39 import eu.etaxonomy.vaadin.event.EntityEditorActionListener;
40 import eu.etaxonomy.vaadin.mvp.AbstractCdmEditorPresenter;
41 import eu.etaxonomy.vaadin.permission.EditPermissionTester;
42
43 /**
44 * Manages the a collection of items internally as LinkedList<V>. If the Collection to operate on is a Set a Converter must be
45 * set. Internally used fields are used in un-buffered mode. The actual instances of the field type <code>F</code> to be used to
46 * edit or select the entities is created by a implementation of the <code>EntityFieldInstantiator</code>,
47 * see {@link #setEntityFieldInstantiator(EntityFieldInstantiator).
48 *
49 * @author a.kohlbecker
50 * @since May 11, 2017
51 *
52 */
53 public class ToManyRelatedEntitiesListSelect<V extends Object, F extends AbstractField<V>> extends CompositeCustomField<List<V>> {
54
55 private static final long serialVersionUID = 4670707714503199599L;
56
57 private static final Logger logger = LogManager.getLogger();
58
59 protected Class<F> fieldType;
60
61 protected Class<V> itemType;
62
63 private FieldGroup parentFieldGroup = null;
64
65 private Boolean valueInitiallyWasNull = null;
66
67 protected boolean isOrderedCollection = false;
68
69 /**
70 * with a button to edit existing and to add new entities
71 */
72 private boolean withEditButton = false;
73
74 protected boolean addEmptyRowOnInitContent = true;
75
76 private EntityFieldInstantiator<F> entityFieldInstantiator;
77
78 private EditPermissionTester editPermissionTester;
79
80 private EntityEditorActionListener editActionListener;
81
82 /**
83 * X index of the data field in the grid
84 */
85 private static final int GRID_X_FIELD = 0;
86
87 private static final int GRID_X_BUTTON_GROUP = 1;
88
89 private int GRID_COLS = 2;
90
91 private GridLayout grid = new GridLayout(GRID_COLS, 1);
92
93 private boolean creatingFields;
94
95 private List<Validator> fieldValidators = new ArrayList<>();
96
97 public ToManyRelatedEntitiesListSelect(Class<V> itemType, Class<F> fieldType, String caption){
98 this.fieldType = fieldType;
99 this.itemType = itemType;
100 setCaption(caption);
101 }
102
103 /**
104 * @param field
105 * @return
106 */
107 protected Integer findRow(F field) {
108 Integer row = null;
109 for(int r = 0; r < grid.getRows(); r++){
110 if(grid.getComponent(GRID_X_FIELD, r).equals(field)){
111 row = r;
112 break;
113 }
114 }
115 return row;
116 }
117
118 /**
119 *
120 * @return an unmodifiable List of the data Fields
121 */
122 protected List<F> fields() {
123 Integer row = null;
124 List<F> fields = new ArrayList<>();
125 for(int r = 0; r < grid.getRows(); r++){
126 fields.add((F) grid.getComponent(GRID_X_FIELD, r));
127 }
128 return fields;
129 }
130
131 /**
132 * @param field
133 * @return
134 */
135 private void addRowAfter(F field) {
136
137 List<V> nestedValues = getValueFromNestedFields();
138
139 if(isOrderedCollection){
140
141 } else {
142
143 }
144
145 Integer row = findRow(field);
146
147 grid.insertRow(row + 1);
148
149 // setting null as value for new rows
150 // see newFieldInstance() !!!
151 addNewRow(row + 1, null);
152 updateValue();
153
154 }
155
156 /**
157 * @param field
158 * @return
159 */
160 private void removeRow(F field) {
161
162 Integer row = findRow(field);
163 grid.removeRow(row);
164 // TODO remove from nested fields
165 updateValue();
166 updateComponentStates();
167 }
168
169
170 /**
171 * @param field
172 * @return
173 */
174 private void moveRowDown(F field) {
175
176 Integer row = findRow(field);
177 swapRows(row);
178 }
179
180 /**
181 * @param field
182 * @return
183 */
184 private void moveRowUp(F field) {
185
186 Integer row = findRow(field);
187 swapRows(row - 1);
188 }
189
190 /**
191 * @param i
192 */
193 private void swapRows(int i) {
194 if(i >= 0 && i + 1 < grid.getRows()){
195 grid.replaceComponent(grid.getComponent(GRID_X_FIELD, i), grid.getComponent(GRID_X_FIELD, i + 1));
196 grid.replaceComponent(grid.getComponent(GRID_X_FIELD + 1 , i), grid.getComponent(GRID_X_FIELD + 1, i + 1));
197 updateComponentStates();
198 updateValue();
199 } else {
200 throw new RuntimeException("Cannot swap rows out of the grid bounds");
201 }
202 }
203
204 /**
205 * update Value is only called in turn of UI changes like adding, removing, swapping rows
206 */
207 private void updateValue() {
208 List<V> nestedValues = getValueFromNestedFields();
209 List<V> beanList = getValue();
210 if(beanList != null){
211 beanList.clear();
212 beanList.addAll(nestedValues);
213 }
214 setInternalValue(beanList);
215 }
216
217 /**
218 * {@inheritDoc}
219 */
220 @Override
221 protected Component initContent() {
222 grid.setColumnExpandRatio(0, 1.0f);
223 // set internal value to null to add an empty row
224
225 if(addEmptyRowOnInitContent){
226 // add an empty row
227 setInternalValue(null);
228 }
229 return grid;
230 }
231
232 /**
233 * {@inheritDoc}
234 */
235 @Override
236 public Class getType() {
237 return List.class;
238 }
239
240 /**
241 * {@inheritDoc}
242 */
243 @Override
244 protected void setInternalValue(List<V> newValue) {
245
246 super.setInternalValue(newValue);
247
248 if(valueInitiallyWasNull == null){
249 valueInitiallyWasNull = newValue == null;
250 }
251
252 if(newValue != null){
253 // newValue is already converted, need to use the original value from the data source
254 boolean isListType = List.class.isAssignableFrom(getPropertyDataSource().getValue().getClass());
255 // if(valueInitiallyWasNull && isOrderedCollection != isListType){
256 if(valueInitiallyWasNull && isOrderedCollection != isListType){
257 // need to reset the grid in this case, so that the button groups are created correctly
258 clearRows();
259 }
260 isOrderedCollection = isListType;
261 } else {
262 clearRows();
263 }
264
265 if(!creatingFields){
266 createFieldsForData();
267 }
268 }
269
270 private void clearRows() {
271 grid.removeAllComponents();
272 grid.setRows(1);
273 }
274
275 private void createFieldsForData(){
276
277 creatingFields = true;
278 List<V> data = getValue();
279 if(data == null || data.isEmpty()){
280 data = Arrays.asList((V)null);
281 }
282 for(int row = 0; row < data.size(); row++){
283 boolean newRowNeeded = true;
284 if(grid.getRows() > row){
285 Component fieldComponent = grid.getComponent(GRID_X_FIELD, row);
286 if(fieldComponent != null){
287 newRowNeeded = false;
288 F field = (F)fieldComponent;
289 if(data.get(row) != null && field.getValue() != data.get(row)){
290 boolean roState = field.isReadOnly();
291 field.setReadOnly(false);
292 field.setValue(data.get(row));
293 field.setReadOnly(roState);
294 }
295 }
296 }
297 if(newRowNeeded){
298 addNewRow(row, data.get(row));
299 } else {
300 // update the editOrCreate buttons
301 ButtonGroup bg = (ToManyRelatedEntitiesListSelect<V, F>.ButtonGroup) grid.getComponent(GRID_X_BUTTON_GROUP, row);
302 updateEditOrCreateButton(bg, data.get(row));
303 }
304 }
305 creatingFields = false;
306 }
307
308 /**
309 * Obtains the List of values directly from the nested fields and ignores the
310 * value of the <code>propertyDataSource</code>. This is useful when the ToManyRelatedEntitiesListSelect
311 * is operating on a transient field, in which case the property is considered being read only by vaadin
312 * so that the commit is doing nothing.
313 *
314 * See also {@link AbstractCdmEditorPresenter#handleTransientProperties(DTO bean)}
315 *
316 * @return
317 */
318 public List<V> getValueFromNestedFields() {
319 List<V> nestedValues = new ArrayList<>();
320 for(F f : getNestedFields()) {
321 logger.trace(
322 String.format("getValueFromNestedFields() - %s:%s",
323 f != null ? f.getClass().getSimpleName() : "null",
324 f != null && f.getValue() != null ? f.getValue() : "null"
325 ));
326 V value = f.getValue();
327 if(f != null /*&& value != null*/){
328 nestedValues.add(f.getValue());
329 }
330 }
331 return nestedValues;
332 }
333
334 /**
335 * @param row the row index, starting from 0.
336 * @param val
337 * @return
338 */
339 protected int addNewRow(int row, V val) {
340 try {
341 F field = newFieldInstance(val);
342 for(Validator validator : fieldValidators) {
343 field.addValidator(validator);
344 }
345 ButtonGroup buttonGroup = new ButtonGroup(field);
346 updateEditOrCreateButton(buttonGroup, val);
347 field.addValueChangeListener(e -> {
348 if(!creatingFields){
349 updateValue();
350 Object value = e.getProperty().getValue();
351 updateEditOrCreateButton(buttonGroup, value);
352 fireValueChange(true);
353 }
354 });
355 Property ds = getPropertyDataSource();
356 if(ds != null){
357 Object parentVal = ds.getValue();
358 }
359 addStyledComponent(field);
360
361 // important! all fields must be un-buffered
362 field.setBuffered(false);
363
364 if(getNestedFields().size() == grid.getRows()){
365 grid.setRows(grid.getRows() + 1);
366 }
367 grid.addComponent(field, GRID_X_FIELD, row);
368 grid.addComponent(buttonGroup, GRID_X_BUTTON_GROUP, row);
369 updateComponentStates();
370 nestFieldGroup(field);
371 row++;
372 } catch (InstantiationException e) {
373 // TODO Auto-generated catch block
374 e.printStackTrace();
375 } catch (IllegalAccessException e) {
376 // TODO Auto-generated catch block
377 e.printStackTrace();
378 }
379 return row;
380 }
381
382 /**
383 * @param buttonGroup
384 * @param value
385 */
386 public void updateEditOrCreateButton(ButtonGroup buttonGroup, Object value) {
387
388 if(!withEditButton || buttonGroup == null || buttonGroup.getEditOrCreateButton() == null){
389 return;
390 }
391
392 ButtonFactory buttonStyle;
393 if(value == null){
394 buttonStyle = ButtonFactory.CREATE_NEW;
395 } else {
396 buttonStyle = ButtonFactory.EDIT_ITEM;
397 }
398 buttonGroup.getEditOrCreateButton().setIcon(buttonStyle.getIcon());
399 buttonGroup.getEditOrCreateButton().setDescription(buttonStyle.getDescription());
400 }
401
402 class ButtonGroup extends CssLayout{
403
404 private Button editOrCreate;
405
406 ButtonGroup (F field){
407
408 Button add = ButtonFactory.ADD_ITEM.createButton();
409 add.setDescription("Add item");
410 add.addClickListener(e -> addRowAfter(field));
411
412 if(withEditButton){
413 editOrCreate = ButtonFactory.EDIT_ITEM.createButton();
414 editOrCreate.addClickListener(e -> editOrCreate(field));
415 addComponent(editOrCreate);
416 addStyledComponents(editOrCreate);
417 }
418
419 Button remove = ButtonFactory.REMOVE_ITEM.createButton();
420 remove.setDescription("Remove item");
421 remove.addClickListener(e -> removeRow(field));
422
423
424 addComponent(add);
425 addComponent(remove);
426 addStyledComponents(add, remove);
427 if(isOrderedCollection){
428 Button moveUp = new Button(FontAwesome.ARROW_UP);
429 moveUp.setDescription("Move up");
430 moveUp.addClickListener(e -> moveRowUp(field));
431 Button moveDown = new Button(FontAwesome.ARROW_DOWN);
432 moveDown.addClickListener(e -> moveRowDown(field));
433 moveDown.setDescription("Move down");
434
435 addComponents(moveUp, moveDown);
436 addStyledComponents(moveUp, moveDown);
437 }
438 setStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
439 }
440
441 Button getEditOrCreateButton(){
442 return editOrCreate;
443 }
444
445 }
446
447 /**
448 * @param e
449 * @return
450 */
451 private void editOrCreate(F field) {
452
453 if(editActionListener == null){
454 throw new RuntimeException("editActionListener missing");
455 }
456
457 if(field.getValue() == null){
458 // create
459 editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.ADD, null, field));
460 } else {
461 // edit
462 V value = field.getValue();
463 editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.EDIT, (Class<V>) value.getClass(), value, field));
464 }
465 }
466
467 private void updateComponentStates(){
468
469 boolean isWritable = !getState().readOnly;
470 int fieldsCount = getNestedFields().size();
471
472 for(int row = 0; row < fieldsCount; row++){
473
474 boolean isFirst = row == 0;
475 boolean isLast = row == fieldsCount - 1;
476
477 F field = (F) grid.getComponent(GRID_X_FIELD, row);
478 CssLayout buttonGroup = (CssLayout) grid.getComponent(GRID_X_FIELD + 1, row);
479
480 boolean isWritableField = isWritableField(field);
481 field.setReadOnly(!isWritableField);
482
483 int addButtonIndex = 0;
484 if(withEditButton){
485 addButtonIndex++;
486 // edit
487 Button editCreateButton = ((Button)buttonGroup.getComponent(0));
488 editCreateButton.setDescription(field.getValue() == null ? "New" : "Edit");
489 }
490 // add
491 buttonGroup.getComponent(addButtonIndex).setEnabled(isWritable && (isLast || isOrderedCollection));
492 // remove
493 // can be always true, removing the last entry causes an new empty entry to be added.
494 buttonGroup.getComponent(addButtonIndex + 1).setEnabled(isWritable);
495 // up
496 if(isOrderedCollection && buttonGroup.getComponentCount() > addButtonIndex + 2){
497 buttonGroup.getComponent(addButtonIndex + 2).setEnabled(isWritable && !isFirst);
498 // down
499 buttonGroup.getComponent(addButtonIndex + 3).setEnabled(isWritable && !isLast);
500 }
501 }
502 }
503
504 /**
505 * @param isWritable
506 * @param field
507 * @return
508 */
509 public boolean isWritableField(F field) {
510 boolean isWritable = !getState().readOnly;
511 return isWritable && (field.getValue() == null
512 || field.getValue() != null && testEditButtonPermission(field.getValue()));
513 }
514
515 /**
516 * @param field
517 * @return
518 */
519 protected boolean testEditButtonPermission(Object rowValue) {
520 if(editPermissionTester != null) {
521 return editPermissionTester.userHasEditPermission(rowValue);
522 } else {
523 return true;
524 }
525 }
526
527
528 protected List<F> getNestedFields(){
529
530 List<F> nestedFields = new ArrayList<>(grid.getRows());
531 for(int r = 0; r < grid.getRows(); r++){
532 F f = (F) grid.getComponent(GRID_X_FIELD, r);
533 if(f == null){
534 logger.debug(String.format("NULL field at %d,%d", GRID_X_FIELD, r));
535 } else {
536 logger.trace(String.format("field " + f.hashCode() + " at %d,%d", GRID_X_FIELD, r) + ", value: " + f.getValue());
537 nestedFields.add(f);
538 }
539 }
540 return Collections.unmodifiableList(nestedFields);
541 }
542
543 /**
544 *
545 * @param val
546 * @return
547 * @throws InstantiationException
548 * @throws IllegalAccessException
549 */
550 protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
551
552 F field;
553 if(entityFieldInstantiator != null){
554 field = entityFieldInstantiator.createNewInstance();
555 } else {
556 field = fieldType.newInstance();
557 }
558
559 field.setWidth(100, Unit.PERCENTAGE);
560 field.setValue(val);
561
562 // TODO
563 // when passing null as value the field must take care of creating a new
564 // instance by overriding setValue() in future we could improve this by passing a
565 // NewInstanceFactory to this class
566 return field;
567 }
568
569 /**
570 * Adds the validator to the list of validators which
571 * are applied to new fields and adds the validator to
572 * existing fields
573 *
574 * @param validator
575 */
576 public void addFieldValidator(Validator validator){
577 fieldValidators.add(validator);
578 for(F field : fields()) {
579 field.addValidator(validator);
580 }
581 }
582
583 /**
584 * removes the validator from the list of validators which
585 * are applied to new fields and removes the validator from
586 * existing fields
587 *
588 * @param validator
589 */
590 public void removeFieldValidator(Validator validator){
591 fieldValidators.remove(validator);
592 for(F field : fields()) {
593 field.removeValidator(validator);
594 }
595 }
596
597 /**
598 * @return a unmodifialble List of the fieldValidators
599 */
600 public List<Validator> getFieldValidators() {
601 return Collections.unmodifiableList(fieldValidators);
602 }
603
604 /**
605 * Handle the data binding of the sub fields. Sub-fields can either be composite editor fields
606 * or 'simple' fields, usually select fields.
607 * <p>
608 * Composite editor fields allow editing the nested bean Items and must implement the
609 * {@link NestedFieldGroup} interface. Simple fields are only instantiated in
610 * {@link #newFieldInstance(Object)} where the value of the field is set. No further binding is needed
611 * for these 'simple' fields.
612 *
613 * @param field
614 */
615 protected void nestFieldGroup(F field) {
616 if(NestedFieldGroup.class.isAssignableFrom(fieldType) && parentFieldGroup != null){
617 ((NestedFieldGroup)field).registerParentFieldGroup(parentFieldGroup);
618 }
619 }
620
621 /**
622 * {@inheritDoc}
623 * <p>
624 * However, this class has no local fieldGroup but must delegate to the nested NestedFieldGroup
625 * if there are any. This happens in {@link #nestFieldGroup(AbstractField)}.
626 * <p>
627 */
628 @Override
629 public Optional<FieldGroup> getFieldGroup() {
630 return Optional.empty();
631 }
632
633 /**
634 * This ToMany-CompositeCustomField has no own fields and this no local fieldGroup (see {@link #getFieldGroup()})
635 * which allow changing data. Editing of the list items is delegated to
636 * a list of sub-fields which are responsible for editing and committing the changes.
637 * Therefore the <code>parentFieldGroup</code> is only stored in a local field so that it can
638 * be passed to per item fields in {@link #nestFieldGroup}
639 *
640 * {@inheritDoc}
641 */
642 @Override
643 public void registerParentFieldGroup(FieldGroup parent) {
644 parentFieldGroup = parent;
645 }
646
647 /**
648 * {@inheritDoc}
649 */
650 @Override
651 public void commit() throws SourceException, InvalidValueException {
652
653 List<F> nestedFields = getNestedFields();
654 Set<F> emptyFields = new HashSet<>();
655 for(F f : nestedFields){
656 f.commit();
657 if(f.getValue() == null){
658 emptyFields.add(f);
659 }
660 }
661 for(F deleteF : emptyFields){
662 removeRow(deleteF);
663 }
664 /*
665 List<V> list = (List<V>) getPropertyDataSource().getValue();
666
667 Person p = Person.NewInstance();
668 p.setTitleCache("Hacky", true);
669 list.add((V) p);
670
671 List<V> clonedList = new ArrayList<>(list);
672 list.clear();
673 for(V value : clonedList){
674 if(value != null){
675 list.add(value);
676 }
677 }
678 //
679 */
680 // calling super.commit() is useless if operating on a transient property!!
681 super.commit();
682 // if(getValue().isEmpty() && valueInitiallyWasNull){
683 // setPropertyDataSource(null);
684 // }
685 }
686
687 /**
688 * {@inheritDoc}
689 */
690 @Override
691 public void setWidth(String width) {
692 super.setWidth(width);
693 grid.setWidth(width);
694 }
695
696 @Override
697 public void setWidth(float width, Unit unit){
698 super.setWidth(width, unit);
699 if(grid != null){
700 grid.setWidth(width, unit);
701 }
702 }
703
704 /**
705 * {@inheritDoc}
706 */
707 @Override
708 protected void addDefaultStyles() {
709 // no default styles
710 }
711
712
713 /**
714 * with a button edit existing and to add new entities
715 */
716 public void withEditButton(boolean withEditButton){
717 this.withEditButton = withEditButton;
718 if(getPropertyDataSource() != null) {
719 throw new RuntimeException("withEditButton must not be changed after the datasource is set.");
720 }
721 }
722
723 /**
724 * {@inheritDoc}
725 */
726 @Override
727 public boolean hasNullContent() {
728
729 for(Field f : getNestedFields()){
730 if(f instanceof CompositeCustomField){
731 if(!((CompositeCustomField)f).hasNullContent()){
732 return false;
733 }
734 }
735 }
736 return true;
737 }
738
739 /**
740 * @return the enityFieldInstantiator
741 */
742 public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
743 return entityFieldInstantiator;
744 }
745
746 /**
747 * @param enityFieldInstantiator the enityFieldInstantiator to set
748 */
749 public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
750 this.entityFieldInstantiator = entityFieldInstantiator;
751 }
752
753 /**
754 * {@inheritDoc}
755 */
756 @Override
757 public void setReadOnly(boolean readOnly) {
758 super.setReadOnly(readOnly);
759 updateComponentStates();
760 }
761
762
763 /**
764 * @return the editPermissionTester
765 */
766 public EditPermissionTester getEditPermissionTester() {
767 return editPermissionTester;
768 }
769
770 /**
771 * @param editPermissionTester the editPermissionTester to set
772 */
773 public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
774 this.editPermissionTester = editPermissionTester;
775 }
776
777 /**
778 * @return the editActionListener
779 */
780 public EntityEditorActionListener getEditActionListener() {
781 return editActionListener;
782 }
783
784 /**
785 * @param editActionListener the editActionListener to set
786 */
787 public void setEditActionListener(EntityEditorActionListener editActionListener) {
788 this.editActionListener = editActionListener;
789 }
790
791 }