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

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

    
441

    
442
    protected List<F> getNestedFields(){
443

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
584

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

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

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

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

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

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

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

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

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

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

    
660
}
(11-11/14)