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.Set;
17

    
18
import org.apache.log4j.Logger;
19

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

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

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

    
50
    private static final long serialVersionUID = 4670707714503199599L;
51

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

    
54
    protected Class<F> fieldType;
55

    
56
    protected Class<V> itemType;
57

    
58
    private FieldGroup parentFieldGroup = null;
59

    
60
    private Boolean valueInitiallyWasNull = null;
61

    
62
    protected boolean isOrderedCollection = false;
63

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

    
69
    protected boolean addEmptyRowOnInitContent = true;
70

    
71
    private EntityFieldInstantiator<F> entityFieldInstantiator;
72

    
73
    private EditPermissionTester editPermissionTester;
74

    
75
    private EntityEditorActionListener editActionListener;
76

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

    
82
    private static final int GRID_X_BUTTON_GROUP = 1;
83

    
84
    private int GRID_COLS = 2;
85

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

    
88
    private boolean creatingFields;
89

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

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

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

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

    
119
        if(isOrderedCollection){
120

    
121
        } else {
122

    
123
        }
124

    
125
        Integer row = findRow(field);
126

    
127
        grid.insertRow(row + 1);
128

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

    
134
    }
135

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

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

    
149

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

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

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

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

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

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

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

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

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

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

    
226
        super.setInternalValue(newValue);
227

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

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

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

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

    
255
    private void createFieldsForData(){
256

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

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

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

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

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

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

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

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

    
376
    class ButtonGroup extends CssLayout{
377

    
378
        private Button editOrCreate;
379

    
380
        ButtonGroup (F field){
381

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

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

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

    
397

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

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

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

    
419
    }
420

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

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

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

    
441
    private void updateButtonStates(){
442

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

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

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

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

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

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

    
489

    
490
    protected List<F> getNestedFields(){
491

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
639

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

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

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

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

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

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

    
689

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

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

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

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

    
718
}
(14-14/19)