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
                grid.setRows(1);
232
                grid.removeAllComponents();
233
            }
234
            isOrderedCollection = isListType;
235
        } else {
236
            // reset the grid
237
            grid.removeAllComponents();
238
            grid.setRows(1);
239
        }
240

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

    
246
    private void createFieldsForData(){
247

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

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

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

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

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

    
337
    private Component buttonGroup(F field){
338

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

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

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

    
355

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

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

    
372
        return buttonGroup;
373
    }
374

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

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

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

    
395
    private void updateButtonStates(){
396

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

    
400
            boolean isFirst = row == 0;
401
            boolean isLast = row == fieldsCount - 1;
402

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

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

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

    
440

    
441
    protected List<F> getNestedFields(){
442

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

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

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

    
471
        field.setWidth(100, Unit.PERCENTAGE);
472
        field.setValue(val);
473

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

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

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

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

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

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

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

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

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

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

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

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

    
583

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

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

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

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

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

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

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

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

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

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

    
659
}
(11-11/14)