Project

General

Profile

Download (19.4 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.List;
15

    
16
import org.apache.log4j.Logger;
17

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

    
30
import eu.etaxonomy.vaadin.event.EditorActionType;
31
import eu.etaxonomy.vaadin.event.EntityEditorActionEvent;
32
import eu.etaxonomy.vaadin.event.EntityEditorActionListener;
33
import eu.etaxonomy.vaadin.permission.EditPermissionTester;
34

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

    
47
    private static final long serialVersionUID = 4670707714503199599L;
48

    
49
    private static final Logger logger = Logger.getLogger(ToManyRelatedEntitiesListSelect.class);
50

    
51
    protected Class<F> fieldType;
52

    
53
    protected Class<V> itemType;
54

    
55
    private FieldGroup parentFieldGroup = null;
56

    
57
    private Boolean valueInitiallyWasNull = null;
58

    
59
    protected boolean isOrderedCollection = false;
60

    
61
    /**
62
     * with a button to edit existing and to add new entities
63
     */
64
    private boolean withEditButton = false;
65

    
66
    protected boolean addEmptyRowOnInitContent = true;
67

    
68
    private EntityFieldInstantiator<F> entityFieldInstantiator;
69

    
70
    private EditPermissionTester editPermissionTester;
71

    
72
    private EntityEditorActionListener editActionListener;
73

    
74
    /**
75
     * X index of the data field in the grid
76
     */
77
    private static final int GRID_X_FIELD = 0;
78

    
79
    private int GRID_COLS = 2;
80

    
81
    private GridLayout grid = new GridLayout(GRID_COLS, 1);
82

    
83
    private boolean creatingFields;
84

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

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

    
106
    /**
107
     * @param field
108
     * @return
109
     */
110
    private void addRowAfter(F field) {
111

    
112
        List<V> nestedValues = getValueFromNestedFields();
113

    
114
        if(isOrderedCollection){
115

    
116
        } else {
117

    
118
        }
119

    
120
        Integer row = findRow(field);
121

    
122
        grid.insertRow(row + 1);
123

    
124
        // setting null as value for new rows
125
        // see newFieldInstance() !!!
126
        addNewRow(row + 1, null);
127
        updateValue();
128

    
129
    }
130

    
131
    /**
132
     * @param field
133
     * @return
134
     */
135
    private void removeRow(F field) {
136

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

    
144

    
145
    /**
146
     * @param field
147
     * @return
148
     */
149
    private void moveRowDown(F field) {
150

    
151
        Integer row = findRow(field);
152
        swapRows(row);
153
    }
154

    
155
    /**
156
     * @param field
157
     * @return
158
     */
159
    private void moveRowUp(F field) {
160

    
161
        Integer row = findRow(field);
162
        swapRows(row - 1);
163
    }
164

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

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

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

    
198
        if(addEmptyRowOnInitContent){
199
            // add an empty row
200
            setInternalValue(null);
201
        }
202
        return grid;
203
    }
204

    
205
    /**
206
     * {@inheritDoc}
207
     */
208
    @Override
209
    public Class getType() {
210
        return List.class;
211
    }
212

    
213
    /**
214
     * {@inheritDoc}
215
     */
216
    @Override
217
    protected void setInternalValue(List<V> newValue) {
218

    
219
        super.setInternalValue(newValue);
220

    
221
        if(valueInitiallyWasNull == null){
222
            valueInitiallyWasNull = newValue == null;
223
        }
224

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

    
238
        if(!creatingFields){
239
            createFieldsForData();
240
        }
241
    }
242

    
243
    private void clearRows() {
244
        grid.removeAllComponents();
245
        grid.setRows(1);
246
    }
247

    
248
    private void createFieldsForData(){
249

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

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

    
300
    /**
301
     * @param row the row index, starting from 0.
302
     * @param val
303
     * @return
304
     */
305
    protected int addNewRow(int row, V val) {
306
        try {
307
            F field = newFieldInstance(val);
308
            field.addValueChangeListener(e -> {
309
                updateValue();
310
                fireValueChange(true);
311
            });
312
            Property ds = getPropertyDataSource();
313
            if(ds != null){
314
                Object parentVal = ds.getValue();
315
            }
316
            addStyledComponent(field);
317

    
318
            // important! all fields must be un-buffered
319
            field.setBuffered(false);
320

    
321
            if(getNestedFields().size() == grid.getRows()){
322
                grid.setRows(grid.getRows() + 1);
323
            }
324
            grid.addComponent(field, GRID_X_FIELD, row);
325
            grid.addComponent(buttonGroup(field), 1, row);
326
            updateButtonStates();
327
            nestFieldGroup(field);
328
            row++;
329
        } catch (InstantiationException e) {
330
            // TODO Auto-generated catch block
331
            e.printStackTrace();
332
        } catch (IllegalAccessException e) {
333
            // TODO Auto-generated catch block
334
            e.printStackTrace();
335
        }
336
        return row;
337
    }
338

    
339
    private Component buttonGroup(F field){
340

    
341
        CssLayout buttonGroup = new CssLayout();
342
        Button add = new Button(FontAwesome.PLUS);
343
        add.setDescription("Add item");
344
        add.addClickListener(e -> addRowAfter(field));
345

    
346
        if(withEditButton){
347
            Button edit = new Button(FontAwesome.EDIT);
348
            edit.addClickListener(e -> editOrCreate(field));
349
            buttonGroup.addComponent(edit);
350
            addStyledComponents(edit);
351
        }
352

    
353
        Button remove = new Button(FontAwesome.MINUS);
354
        remove.setDescription("Remove item");
355
        remove.addClickListener(e -> removeRow(field));
356

    
357

    
358
        buttonGroup.addComponent(add);
359
        buttonGroup.addComponent(remove);
360
        addStyledComponents(add, remove);
361
        if(isOrderedCollection){
362
            Button moveUp = new Button(FontAwesome.ARROW_UP);
363
            moveUp.setDescription("Move up");
364
            moveUp.addClickListener(e -> moveRowUp(field));
365
            Button moveDown = new Button(FontAwesome.ARROW_DOWN);
366
            moveDown.addClickListener(e -> moveRowDown(field));
367
            moveDown.setDescription("Move down");
368

    
369
            buttonGroup.addComponents(moveUp, moveDown);
370
            addStyledComponents(moveUp, moveDown);
371
        }
372
        buttonGroup.setStyleName(ValoTheme.LAYOUT_COMPONENT_GROUP);
373

    
374
        return buttonGroup;
375
    }
376

    
377
    /**
378
     * @param e
379
     * @return
380
     */
381
    private void editOrCreate(F field) {
382

    
383
        if(editActionListener == null){
384
            throw new RuntimeException("editActionListener missing");
385
        }
386

    
387
        if(field.getValue() == null){
388
            // create
389
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.ADD, null, field));
390
        } else {
391
            // edit
392
            V value = field.getValue();
393
            editActionListener.onEntityEditorActionEvent(new EntityEditorActionEvent<V>(EditorActionType.EDIT, (Class<V>) value.getClass(), value, field));
394
        }
395
    }
396

    
397
    private void updateButtonStates(){
398

    
399
        int fieldsCount = getNestedFields().size();
400
        for(int row = 0; row < fieldsCount; row++){
401

    
402
            boolean isFirst = row == 0;
403
            boolean isLast = row == fieldsCount - 1;
404

    
405
            F field = (F) grid.getComponent(GRID_X_FIELD, row);
406
            CssLayout buttonGroup = (CssLayout) grid.getComponent(GRID_X_FIELD + 1, row);
407

    
408
            int addButtonIndex = 0;
409
            if(withEditButton){
410
                addButtonIndex++;
411
                // edit
412
                Button editCreateButton = ((Button)buttonGroup.getComponent(0));
413
                editCreateButton.setDescription(field.getValue() == null ? "New" : "Edit");
414
                editCreateButton.setEnabled(field.getValue() == null
415
                        || field.getValue() != null && testEditButtonPermission(field.getValue()));
416
            }
417
            // add
418
            buttonGroup.getComponent(addButtonIndex).setEnabled(isLast || isOrderedCollection);
419
            // remove
420
            buttonGroup.getComponent(addButtonIndex + 1).setEnabled(field.getValue() != null);
421
            // up
422
            if(isOrderedCollection && buttonGroup.getComponentCount() >  addButtonIndex + 2){
423
                buttonGroup.getComponent(addButtonIndex + 2).setEnabled(!isFirst);
424
                // down
425
                buttonGroup.getComponent(addButtonIndex + 3).setEnabled(!isLast);
426
            }
427
        }
428
    }
429

    
430
    /**
431
     * @param field
432
     * @return
433
     */
434
    protected boolean testEditButtonPermission(Object rowValue) {
435
        if(editPermissionTester != null) {
436
            return editPermissionTester.userHasEditPermission(rowValue);
437
        } else {
438
            return true;
439
        }
440
    }
441

    
442

    
443
    protected List<F> getNestedFields(){
444

    
445
        List<F> nestedFields = new ArrayList<>(grid.getRows());
446
        for(int r = 0; r < grid.getRows(); r++){
447
            F f = (F) grid.getComponent(GRID_X_FIELD, r);
448
            if(f == null){
449
                logger.debug(String.format("NULL field at %d,%d", GRID_X_FIELD, r));
450
            } else {
451
                nestedFields.add(f);
452
            }
453
        }
454
        return Collections.unmodifiableList(nestedFields);
455
    }
456

    
457
    /**
458
     *
459
     * @param val
460
     * @return
461
     * @throws InstantiationException
462
     * @throws IllegalAccessException
463
     */
464
    protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
465

    
466
        F field;
467
        if(entityFieldInstantiator != null){
468
            field = entityFieldInstantiator.createNewInstance();
469
        } else {
470
            field = fieldType.newInstance();
471
        }
472

    
473
        field.setWidth(100, Unit.PERCENTAGE);
474
        field.setValue(val);
475

    
476
        // TODO
477
        // when passing null as value the field must take care of creating a new
478
        // instance by overriding setValue() in future we could improve this by passing a
479
        // NewInstanceFactory to this class
480
        return field;
481
    }
482

    
483
    /**
484
     * Handle the data binding of the sub fields. Sub-fields can either be composite editor fields
485
     * or 'simple' fields, usually select fields.
486
     * <p>
487
     * Composite editor fields allow editing the nested bean Items and must implement the
488
     * {@link NestedFieldGroup} interface. Simple fields are only instantiated in
489
     * {@link #newFieldInstance(Object)} where the value of the field is set. No further binding is needed
490
     * for these 'simple' fields.
491
     *
492
     * @param field
493
     */
494
    protected void nestFieldGroup(F field) {
495
        if(NestedFieldGroup.class.isAssignableFrom(fieldType) && parentFieldGroup != null){
496
            ((NestedFieldGroup)field).registerParentFieldGroup(parentFieldGroup);
497
        }
498
    }
499

    
500
    /**
501
     * {@inheritDoc}
502
     * <p>
503
     * However, this class has no local fieldGroup but must delegate to the nested NestedFieldGroup
504
     * if there are any. This happens in {@link #nestFieldGroup(AbstractField)}.
505
     * <p>
506
     */
507
    @Override
508
    public FieldGroup getFieldGroup() {
509
        return null;
510
    }
511

    
512
    /**
513
     * This ToMany-CompositeCustomField has no own fields and this no local fieldGroup (see {@link #getFieldGroup()})
514
     * which allow changing data. Editing of the list items is delegated to
515
     * a list of sub-fields which are responsible for editing and committing the changes.
516
     * Therefore the <code>parentFieldGroup</code> is only stored in a local field so that it can
517
     * be passed to per item fields in {@link #nestFieldGroup}
518
     *
519
     * {@inheritDoc}
520
     */
521
    @Override
522
    public void registerParentFieldGroup(FieldGroup parent) {
523
        parentFieldGroup = parent;
524
    }
525

    
526
    /**
527
     * {@inheritDoc}
528
     */
529
    @Override
530
    public void commit() throws SourceException, InvalidValueException {
531

    
532
        List<F> nestedFields = getNestedFields();
533
        for(F f : nestedFields){
534
            f.commit();
535

    
536
        }
537
        /*
538
        List<V> list = (List<V>) getPropertyDataSource().getValue();
539

    
540
        Person p = Person.NewInstance();
541
        p.setTitleCache("Hacky", true);
542
        list.add((V) p);
543

    
544
        List<V> clonedList = new ArrayList<>(list);
545
        list.clear();
546
        for(V value : clonedList){
547
            if(value != null){
548
                list.add(value);
549
            }
550
        }
551
        //
552
         */
553
        // calling super.commit() is useless if operating on a transient property!!
554
        super.commit();
555
//        if(getValue().isEmpty() && valueInitiallyWasNull){
556
//            setPropertyDataSource(null);
557
//        }
558
    }
559

    
560
    /**
561
     * {@inheritDoc}
562
     */
563
    @Override
564
    public void setWidth(String width) {
565
        super.setWidth(width);
566
        grid.setWidth(width);
567
    }
568

    
569
    @Override
570
    public void setWidth(float width, Unit unit){
571
        super.setWidth(width, unit);
572
        if(grid != null){
573
            grid.setWidth(width, unit);
574
        }
575
    }
576

    
577
    /**
578
     * {@inheritDoc}
579
     */
580
    @Override
581
    protected void addDefaultStyles() {
582
        // no default styles
583
    }
584

    
585

    
586
    /**
587
     * with a button edit existing and to add new entities
588
     */
589
    public void withEditButton(boolean withEditButton){
590
        this.withEditButton = withEditButton;
591
    }
592

    
593
    /**
594
     * {@inheritDoc}
595
     */
596
    @Override
597
    public boolean hasNullContent() {
598

    
599
        for(Field f : getNestedFields()){
600
            if(f instanceof CompositeCustomField){
601
                if(!((CompositeCustomField)f).hasNullContent()){
602
                    return false;
603
                }
604
            }
605
        }
606
        return true;
607
    }
608

    
609
    /**
610
     * @return the enityFieldInstantiator
611
     */
612
    public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
613
        return entityFieldInstantiator;
614
    }
615

    
616
    /**
617
     * @param enityFieldInstantiator the enityFieldInstantiator to set
618
     */
619
    public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
620
        this.entityFieldInstantiator = entityFieldInstantiator;
621
    }
622

    
623
    /**
624
     * {@inheritDoc}
625
     */
626
    @Override
627
    public void setReadOnly(boolean readOnly) {
628
        super.setReadOnly(readOnly);
629
        setDeepReadOnly(readOnly, getContent(), null);
630
        updateButtonStates();
631
    }
632

    
633
    /**
634
     * @return the editPermissionTester
635
     */
636
    public EditPermissionTester getEditPermissionTester() {
637
        return editPermissionTester;
638
    }
639

    
640
    /**
641
     * @param editPermissionTester the editPermissionTester to set
642
     */
643
    public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
644
        this.editPermissionTester = editPermissionTester;
645
    }
646

    
647
    /**
648
     * @return the editActionListener
649
     */
650
    public EntityEditorActionListener getEditActionListener() {
651
        return editActionListener;
652
    }
653

    
654
    /**
655
     * @param editActionListener the editActionListener to set
656
     */
657
    public void setEditActionListener(EntityEditorActionListener editActionListener) {
658
        this.editActionListener = editActionListener;
659
    }
660

    
661
}
(11-11/14)