Project

General

Profile

Download (21.3 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
        beanList.clear();
191
        beanList.addAll(nestedValues);
192
        setInternalValue(beanList);
193
    }
194

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

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

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

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

    
224
        super.setInternalValue(newValue);
225

    
226
        if(valueInitiallyWasNull == null){
227
            valueInitiallyWasNull = newValue == null;
228
        }
229

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

    
243
        if(!creatingFields){
244
            createFieldsForData();
245
        }
246
    }
247

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

    
253
    private void createFieldsForData(){
254

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

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

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

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

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

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

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

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

    
374
    class ButtonGroup extends CssLayout{
375

    
376
        private Button editOrCreate;
377

    
378
        ButtonGroup (F field){
379

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

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

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

    
395

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

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

    
413
        Button getEditOrCreateButton(){
414
            return editOrCreate;
415
        }
416

    
417
    }
418

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

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

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

    
439
    private void updateButtonStates(){
440

    
441
        int fieldsCount = getNestedFields().size();
442
        for(int row = 0; row < fieldsCount; row++){
443

    
444
            boolean isFirst = row == 0;
445
            boolean isLast = row == fieldsCount - 1;
446

    
447
            F field = (F) grid.getComponent(GRID_X_FIELD, row);
448
            CssLayout buttonGroup = (CssLayout) grid.getComponent(GRID_X_FIELD + 1, row);
449

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

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

    
485

    
486
    protected List<F> getNestedFields(){
487

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

    
500
    /**
501
     *
502
     * @param val
503
     * @return
504
     * @throws InstantiationException
505
     * @throws IllegalAccessException
506
     */
507
    protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
508

    
509
        F field;
510
        if(entityFieldInstantiator != null){
511
            field = entityFieldInstantiator.createNewInstance();
512
        } else {
513
            field = fieldType.newInstance();
514
        }
515

    
516
        field.setWidth(100, Unit.PERCENTAGE);
517
        field.setValue(val);
518

    
519
        // TODO
520
        // when passing null as value the field must take care of creating a new
521
        // instance by overriding setValue() in future we could improve this by passing a
522
        // NewInstanceFactory to this class
523
        return field;
524
    }
525

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

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

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

    
569
    /**
570
     * {@inheritDoc}
571
     */
572
    @Override
573
    public void commit() throws SourceException, InvalidValueException {
574

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

    
589
        Person p = Person.NewInstance();
590
        p.setTitleCache("Hacky", true);
591
        list.add((V) p);
592

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

    
609
    /**
610
     * {@inheritDoc}
611
     */
612
    @Override
613
    public void setWidth(String width) {
614
        super.setWidth(width);
615
        grid.setWidth(width);
616
    }
617

    
618
    @Override
619
    public void setWidth(float width, Unit unit){
620
        super.setWidth(width, unit);
621
        if(grid != null){
622
            grid.setWidth(width, unit);
623
        }
624
    }
625

    
626
    /**
627
     * {@inheritDoc}
628
     */
629
    @Override
630
    protected void addDefaultStyles() {
631
        // no default styles
632
    }
633

    
634

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

    
645
    /**
646
     * {@inheritDoc}
647
     */
648
    @Override
649
    public boolean hasNullContent() {
650

    
651
        for(Field f : getNestedFields()){
652
            if(f instanceof CompositeCustomField){
653
                if(!((CompositeCustomField)f).hasNullContent()){
654
                    return false;
655
                }
656
            }
657
        }
658
        return true;
659
    }
660

    
661
    /**
662
     * @return the enityFieldInstantiator
663
     */
664
    public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
665
        return entityFieldInstantiator;
666
    }
667

    
668
    /**
669
     * @param enityFieldInstantiator the enityFieldInstantiator to set
670
     */
671
    public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
672
        this.entityFieldInstantiator = entityFieldInstantiator;
673
    }
674

    
675
    /**
676
     * {@inheritDoc}
677
     */
678
    @Override
679
    public void setReadOnly(boolean readOnly) {
680
        super.setReadOnly(readOnly);
681
        setDeepReadOnly(readOnly, getContent(), null);
682
        updateButtonStates();
683
    }
684

    
685
    /**
686
     * @return the editPermissionTester
687
     */
688
    public EditPermissionTester getEditPermissionTester() {
689
        return editPermissionTester;
690
    }
691

    
692
    /**
693
     * @param editPermissionTester the editPermissionTester to set
694
     */
695
    public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
696
        this.editPermissionTester = editPermissionTester;
697
    }
698

    
699
    /**
700
     * @return the editActionListener
701
     */
702
    public EntityEditorActionListener getEditActionListener() {
703
        return editActionListener;
704
    }
705

    
706
    /**
707
     * @param editActionListener the editActionListener to set
708
     */
709
    public void setEditActionListener(EntityEditorActionListener editActionListener) {
710
        this.editActionListener = editActionListener;
711
    }
712

    
713
}
(14-14/17)