Project

General

Profile

Download (19.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.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, false);
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
        setInternalValue(newValue, true);
220

    
221
    }
222

    
223
    protected void setInternalValue(List<V> newValue, boolean doUpdateFields) {
224

    
225
        super.setInternalValue(newValue);
226

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

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

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

    
247
    private void createFieldsForData(){
248

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

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

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

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

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

    
338
    private Component buttonGroup(F field){
339

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

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

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

    
356

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

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

    
373
        return buttonGroup;
374
    }
375

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

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

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

    
396
    private void updateButtonStates(){
397

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

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

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

    
407
            int addButtonIndex = 0;
408
            if(withEditButton){
409
                addButtonIndex++;
410
                // edit
411
                ((Button)buttonGroup.getComponent(0)).setDescription(field.getValue() == null ? "New" : "Edit");
412
                buttonGroup.getComponent(0).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());
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)