Project

General

Profile

Download (20.8 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 int GRID_COLS = 2;
83

    
84
    private GridLayout grid = new GridLayout(GRID_COLS, 1);
85

    
86
    private boolean creatingFields;
87

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

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

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

    
115
        List<V> nestedValues = getValueFromNestedFields();
116

    
117
        if(isOrderedCollection){
118

    
119
        } else {
120

    
121
        }
122

    
123
        Integer row = findRow(field);
124

    
125
        grid.insertRow(row + 1);
126

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

    
132
    }
133

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

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

    
147

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

    
154
        Integer row = findRow(field);
155
        swapRows(row);
156
    }
157

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

    
164
        Integer row = findRow(field);
165
        swapRows(row - 1);
166
    }
167

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

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

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

    
201
        if(addEmptyRowOnInitContent){
202
            // add an empty row
203
            setInternalValue(null);
204
        }
205
        return grid;
206
    }
207

    
208
    /**
209
     * {@inheritDoc}
210
     */
211
    @Override
212
    public Class getType() {
213
        return List.class;
214
    }
215

    
216
    /**
217
     * {@inheritDoc}
218
     */
219
    @Override
220
    protected void setInternalValue(List<V> newValue) {
221

    
222
        super.setInternalValue(newValue);
223

    
224
        if(valueInitiallyWasNull == null){
225
            valueInitiallyWasNull = newValue == null;
226
        }
227

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

    
241
        if(!creatingFields){
242
            createFieldsForData();
243
        }
244
    }
245

    
246
    private void clearRows() {
247
        grid.removeAllComponents();
248
        grid.setRows(1);
249
    }
250

    
251
    private void createFieldsForData(){
252

    
253
        creatingFields = true;
254
        List<V> data = getValue();
255
        if(data == null || data.isEmpty()){
256
            data = Arrays.asList((V)null);
257
        }
258
        for(int row = 0; row < data.size(); row++){
259
            boolean newRowNeeded = true;
260
            if(grid.getRows() > row){
261
                Component fieldComponent = grid.getComponent(GRID_X_FIELD, row);
262
                if(fieldComponent != null){
263
                    newRowNeeded = false;
264
                    F field = (F)fieldComponent;
265
                    if(data.get(row) != null && field.getValue() != data.get(row)){
266
                        field.setValue(data.get(row));
267
                    }
268
                }
269
            }
270
            if(newRowNeeded){
271
                addNewRow(row, data.get(row));
272
            }
273
        }
274
        creatingFields = false;
275
    }
276

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

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

    
327
            // important! all fields must be un-buffered
328
            field.setBuffered(false);
329

    
330
            if(getNestedFields().size() == grid.getRows()){
331
                grid.setRows(grid.getRows() + 1);
332
            }
333
            grid.addComponent(field, GRID_X_FIELD, row);
334
            grid.addComponent(buttonGroup, 1, row);
335
            updateButtonStates();
336
            nestFieldGroup(field);
337
            row++;
338
        } catch (InstantiationException e) {
339
            // TODO Auto-generated catch block
340
            e.printStackTrace();
341
        } catch (IllegalAccessException e) {
342
            // TODO Auto-generated catch block
343
            e.printStackTrace();
344
        }
345
        return row;
346
    }
347

    
348
    /**
349
     * @param buttonGroup
350
     * @param value
351
     */
352
    public void updateEditOrCreateButton(ButtonGroup buttonGroup, Object value) {
353

    
354
        if(buttonGroup == null || buttonGroup.getEditOrCreateButton() == null){
355
            return;
356
        }
357

    
358
        ButtonFactory buttonStyle;
359
        if(value == null){
360
            buttonStyle = ButtonFactory.CREATE_NEW;
361
        } else {
362
            buttonStyle = ButtonFactory.EDIT_ITEM;
363
        }
364
        buttonGroup.getEditOrCreateButton().setIcon(buttonStyle.getIcon());
365
        buttonGroup.getEditOrCreateButton().setDescription(buttonStyle.getDescription());
366
    }
367

    
368
    class ButtonGroup extends CssLayout{
369

    
370
        private Button editOrCreate;
371

    
372
        ButtonGroup (F field){
373

    
374
            Button add = ButtonFactory.ADD_ITEM.createButton();
375
            add.setDescription("Add item");
376
            add.addClickListener(e -> addRowAfter(field));
377

    
378
            if(withEditButton){
379
                editOrCreate = ButtonFactory.EDIT_ITEM.createButton();
380
                editOrCreate.addClickListener(e -> editOrCreate(field));
381
                addComponent(editOrCreate);
382
                addStyledComponents(editOrCreate);
383
            }
384

    
385
            Button remove = ButtonFactory.REMOVE_ITEM.createButton();
386
            remove.setDescription("Remove item");
387
            remove.addClickListener(e -> removeRow(field));
388

    
389

    
390
            addComponent(add);
391
            addComponent(remove);
392
            addStyledComponents(add, remove);
393
            if(isOrderedCollection){
394
                Button moveUp = new Button(FontAwesome.ARROW_UP);
395
                moveUp.setDescription("Move up");
396
                moveUp.addClickListener(e -> moveRowUp(field));
397
                Button moveDown = new Button(FontAwesome.ARROW_DOWN);
398
                moveDown.addClickListener(e -> moveRowDown(field));
399
                moveDown.setDescription("Move down");
400

    
401
                addComponents(moveUp, moveDown);
402
                addStyledComponents(moveUp, moveDown);
403
            }
404
            setStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
405
        }
406

    
407
        Button getEditOrCreateButton(){
408
            return editOrCreate;
409
        }
410

    
411
    }
412

    
413
    /**
414
     * @param e
415
     * @return
416
     */
417
    private void editOrCreate(F field) {
418

    
419
        if(editActionListener == null){
420
            throw new RuntimeException("editActionListener missing");
421
        }
422

    
423
        if(field.getValue() == null){
424
            // create
425
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.ADD, null, field));
426
        } else {
427
            // edit
428
            V value = field.getValue();
429
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.EDIT, (Class<V>) value.getClass(), value, field));
430
        }
431
    }
432

    
433
    private void updateButtonStates(){
434

    
435
        int fieldsCount = getNestedFields().size();
436
        for(int row = 0; row < fieldsCount; row++){
437

    
438
            boolean isFirst = row == 0;
439
            boolean isLast = row == fieldsCount - 1;
440

    
441
            F field = (F) grid.getComponent(GRID_X_FIELD, row);
442
            CssLayout buttonGroup = (CssLayout) grid.getComponent(GRID_X_FIELD + 1, row);
443

    
444
            int addButtonIndex = 0;
445
            if(withEditButton){
446
                addButtonIndex++;
447
                // edit
448
                Button editCreateButton = ((Button)buttonGroup.getComponent(0));
449
                editCreateButton.setDescription(field.getValue() == null ? "New" : "Edit");
450
                editCreateButton.setEnabled(field.getValue() == null
451
                        || field.getValue() != null && testEditButtonPermission(field.getValue()));
452
            }
453
            // add
454
            buttonGroup.getComponent(addButtonIndex).setEnabled(isLast || isOrderedCollection);
455
            // remove
456
            // can be always true, removing the last entry causes an new empty entry to be added.
457
            buttonGroup.getComponent(addButtonIndex + 1).setEnabled(true);
458
            // up
459
            if(isOrderedCollection && buttonGroup.getComponentCount() >  addButtonIndex + 2){
460
                buttonGroup.getComponent(addButtonIndex + 2).setEnabled(!isFirst);
461
                // down
462
                buttonGroup.getComponent(addButtonIndex + 3).setEnabled(!isLast);
463
            }
464
        }
465
    }
466

    
467
    /**
468
     * @param field
469
     * @return
470
     */
471
    protected boolean testEditButtonPermission(Object rowValue) {
472
        if(editPermissionTester != null) {
473
            return editPermissionTester.userHasEditPermission(rowValue);
474
        } else {
475
            return true;
476
        }
477
    }
478

    
479

    
480
    protected List<F> getNestedFields(){
481

    
482
        List<F> nestedFields = new ArrayList<>(grid.getRows());
483
        for(int r = 0; r < grid.getRows(); r++){
484
            F f = (F) grid.getComponent(GRID_X_FIELD, r);
485
            if(f == null){
486
                logger.debug(String.format("NULL field at %d,%d", GRID_X_FIELD, r));
487
            } else {
488
                nestedFields.add(f);
489
            }
490
        }
491
        return Collections.unmodifiableList(nestedFields);
492
    }
493

    
494
    /**
495
     *
496
     * @param val
497
     * @return
498
     * @throws InstantiationException
499
     * @throws IllegalAccessException
500
     */
501
    protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
502

    
503
        F field;
504
        if(entityFieldInstantiator != null){
505
            field = entityFieldInstantiator.createNewInstance();
506
        } else {
507
            field = fieldType.newInstance();
508
        }
509

    
510
        field.setWidth(100, Unit.PERCENTAGE);
511
        field.setValue(val);
512

    
513
        // TODO
514
        // when passing null as value the field must take care of creating a new
515
        // instance by overriding setValue() in future we could improve this by passing a
516
        // NewInstanceFactory to this class
517
        return field;
518
    }
519

    
520
    /**
521
     * Handle the data binding of the sub fields. Sub-fields can either be composite editor fields
522
     * or 'simple' fields, usually select fields.
523
     * <p>
524
     * Composite editor fields allow editing the nested bean Items and must implement the
525
     * {@link NestedFieldGroup} interface. Simple fields are only instantiated in
526
     * {@link #newFieldInstance(Object)} where the value of the field is set. No further binding is needed
527
     * for these 'simple' fields.
528
     *
529
     * @param field
530
     */
531
    protected void nestFieldGroup(F field) {
532
        if(NestedFieldGroup.class.isAssignableFrom(fieldType) && parentFieldGroup != null){
533
            ((NestedFieldGroup)field).registerParentFieldGroup(parentFieldGroup);
534
        }
535
    }
536

    
537
    /**
538
     * {@inheritDoc}
539
     * <p>
540
     * However, this class has no local fieldGroup but must delegate to the nested NestedFieldGroup
541
     * if there are any. This happens in {@link #nestFieldGroup(AbstractField)}.
542
     * <p>
543
     */
544
    @Override
545
    public FieldGroup getFieldGroup() {
546
        return null;
547
    }
548

    
549
    /**
550
     * This ToMany-CompositeCustomField has no own fields and this no local fieldGroup (see {@link #getFieldGroup()})
551
     * which allow changing data. Editing of the list items is delegated to
552
     * a list of sub-fields which are responsible for editing and committing the changes.
553
     * Therefore the <code>parentFieldGroup</code> is only stored in a local field so that it can
554
     * be passed to per item fields in {@link #nestFieldGroup}
555
     *
556
     * {@inheritDoc}
557
     */
558
    @Override
559
    public void registerParentFieldGroup(FieldGroup parent) {
560
        parentFieldGroup = parent;
561
    }
562

    
563
    /**
564
     * {@inheritDoc}
565
     */
566
    @Override
567
    public void commit() throws SourceException, InvalidValueException {
568

    
569
        List<F> nestedFields = getNestedFields();
570
        Set<F> emptyFields = new HashSet<>();
571
        for(F f : nestedFields){
572
            f.commit();
573
            if(f.getValue() == null){
574
                emptyFields.add(f);
575
            }
576
        }
577
        for(F deleteF : emptyFields){
578
            removeRow(deleteF);
579
        }
580
        /*
581
        List<V> list = (List<V>) getPropertyDataSource().getValue();
582

    
583
        Person p = Person.NewInstance();
584
        p.setTitleCache("Hacky", true);
585
        list.add((V) p);
586

    
587
        List<V> clonedList = new ArrayList<>(list);
588
        list.clear();
589
        for(V value : clonedList){
590
            if(value != null){
591
                list.add(value);
592
            }
593
        }
594
        //
595
         */
596
        // calling super.commit() is useless if operating on a transient property!!
597
        super.commit();
598
//        if(getValue().isEmpty() && valueInitiallyWasNull){
599
//            setPropertyDataSource(null);
600
//        }
601
    }
602

    
603
    /**
604
     * {@inheritDoc}
605
     */
606
    @Override
607
    public void setWidth(String width) {
608
        super.setWidth(width);
609
        grid.setWidth(width);
610
    }
611

    
612
    @Override
613
    public void setWidth(float width, Unit unit){
614
        super.setWidth(width, unit);
615
        if(grid != null){
616
            grid.setWidth(width, unit);
617
        }
618
    }
619

    
620
    /**
621
     * {@inheritDoc}
622
     */
623
    @Override
624
    protected void addDefaultStyles() {
625
        // no default styles
626
    }
627

    
628

    
629
    /**
630
     * with a button edit existing and to add new entities
631
     */
632
    public void withEditButton(boolean withEditButton){
633
        this.withEditButton = withEditButton;
634
    }
635

    
636
    /**
637
     * {@inheritDoc}
638
     */
639
    @Override
640
    public boolean hasNullContent() {
641

    
642
        for(Field f : getNestedFields()){
643
            if(f instanceof CompositeCustomField){
644
                if(!((CompositeCustomField)f).hasNullContent()){
645
                    return false;
646
                }
647
            }
648
        }
649
        return true;
650
    }
651

    
652
    /**
653
     * @return the enityFieldInstantiator
654
     */
655
    public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
656
        return entityFieldInstantiator;
657
    }
658

    
659
    /**
660
     * @param enityFieldInstantiator the enityFieldInstantiator to set
661
     */
662
    public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
663
        this.entityFieldInstantiator = entityFieldInstantiator;
664
    }
665

    
666
    /**
667
     * {@inheritDoc}
668
     */
669
    @Override
670
    public void setReadOnly(boolean readOnly) {
671
        super.setReadOnly(readOnly);
672
        setDeepReadOnly(readOnly, getContent(), null);
673
        updateButtonStates();
674
    }
675

    
676
    /**
677
     * @return the editPermissionTester
678
     */
679
    public EditPermissionTester getEditPermissionTester() {
680
        return editPermissionTester;
681
    }
682

    
683
    /**
684
     * @param editPermissionTester the editPermissionTester to set
685
     */
686
    public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
687
        this.editPermissionTester = editPermissionTester;
688
    }
689

    
690
    /**
691
     * @return the editActionListener
692
     */
693
    public EntityEditorActionListener getEditActionListener() {
694
        return editActionListener;
695
    }
696

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

    
704
}
(14-14/17)