2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
9 package eu
.etaxonomy
.vaadin
.component
;
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
;
19 import org
.apache
.logging
.log4j
.LogManager
;
20 import org
.apache
.logging
.log4j
.Logger
;
21 import org
.osgi
.dto
.DTO
;
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
;
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
;
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).
49 * @author a.kohlbecker
53 public class ToManyRelatedEntitiesListSelect
<V
extends Object
, F
extends AbstractField
<V
>> extends CompositeCustomField
<List
<V
>> {
55 private static final long serialVersionUID
= 4670707714503199599L;
57 private static final Logger logger
= LogManager
.getLogger();
59 protected Class
<F
> fieldType
;
61 protected Class
<V
> itemType
;
63 private FieldGroup parentFieldGroup
= null;
65 private Boolean valueInitiallyWasNull
= null;
67 protected boolean isOrderedCollection
= false;
70 * with a button to edit existing and to add new entities
72 private boolean withEditButton
= false;
74 protected boolean addEmptyRowOnInitContent
= true;
76 private EntityFieldInstantiator
<F
> entityFieldInstantiator
;
78 private EditPermissionTester editPermissionTester
;
80 private EntityEditorActionListener editActionListener
;
83 * X index of the data field in the grid
85 private static final int GRID_X_FIELD
= 0;
87 private static final int GRID_X_BUTTON_GROUP
= 1;
89 private int GRID_COLS
= 2;
91 private GridLayout grid
= new GridLayout(GRID_COLS
, 1);
93 private boolean creatingFields
;
95 private List
<Validator
> fieldValidators
= new ArrayList
<>();
97 public ToManyRelatedEntitiesListSelect(Class
<V
> itemType
, Class
<F
> fieldType
, String caption
){
98 this.fieldType
= fieldType
;
99 this.itemType
= itemType
;
107 protected Integer
findRow(F field
) {
109 for(int r
= 0; r
< grid
.getRows(); r
++){
110 if(grid
.getComponent(GRID_X_FIELD
, r
).equals(field
)){
120 * @return an unmodifiable List of the data Fields
122 protected List
<F
> fields() {
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
));
135 private void addRowAfter(F field
) {
137 List
<V
> nestedValues
= getValueFromNestedFields();
139 if(isOrderedCollection
){
145 Integer row
= findRow(field
);
147 grid
.insertRow(row
+ 1);
149 // setting null as value for new rows
150 // see newFieldInstance() !!!
151 addNewRow(row
+ 1, null);
160 private void removeRow(F field
) {
162 Integer row
= findRow(field
);
164 // TODO remove from nested fields
166 updateComponentStates();
174 private void moveRowDown(F field
) {
176 Integer row
= findRow(field
);
184 private void moveRowUp(F field
) {
186 Integer row
= findRow(field
);
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();
200 throw new RuntimeException("Cannot swap rows out of the grid bounds");
205 * update Value is only called in turn of UI changes like adding, removing, swapping rows
207 private void updateValue() {
208 List
<V
> nestedValues
= getValueFromNestedFields();
209 List
<V
> beanList
= getValue();
210 if(beanList
!= null){
212 beanList
.addAll(nestedValues
);
214 setInternalValue(beanList
);
221 protected Component
initContent() {
222 grid
.setColumnExpandRatio(0, 1.0f
);
223 // set internal value to null to add an empty row
225 if(addEmptyRowOnInitContent
){
227 setInternalValue(null);
236 public Class
getType() {
244 protected void setInternalValue(List
<V
> newValue
) {
246 super.setInternalValue(newValue
);
248 if(valueInitiallyWasNull
== null){
249 valueInitiallyWasNull
= newValue
== null;
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
260 isOrderedCollection
= isListType
;
266 createFieldsForData();
270 private void clearRows() {
271 grid
.removeAllComponents();
275 private void createFieldsForData(){
277 creatingFields
= true;
278 List
<V
> data
= getValue();
279 if(data
== null || data
.isEmpty()){
280 data
= Arrays
.asList((V
)null);
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
);
298 addNewRow(row
, data
.get(row
));
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
));
305 creatingFields
= false;
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.
314 * See also {@link AbstractCdmEditorPresenter#handleTransientProperties(DTO bean)}
318 public List
<V
> getValueFromNestedFields() {
319 List
<V
> nestedValues
= new ArrayList
<>();
320 for(F f
: getNestedFields()) {
322 String
.format("getValueFromNestedFields() - %s:%s",
323 f
!= null ? f
.getClass().getSimpleName() : "null",
324 f
!= null && f
.getValue() != null ? f
.getValue() : "null"
326 V value
= f
.getValue();
327 if(f
!= null /*&& value != null*/){
328 nestedValues
.add(f
.getValue());
335 * @param row the row index, starting from 0.
339 protected int addNewRow(int row
, V val
) {
341 F field
= newFieldInstance(val
);
342 for(Validator validator
: fieldValidators
) {
343 field
.addValidator(validator
);
345 ButtonGroup buttonGroup
= new ButtonGroup(field
);
346 updateEditOrCreateButton(buttonGroup
, val
);
347 field
.addValueChangeListener(e
-> {
350 Object value
= e
.getProperty().getValue();
351 updateEditOrCreateButton(buttonGroup
, value
);
352 fireValueChange(true);
355 Property ds
= getPropertyDataSource();
357 Object parentVal
= ds
.getValue();
359 addStyledComponent(field
);
361 // important! all fields must be un-buffered
362 field
.setBuffered(false);
364 if(getNestedFields().size() == grid
.getRows()){
365 grid
.setRows(grid
.getRows() + 1);
367 grid
.addComponent(field
, GRID_X_FIELD
, row
);
368 grid
.addComponent(buttonGroup
, GRID_X_BUTTON_GROUP
, row
);
369 updateComponentStates();
370 nestFieldGroup(field
);
372 } catch (InstantiationException e
) {
373 // TODO Auto-generated catch block
375 } catch (IllegalAccessException e
) {
376 // TODO Auto-generated catch block
386 public void updateEditOrCreateButton(ButtonGroup buttonGroup
, Object value
) {
388 if(!withEditButton
|| buttonGroup
== null || buttonGroup
.getEditOrCreateButton() == null){
392 ButtonFactory buttonStyle
;
394 buttonStyle
= ButtonFactory
.CREATE_NEW
;
396 buttonStyle
= ButtonFactory
.EDIT_ITEM
;
398 buttonGroup
.getEditOrCreateButton().setIcon(buttonStyle
.getIcon());
399 buttonGroup
.getEditOrCreateButton().setDescription(buttonStyle
.getDescription());
402 class ButtonGroup
extends CssLayout
{
404 private Button editOrCreate
;
406 ButtonGroup (F field
){
408 Button add
= ButtonFactory
.ADD_ITEM
.createButton();
409 add
.setDescription("Add item");
410 add
.addClickListener(e
-> addRowAfter(field
));
413 editOrCreate
= ButtonFactory
.EDIT_ITEM
.createButton();
414 editOrCreate
.addClickListener(e
-> editOrCreate(field
));
415 addComponent(editOrCreate
);
416 addStyledComponents(editOrCreate
);
419 Button remove
= ButtonFactory
.REMOVE_ITEM
.createButton();
420 remove
.setDescription("Remove item");
421 remove
.addClickListener(e
-> removeRow(field
));
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");
435 addComponents(moveUp
, moveDown
);
436 addStyledComponents(moveUp
, moveDown
);
438 setStyleName(ValoTheme
.LAYOUT_COMPONENT_GROUP
);
441 Button
getEditOrCreateButton(){
451 private void editOrCreate(F field
) {
453 if(editActionListener
== null){
454 throw new RuntimeException("editActionListener missing");
457 if(field
.getValue() == null){
459 editActionListener
.onEntityEditorActionEvent(new EntityEditorActionEvent
<V
>(EditorActionType
.ADD
, null, field
));
462 V value
= field
.getValue();
463 editActionListener
.onEntityEditorActionEvent(new EntityEditorActionEvent
<V
>(EditorActionType
.EDIT
, (Class
<V
>) value
.getClass(), value
, field
));
467 private void updateComponentStates(){
469 boolean isWritable
= !getState().readOnly
;
470 int fieldsCount
= getNestedFields().size();
472 for(int row
= 0; row
< fieldsCount
; row
++){
474 boolean isFirst
= row
== 0;
475 boolean isLast
= row
== fieldsCount
- 1;
477 F field
= (F
) grid
.getComponent(GRID_X_FIELD
, row
);
478 CssLayout buttonGroup
= (CssLayout
) grid
.getComponent(GRID_X_FIELD
+ 1, row
);
480 boolean isWritableField
= isWritableField(field
);
481 field
.setReadOnly(!isWritableField
);
483 int addButtonIndex
= 0;
487 Button editCreateButton
= ((Button
)buttonGroup
.getComponent(0));
488 editCreateButton
.setDescription(field
.getValue() == null ?
"New" : "Edit");
491 buttonGroup
.getComponent(addButtonIndex
).setEnabled(isWritable
&& (isLast
|| isOrderedCollection
));
493 // can be always true, removing the last entry causes an new empty entry to be added.
494 buttonGroup
.getComponent(addButtonIndex
+ 1).setEnabled(isWritable
);
496 if(isOrderedCollection
&& buttonGroup
.getComponentCount() > addButtonIndex
+ 2){
497 buttonGroup
.getComponent(addButtonIndex
+ 2).setEnabled(isWritable
&& !isFirst
);
499 buttonGroup
.getComponent(addButtonIndex
+ 3).setEnabled(isWritable
&& !isLast
);
509 public boolean isWritableField(F field
) {
510 boolean isWritable
= !getState().readOnly
;
511 return isWritable
&& (field
.getValue() == null
512 || field
.getValue() != null && testEditButtonPermission(field
.getValue()));
519 protected boolean testEditButtonPermission(Object rowValue
) {
520 if(editPermissionTester
!= null) {
521 return editPermissionTester
.userHasEditPermission(rowValue
);
528 protected List
<F
> getNestedFields(){
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
);
534 logger
.debug(String
.format("NULL field at %d,%d", GRID_X_FIELD
, r
));
536 logger
.trace(String
.format("field " + f
.hashCode() + " at %d,%d", GRID_X_FIELD
, r
) + ", value: " + f
.getValue());
540 return Collections
.unmodifiableList(nestedFields
);
547 * @throws InstantiationException
548 * @throws IllegalAccessException
550 protected F
newFieldInstance(V val
) throws InstantiationException
, IllegalAccessException
{
553 if(entityFieldInstantiator
!= null){
554 field
= entityFieldInstantiator
.createNewInstance();
556 field
= fieldType
.newInstance();
559 field
.setWidth(100, Unit
.PERCENTAGE
);
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
570 * Adds the validator to the list of validators which
571 * are applied to new fields and adds the validator to
576 public void addFieldValidator(Validator validator
){
577 fieldValidators
.add(validator
);
578 for(F field
: fields()) {
579 field
.addValidator(validator
);
584 * removes the validator from the list of validators which
585 * are applied to new fields and removes the validator from
590 public void removeFieldValidator(Validator validator
){
591 fieldValidators
.remove(validator
);
592 for(F field
: fields()) {
593 field
.removeValidator(validator
);
598 * @return a unmodifialble List of the fieldValidators
600 public List
<Validator
> getFieldValidators() {
601 return Collections
.unmodifiableList(fieldValidators
);
605 * Handle the data binding of the sub fields. Sub-fields can either be composite editor fields
606 * or 'simple' fields, usually select fields.
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.
615 protected void nestFieldGroup(F field
) {
616 if(NestedFieldGroup
.class.isAssignableFrom(fieldType
) && parentFieldGroup
!= null){
617 ((NestedFieldGroup
)field
).registerParentFieldGroup(parentFieldGroup
);
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)}.
629 public Optional
<FieldGroup
> getFieldGroup() {
630 return Optional
.empty();
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}
643 public void registerParentFieldGroup(FieldGroup parent
) {
644 parentFieldGroup
= parent
;
651 public void commit() throws SourceException
, InvalidValueException
{
653 List
<F
> nestedFields
= getNestedFields();
654 Set
<F
> emptyFields
= new HashSet
<>();
655 for(F f
: nestedFields
){
657 if(f
.getValue() == null){
661 for(F deleteF
: emptyFields
){
665 List<V> list = (List<V>) getPropertyDataSource().getValue();
667 Person p = Person.NewInstance();
668 p.setTitleCache("Hacky", true);
671 List<V> clonedList = new ArrayList<>(list);
673 for(V value : clonedList){
680 // calling super.commit() is useless if operating on a transient property!!
682 // if(getValue().isEmpty() && valueInitiallyWasNull){
683 // setPropertyDataSource(null);
691 public void setWidth(String width
) {
692 super.setWidth(width
);
693 grid
.setWidth(width
);
697 public void setWidth(float width
, Unit unit
){
698 super.setWidth(width
, unit
);
700 grid
.setWidth(width
, unit
);
708 protected void addDefaultStyles() {
714 * with a button edit existing and to add new entities
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.");
727 public boolean hasNullContent() {
729 for(Field f
: getNestedFields()){
730 if(f
instanceof CompositeCustomField
){
731 if(!((CompositeCustomField
)f
).hasNullContent()){
740 * @return the enityFieldInstantiator
742 public EntityFieldInstantiator
<F
> getEntityFieldInstantiator() {
743 return entityFieldInstantiator
;
747 * @param enityFieldInstantiator the enityFieldInstantiator to set
749 public void setEntityFieldInstantiator(EntityFieldInstantiator
<F
> entityFieldInstantiator
) {
750 this.entityFieldInstantiator
= entityFieldInstantiator
;
757 public void setReadOnly(boolean readOnly
) {
758 super.setReadOnly(readOnly
);
759 updateComponentStates();
764 * @return the editPermissionTester
766 public EditPermissionTester
getEditPermissionTester() {
767 return editPermissionTester
;
771 * @param editPermissionTester the editPermissionTester to set
773 public void setEditPermissionTester(EditPermissionTester editPermissionTester
) {
774 this.editPermissionTester
= editPermissionTester
;
778 * @return the editActionListener
780 public EntityEditorActionListener
getEditActionListener() {
781 return editActionListener
;
785 * @param editActionListener the editActionListener to set
787 public void setEditActionListener(EntityEditorActionListener editActionListener
) {
788 this.editActionListener
= editActionListener
;