Project

General

Profile

Download (21.6 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.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.log4j.Logger;
20

    
21
import com.vaadin.data.Property;
22
import com.vaadin.data.Validator.InvalidValueException;
23
import com.vaadin.data.fieldgroup.FieldGroup;
24
import com.vaadin.server.FontAwesome;
25
import com.vaadin.ui.AbstractField;
26
import com.vaadin.ui.Button;
27
import com.vaadin.ui.Component;
28
import com.vaadin.ui.CssLayout;
29
import com.vaadin.ui.Field;
30
import com.vaadin.ui.GridLayout;
31
import com.vaadin.ui.themes.ValoTheme;
32

    
33
import eu.etaxonomy.cdm.vaadin.component.ButtonFactory;
34
import eu.etaxonomy.vaadin.event.EditorActionType;
35
import eu.etaxonomy.vaadin.event.EntityEditorActionEvent;
36
import eu.etaxonomy.vaadin.event.EntityEditorActionListener;
37
import eu.etaxonomy.vaadin.permission.EditPermissionTester;
38

    
39
/**
40
 * Manages the a collection of items internally as LinkedList<V>. If the Collection to operate on is a Set a Converter must be
41
 * set. Internally used fields are used in un-buffered mode. The actual instances of the field type <code>F</code> to be used to
42
 * edit or select the entities is created by a implementation of the <code>EntityFieldInstantiator</code>,
43
 * see {@link #setEntityFieldInstantiator(EntityFieldInstantiator).
44
 *
45
 * @author a.kohlbecker
46
 * @since May 11, 2017
47
 *
48
 */
49
public class ToManyRelatedEntitiesListSelect<V extends Object, F extends AbstractField<V>>  extends CompositeCustomField<List<V>> {
50

    
51
    private static final long serialVersionUID = 4670707714503199599L;
52

    
53
    private static final Logger logger = Logger.getLogger(ToManyRelatedEntitiesListSelect.class);
54

    
55
    protected Class<F> fieldType;
56

    
57
    protected Class<V> itemType;
58

    
59
    private FieldGroup parentFieldGroup = null;
60

    
61
    private Boolean valueInitiallyWasNull = null;
62

    
63
    protected boolean isOrderedCollection = false;
64

    
65
    /**
66
     * with a button to edit existing and to add new entities
67
     */
68
    private boolean withEditButton = false;
69

    
70
    protected boolean addEmptyRowOnInitContent = true;
71

    
72
    private EntityFieldInstantiator<F> entityFieldInstantiator;
73

    
74
    private EditPermissionTester editPermissionTester;
75

    
76
    private EntityEditorActionListener editActionListener;
77

    
78
    /**
79
     * X index of the data field in the grid
80
     */
81
    private static final int GRID_X_FIELD = 0;
82

    
83
    private static final int GRID_X_BUTTON_GROUP = 1;
84

    
85
    private int GRID_COLS = 2;
86

    
87
    private GridLayout grid = new GridLayout(GRID_COLS, 1);
88

    
89
    private boolean creatingFields;
90

    
91
    public  ToManyRelatedEntitiesListSelect(Class<V> itemType, Class<F> fieldType, String caption){
92
        this.fieldType = fieldType;
93
        this.itemType = itemType;
94
        setCaption(caption);
95
    }
96

    
97
    /**
98
     * @param field
99
     * @return
100
     */
101
    protected Integer findRow(F field) {
102
        Integer row = null;
103
        for(int r = 0; r < grid.getRows(); r++){
104
            if(grid.getComponent(GRID_X_FIELD, r).equals(field)){
105
                row = r;
106
                break;
107
            }
108
        }
109
        return row;
110
    }
111

    
112
    /**
113
     * @param field
114
     * @return
115
     */
116
    private void addRowAfter(F field) {
117

    
118
        List<V> nestedValues = getValueFromNestedFields();
119

    
120
        if(isOrderedCollection){
121

    
122
        } else {
123

    
124
        }
125

    
126
        Integer row = findRow(field);
127

    
128
        grid.insertRow(row + 1);
129

    
130
        // setting null as value for new rows
131
        // see newFieldInstance() !!!
132
        addNewRow(row + 1, null);
133
        updateValue();
134

    
135
    }
136

    
137
    /**
138
     * @param field
139
     * @return
140
     */
141
    private void removeRow(F field) {
142

    
143
        Integer row = findRow(field);
144
        grid.removeRow(row);
145
        // TODO remove from nested fields
146
        updateValue();
147
        updateButtonStates();
148
    }
149

    
150

    
151
    /**
152
     * @param field
153
     * @return
154
     */
155
    private void moveRowDown(F field) {
156

    
157
        Integer row = findRow(field);
158
        swapRows(row);
159
    }
160

    
161
    /**
162
     * @param field
163
     * @return
164
     */
165
    private void moveRowUp(F field) {
166

    
167
        Integer row = findRow(field);
168
        swapRows(row - 1);
169
    }
170

    
171
    /**
172
     * @param i
173
     */
174
    private void swapRows(int i) {
175
        if(i >= 0 && i + 1 < grid.getRows()){
176
            grid.replaceComponent(grid.getComponent(GRID_X_FIELD, i), grid.getComponent(GRID_X_FIELD, i + 1));
177
            grid.replaceComponent(grid.getComponent(GRID_X_FIELD  + 1 , i), grid.getComponent(GRID_X_FIELD + 1, i + 1));
178
            updateButtonStates();
179
            updateValue();
180
        } else {
181
            throw new RuntimeException("Cannot swap rows out of the grid bounds");
182
        }
183
    }
184

    
185
    /**
186
     * update Value is only called in turn of UI changes like adding, removing, swapping rows
187
     */
188
    private void updateValue() {
189
        List<V> nestedValues = getValueFromNestedFields();
190
        List<V> beanList = getValue();
191
        if(beanList != null){
192
            beanList.clear();
193
            beanList.addAll(nestedValues);
194
        }
195
        setInternalValue(beanList);
196
    }
197

    
198
    /**
199
     * {@inheritDoc}
200
     */
201
    @Override
202
    protected Component initContent() {
203
        grid.setColumnExpandRatio(0, 1.0f);
204
        // set internal value to null to add an empty row
205

    
206
        if(addEmptyRowOnInitContent){
207
            // add an empty row
208
            setInternalValue(null);
209
        }
210
        return grid;
211
    }
212

    
213
    /**
214
     * {@inheritDoc}
215
     */
216
    @Override
217
    public Class getType() {
218
        return List.class;
219
    }
220

    
221
    /**
222
     * {@inheritDoc}
223
     */
224
    @Override
225
    protected void setInternalValue(List<V> newValue) {
226

    
227
        super.setInternalValue(newValue);
228

    
229
        if(valueInitiallyWasNull == null){
230
            valueInitiallyWasNull = newValue == null;
231
        }
232

    
233
        if(newValue != null){
234
            // newValue is already converted, need to use the original value from the data source
235
            boolean isListType = List.class.isAssignableFrom(getPropertyDataSource().getValue().getClass());
236
            // if(valueInitiallyWasNull && isOrderedCollection != isListType){
237
            if(valueInitiallyWasNull && isOrderedCollection != isListType){
238
                // need to reset the grid in this case, so that the button groups are created correctly
239
                clearRows();
240
            }
241
            isOrderedCollection = isListType;
242
        } else {
243
            clearRows();
244
        }
245

    
246
        if(!creatingFields){
247
            createFieldsForData();
248
        }
249
    }
250

    
251
    private void clearRows() {
252
        grid.removeAllComponents();
253
        grid.setRows(1);
254
    }
255

    
256
    private void createFieldsForData(){
257

    
258
        creatingFields = true;
259
        List<V> data = getValue();
260
        if(data == null || data.isEmpty()){
261
            data = Arrays.asList((V)null);
262
        }
263
        for(int row = 0; row < data.size(); row++){
264
            boolean newRowNeeded = true;
265
            if(grid.getRows() > row){
266
                Component fieldComponent = grid.getComponent(GRID_X_FIELD, row);
267
                if(fieldComponent != null){
268
                    newRowNeeded = false;
269
                    F field = (F)fieldComponent;
270
                    if(data.get(row) != null && field.getValue() != data.get(row)){
271
                        field.setValue(data.get(row));
272
                    }
273
                }
274
            }
275
            if(newRowNeeded){
276
                addNewRow(row, data.get(row));
277
            } else {
278
                // update the editOrCreate buttons
279
                ButtonGroup bg = (ToManyRelatedEntitiesListSelect<V, F>.ButtonGroup) grid.getComponent(GRID_X_BUTTON_GROUP, row);
280
                updateEditOrCreateButton(bg, data.get(row));
281
            }
282
        }
283
        creatingFields = false;
284
    }
285

    
286
    /**
287
     * Obtains the List of values directly from the nested fields and ignores the
288
     * value of the <code>propertyDataSource</code>. This is useful when the ToManyRelatedEntitiesListSelect
289
     * is operating on a transient field, in which case the property is considered being read only by vaadin
290
     * so that the commit is doing nothing.
291
     *
292
     * See also {@link AbstractCdmEditorPresenter#handleTransientProperties(DTO bean)}
293
     *
294
     * @return
295
     */
296
    public List<V> getValueFromNestedFields() {
297
        List<V> nestedValues = new ArrayList<>();
298
        for(F f : getNestedFields()) {
299
            logger.trace(
300
                    String.format("getValueFromNestedFields() - %s:%s",
301
                       f != null ? f.getClass().getSimpleName() : "null",
302
                       f != null && f.getValue() != null ? f.getValue() : "null"
303
            ));
304
            V value = f.getValue();
305
            if(f != null /*&& value != null*/){
306
                nestedValues.add(f.getValue());
307
            }
308
         }
309
        return nestedValues;
310
    }
311

    
312
    /**
313
     * @param row the row index, starting from 0.
314
     * @param val
315
     * @return
316
     */
317
    protected int addNewRow(int row, V val) {
318
        try {
319
            F field = newFieldInstance(val);
320
            ButtonGroup buttonGroup = new ButtonGroup(field);
321
            updateEditOrCreateButton(buttonGroup, val);
322
            field.addValueChangeListener(e -> {
323
                if(!creatingFields){
324
                    updateValue();
325
                    Object value = e.getProperty().getValue();
326
                    updateEditOrCreateButton(buttonGroup, value);
327
                    fireValueChange(true);
328
                }
329
            });
330
            Property ds = getPropertyDataSource();
331
            if(ds != null){
332
                Object parentVal = ds.getValue();
333
            }
334
            addStyledComponent(field);
335

    
336
            // important! all fields must be un-buffered
337
            field.setBuffered(false);
338

    
339
            if(getNestedFields().size() == grid.getRows()){
340
                grid.setRows(grid.getRows() + 1);
341
            }
342
            grid.addComponent(field, GRID_X_FIELD, row);
343
            grid.addComponent(buttonGroup, GRID_X_BUTTON_GROUP, row);
344
            updateButtonStates();
345
            nestFieldGroup(field);
346
            row++;
347
        } catch (InstantiationException e) {
348
            // TODO Auto-generated catch block
349
            e.printStackTrace();
350
        } catch (IllegalAccessException e) {
351
            // TODO Auto-generated catch block
352
            e.printStackTrace();
353
        }
354
        return row;
355
    }
356

    
357
    /**
358
     * @param buttonGroup
359
     * @param value
360
     */
361
    public void updateEditOrCreateButton(ButtonGroup buttonGroup, Object value) {
362

    
363
        if(!withEditButton || buttonGroup == null || buttonGroup.getEditOrCreateButton() == null){
364
            return;
365
        }
366

    
367
        ButtonFactory buttonStyle;
368
        if(value == null){
369
            buttonStyle = ButtonFactory.CREATE_NEW;
370
        } else {
371
            buttonStyle = ButtonFactory.EDIT_ITEM;
372
        }
373
        buttonGroup.getEditOrCreateButton().setIcon(buttonStyle.getIcon());
374
        buttonGroup.getEditOrCreateButton().setDescription(buttonStyle.getDescription());
375
    }
376

    
377
    class ButtonGroup extends CssLayout{
378

    
379
        private Button editOrCreate;
380

    
381
        ButtonGroup (F field){
382

    
383
            Button add = ButtonFactory.ADD_ITEM.createButton();
384
            add.setDescription("Add item");
385
            add.addClickListener(e -> addRowAfter(field));
386

    
387
            if(withEditButton){
388
                editOrCreate = ButtonFactory.EDIT_ITEM.createButton();
389
                editOrCreate.addClickListener(e -> editOrCreate(field));
390
                addComponent(editOrCreate);
391
                addStyledComponents(editOrCreate);
392
            }
393

    
394
            Button remove = ButtonFactory.REMOVE_ITEM.createButton();
395
            remove.setDescription("Remove item");
396
            remove.addClickListener(e -> removeRow(field));
397

    
398

    
399
            addComponent(add);
400
            addComponent(remove);
401
            addStyledComponents(add, remove);
402
            if(isOrderedCollection){
403
                Button moveUp = new Button(FontAwesome.ARROW_UP);
404
                moveUp.setDescription("Move up");
405
                moveUp.addClickListener(e -> moveRowUp(field));
406
                Button moveDown = new Button(FontAwesome.ARROW_DOWN);
407
                moveDown.addClickListener(e -> moveRowDown(field));
408
                moveDown.setDescription("Move down");
409

    
410
                addComponents(moveUp, moveDown);
411
                addStyledComponents(moveUp, moveDown);
412
            }
413
            setStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
414
        }
415

    
416
        Button getEditOrCreateButton(){
417
            return editOrCreate;
418
        }
419

    
420
    }
421

    
422
    /**
423
     * @param e
424
     * @return
425
     */
426
    private void editOrCreate(F field) {
427

    
428
        if(editActionListener == null){
429
            throw new RuntimeException("editActionListener missing");
430
        }
431

    
432
        if(field.getValue() == null){
433
            // create
434
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.ADD, null, field));
435
        } else {
436
            // edit
437
            V value = field.getValue();
438
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.EDIT, (Class<V>) value.getClass(), value, field));
439
        }
440
    }
441

    
442
    private void updateButtonStates(){
443

    
444
        boolean isWritable = !getState().readOnly;
445
        int fieldsCount = getNestedFields().size();
446

    
447
        for(int row = 0; row < fieldsCount; row++){
448

    
449
            boolean isFirst = row == 0;
450
            boolean isLast = row == fieldsCount - 1;
451

    
452
            F field = (F) grid.getComponent(GRID_X_FIELD, row);
453
            CssLayout buttonGroup = (CssLayout) grid.getComponent(GRID_X_FIELD + 1, row);
454

    
455
            int addButtonIndex = 0;
456
            if(withEditButton){
457
                addButtonIndex++;
458
                // edit
459
                Button editCreateButton = ((Button)buttonGroup.getComponent(0));
460
                editCreateButton.setDescription(field.getValue() == null ? "New" : "Edit");
461
                editCreateButton.setEnabled(isWritable && (field.getValue() == null
462
                        || field.getValue() != null && testEditButtonPermission(field.getValue())));
463
            }
464
            // add
465
            buttonGroup.getComponent(addButtonIndex).setEnabled(isWritable && (isLast || isOrderedCollection));
466
            // remove
467
            // can be always true, removing the last entry causes an new empty entry to be added.
468
            buttonGroup.getComponent(addButtonIndex + 1).setEnabled(isWritable);
469
            // up
470
            if(isOrderedCollection && buttonGroup.getComponentCount() >  addButtonIndex + 2){
471
                buttonGroup.getComponent(addButtonIndex + 2).setEnabled(isWritable && !isFirst);
472
                // down
473
                buttonGroup.getComponent(addButtonIndex + 3).setEnabled(isWritable && !isLast);
474
            }
475
        }
476
    }
477

    
478
    /**
479
     * @param field
480
     * @return
481
     */
482
    protected boolean testEditButtonPermission(Object rowValue) {
483
        if(editPermissionTester != null) {
484
            return editPermissionTester.userHasEditPermission(rowValue);
485
        } else {
486
            return true;
487
        }
488
    }
489

    
490

    
491
    protected List<F> getNestedFields(){
492

    
493
        List<F> nestedFields = new ArrayList<>(grid.getRows());
494
        for(int r = 0; r < grid.getRows(); r++){
495
            F f = (F) grid.getComponent(GRID_X_FIELD, r);
496
            if(f == null){
497
                logger.debug(String.format("NULL field at %d,%d", GRID_X_FIELD, r));
498
            } else {
499
                logger.trace(String.format("field " + f.hashCode() + " at %d,%d", GRID_X_FIELD, r) + ", value: " + f.getValue());
500
                nestedFields.add(f);
501
            }
502
        }
503
        return Collections.unmodifiableList(nestedFields);
504
    }
505

    
506
    /**
507
     *
508
     * @param val
509
     * @return
510
     * @throws InstantiationException
511
     * @throws IllegalAccessException
512
     */
513
    protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
514

    
515
        F field;
516
        if(entityFieldInstantiator != null){
517
            field = entityFieldInstantiator.createNewInstance();
518
        } else {
519
            field = fieldType.newInstance();
520
        }
521

    
522
        field.setWidth(100, Unit.PERCENTAGE);
523
        field.setValue(val);
524

    
525
        // TODO
526
        // when passing null as value the field must take care of creating a new
527
        // instance by overriding setValue() in future we could improve this by passing a
528
        // NewInstanceFactory to this class
529
        return field;
530
    }
531

    
532
    /**
533
     * Handle the data binding of the sub fields. Sub-fields can either be composite editor fields
534
     * or 'simple' fields, usually select fields.
535
     * <p>
536
     * Composite editor fields allow editing the nested bean Items and must implement the
537
     * {@link NestedFieldGroup} interface. Simple fields are only instantiated in
538
     * {@link #newFieldInstance(Object)} where the value of the field is set. No further binding is needed
539
     * for these 'simple' fields.
540
     *
541
     * @param field
542
     */
543
    protected void nestFieldGroup(F field) {
544
        if(NestedFieldGroup.class.isAssignableFrom(fieldType) && parentFieldGroup != null){
545
            ((NestedFieldGroup)field).registerParentFieldGroup(parentFieldGroup);
546
        }
547
    }
548

    
549
    /**
550
     * {@inheritDoc}
551
     * <p>
552
     * However, this class has no local fieldGroup but must delegate to the nested NestedFieldGroup
553
     * if there are any. This happens in {@link #nestFieldGroup(AbstractField)}.
554
     * <p>
555
     */
556
    @Override
557
    public Optional<FieldGroup> getFieldGroup() {
558
        return Optional.empty();
559
    }
560

    
561
    /**
562
     * This ToMany-CompositeCustomField has no own fields and this no local fieldGroup (see {@link #getFieldGroup()})
563
     * which allow changing data. Editing of the list items is delegated to
564
     * a list of sub-fields which are responsible for editing and committing the changes.
565
     * Therefore the <code>parentFieldGroup</code> is only stored in a local field so that it can
566
     * be passed to per item fields in {@link #nestFieldGroup}
567
     *
568
     * {@inheritDoc}
569
     */
570
    @Override
571
    public void registerParentFieldGroup(FieldGroup parent) {
572
        parentFieldGroup = parent;
573
    }
574

    
575
    /**
576
     * {@inheritDoc}
577
     */
578
    @Override
579
    public void commit() throws SourceException, InvalidValueException {
580

    
581
        List<F> nestedFields = getNestedFields();
582
        Set<F> emptyFields = new HashSet<>();
583
        for(F f : nestedFields){
584
            f.commit();
585
            if(f.getValue() == null){
586
                emptyFields.add(f);
587
            }
588
        }
589
        for(F deleteF : emptyFields){
590
            removeRow(deleteF);
591
        }
592
        /*
593
        List<V> list = (List<V>) getPropertyDataSource().getValue();
594

    
595
        Person p = Person.NewInstance();
596
        p.setTitleCache("Hacky", true);
597
        list.add((V) p);
598

    
599
        List<V> clonedList = new ArrayList<>(list);
600
        list.clear();
601
        for(V value : clonedList){
602
            if(value != null){
603
                list.add(value);
604
            }
605
        }
606
        //
607
         */
608
        // calling super.commit() is useless if operating on a transient property!!
609
        super.commit();
610
//        if(getValue().isEmpty() && valueInitiallyWasNull){
611
//            setPropertyDataSource(null);
612
//        }
613
    }
614

    
615
    /**
616
     * {@inheritDoc}
617
     */
618
    @Override
619
    public void setWidth(String width) {
620
        super.setWidth(width);
621
        grid.setWidth(width);
622
    }
623

    
624
    @Override
625
    public void setWidth(float width, Unit unit){
626
        super.setWidth(width, unit);
627
        if(grid != null){
628
            grid.setWidth(width, unit);
629
        }
630
    }
631

    
632
    /**
633
     * {@inheritDoc}
634
     */
635
    @Override
636
    protected void addDefaultStyles() {
637
        // no default styles
638
    }
639

    
640

    
641
    /**
642
     * with a button edit existing and to add new entities
643
     */
644
    public void withEditButton(boolean withEditButton){
645
        this.withEditButton = withEditButton;
646
        if(getPropertyDataSource() != null) {
647
            throw new RuntimeException("withEditButton must not be changed after the datasource is set.");
648
        }
649
    }
650

    
651
    /**
652
     * {@inheritDoc}
653
     */
654
    @Override
655
    public boolean hasNullContent() {
656

    
657
        for(Field f : getNestedFields()){
658
            if(f instanceof CompositeCustomField){
659
                if(!((CompositeCustomField)f).hasNullContent()){
660
                    return false;
661
                }
662
            }
663
        }
664
        return true;
665
    }
666

    
667
    /**
668
     * @return the enityFieldInstantiator
669
     */
670
    public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
671
        return entityFieldInstantiator;
672
    }
673

    
674
    /**
675
     * @param enityFieldInstantiator the enityFieldInstantiator to set
676
     */
677
    public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
678
        this.entityFieldInstantiator = entityFieldInstantiator;
679
    }
680

    
681
    /**
682
     * {@inheritDoc}
683
     */
684
    @Override
685
    public void setReadOnly(boolean readOnly) {
686
        super.setReadOnly(readOnly);
687
        updateButtonStates();
688
    }
689

    
690

    
691
    /**
692
     * @return the editPermissionTester
693
     */
694
    public EditPermissionTester getEditPermissionTester() {
695
        return editPermissionTester;
696
    }
697

    
698
    /**
699
     * @param editPermissionTester the editPermissionTester to set
700
     */
701
    public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
702
        this.editPermissionTester = editPermissionTester;
703
    }
704

    
705
    /**
706
     * @return the editActionListener
707
     */
708
    public EntityEditorActionListener getEditActionListener() {
709
        return editActionListener;
710
    }
711

    
712
    /**
713
     * @param editActionListener the editActionListener to set
714
     */
715
    public void setEditActionListener(EntityEditorActionListener editActionListener) {
716
        this.editActionListener = editActionListener;
717
    }
718

    
719
}
(14-14/19)