Project

General

Profile

Download (19.7 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.vaadin.event.EditorActionType;
33
import eu.etaxonomy.vaadin.event.EntityEditorActionEvent;
34
import eu.etaxonomy.vaadin.event.EntityEditorActionListener;
35
import eu.etaxonomy.vaadin.permission.EditPermissionTester;
36

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

    
49
    private static final long serialVersionUID = 4670707714503199599L;
50

    
51
    private static final Logger logger = Logger.getLogger(ToManyRelatedEntitiesListSelect.class);
52

    
53
    protected Class<F> fieldType;
54

    
55
    protected Class<V> itemType;
56

    
57
    private FieldGroup parentFieldGroup = null;
58

    
59
    private Boolean valueInitiallyWasNull = null;
60

    
61
    protected boolean isOrderedCollection = false;
62

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

    
68
    protected boolean addEmptyRowOnInitContent = true;
69

    
70
    private EntityFieldInstantiator<F> entityFieldInstantiator;
71

    
72
    private EditPermissionTester editPermissionTester;
73

    
74
    private EntityEditorActionListener editActionListener;
75

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

    
81
    private int GRID_COLS = 2;
82

    
83
    private GridLayout grid = new GridLayout(GRID_COLS, 1);
84

    
85
    private boolean creatingFields;
86

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

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

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

    
114
        List<V> nestedValues = getValueFromNestedFields();
115

    
116
        if(isOrderedCollection){
117

    
118
        } else {
119

    
120
        }
121

    
122
        Integer row = findRow(field);
123

    
124
        grid.insertRow(row + 1);
125

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

    
131
    }
132

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

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

    
146

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

    
153
        Integer row = findRow(field);
154
        swapRows(row);
155
    }
156

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

    
163
        Integer row = findRow(field);
164
        swapRows(row - 1);
165
    }
166

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

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

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

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

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

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

    
221
        super.setInternalValue(newValue);
222

    
223
        if(valueInitiallyWasNull == null){
224
            valueInitiallyWasNull = newValue == null;
225
        }
226

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

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

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

    
250
    private void createFieldsForData(){
251

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

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

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

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

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

    
341
    private Component buttonGroup(F field){
342

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

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

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

    
359

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

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

    
376
        return buttonGroup;
377
    }
378

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

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

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

    
399
    private void updateButtonStates(){
400

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

    
404
            boolean isFirst = row == 0;
405
            boolean isLast = row == fieldsCount - 1;
406

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

    
410
            int addButtonIndex = 0;
411
            if(withEditButton){
412
                addButtonIndex++;
413
                // edit
414
                Button editCreateButton = ((Button)buttonGroup.getComponent(0));
415
                editCreateButton.setDescription(field.getValue() == null ? "New" : "Edit");
416
                editCreateButton.setEnabled(field.getValue() == null
417
                        || field.getValue() != null && testEditButtonPermission(field.getValue()));
418
            }
419
            // add
420
            buttonGroup.getComponent(addButtonIndex).setEnabled(isLast || isOrderedCollection);
421
            // remove
422
            // can be always true, removing the last entry causes an new empty entry to be added.
423
            buttonGroup.getComponent(addButtonIndex + 1).setEnabled(true);
424
            // up
425
            if(isOrderedCollection && buttonGroup.getComponentCount() >  addButtonIndex + 2){
426
                buttonGroup.getComponent(addButtonIndex + 2).setEnabled(!isFirst);
427
                // down
428
                buttonGroup.getComponent(addButtonIndex + 3).setEnabled(!isLast);
429
            }
430
        }
431
    }
432

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

    
445

    
446
    protected List<F> getNestedFields(){
447

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

    
460
    /**
461
     *
462
     * @param val
463
     * @return
464
     * @throws InstantiationException
465
     * @throws IllegalAccessException
466
     */
467
    protected F newFieldInstance(V val) throws InstantiationException, IllegalAccessException {
468

    
469
        F field;
470
        if(entityFieldInstantiator != null){
471
            field = entityFieldInstantiator.createNewInstance();
472
        } else {
473
            field = fieldType.newInstance();
474
        }
475

    
476
        field.setWidth(100, Unit.PERCENTAGE);
477
        field.setValue(val);
478

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

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

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

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

    
529
    /**
530
     * {@inheritDoc}
531
     */
532
    @Override
533
    public void commit() throws SourceException, InvalidValueException {
534

    
535
        List<F> nestedFields = getNestedFields();
536
        Set<F> emptyFields = new HashSet<>();
537
        for(F f : nestedFields){
538
            f.commit();
539
            if(f.getValue() == null){
540
                emptyFields.add(f);
541
            }
542
        }
543
        for(F deleteF : emptyFields){
544
            removeRow(deleteF);
545
        }
546
        /*
547
        List<V> list = (List<V>) getPropertyDataSource().getValue();
548

    
549
        Person p = Person.NewInstance();
550
        p.setTitleCache("Hacky", true);
551
        list.add((V) p);
552

    
553
        List<V> clonedList = new ArrayList<>(list);
554
        list.clear();
555
        for(V value : clonedList){
556
            if(value != null){
557
                list.add(value);
558
            }
559
        }
560
        //
561
         */
562
        // calling super.commit() is useless if operating on a transient property!!
563
        super.commit();
564
//        if(getValue().isEmpty() && valueInitiallyWasNull){
565
//            setPropertyDataSource(null);
566
//        }
567
    }
568

    
569
    /**
570
     * {@inheritDoc}
571
     */
572
    @Override
573
    public void setWidth(String width) {
574
        super.setWidth(width);
575
        grid.setWidth(width);
576
    }
577

    
578
    @Override
579
    public void setWidth(float width, Unit unit){
580
        super.setWidth(width, unit);
581
        if(grid != null){
582
            grid.setWidth(width, unit);
583
        }
584
    }
585

    
586
    /**
587
     * {@inheritDoc}
588
     */
589
    @Override
590
    protected void addDefaultStyles() {
591
        // no default styles
592
    }
593

    
594

    
595
    /**
596
     * with a button edit existing and to add new entities
597
     */
598
    public void withEditButton(boolean withEditButton){
599
        this.withEditButton = withEditButton;
600
    }
601

    
602
    /**
603
     * {@inheritDoc}
604
     */
605
    @Override
606
    public boolean hasNullContent() {
607

    
608
        for(Field f : getNestedFields()){
609
            if(f instanceof CompositeCustomField){
610
                if(!((CompositeCustomField)f).hasNullContent()){
611
                    return false;
612
                }
613
            }
614
        }
615
        return true;
616
    }
617

    
618
    /**
619
     * @return the enityFieldInstantiator
620
     */
621
    public EntityFieldInstantiator<F> getEntityFieldInstantiator() {
622
        return entityFieldInstantiator;
623
    }
624

    
625
    /**
626
     * @param enityFieldInstantiator the enityFieldInstantiator to set
627
     */
628
    public void setEntityFieldInstantiator(EntityFieldInstantiator<F> entityFieldInstantiator) {
629
        this.entityFieldInstantiator = entityFieldInstantiator;
630
    }
631

    
632
    /**
633
     * {@inheritDoc}
634
     */
635
    @Override
636
    public void setReadOnly(boolean readOnly) {
637
        super.setReadOnly(readOnly);
638
        setDeepReadOnly(readOnly, getContent(), null);
639
        updateButtonStates();
640
    }
641

    
642
    /**
643
     * @return the editPermissionTester
644
     */
645
    public EditPermissionTester getEditPermissionTester() {
646
        return editPermissionTester;
647
    }
648

    
649
    /**
650
     * @param editPermissionTester the editPermissionTester to set
651
     */
652
    public void setEditPermissionTester(EditPermissionTester editPermissionTester) {
653
        this.editPermissionTester = editPermissionTester;
654
    }
655

    
656
    /**
657
     * @return the editActionListener
658
     */
659
    public EntityEditorActionListener getEditActionListener() {
660
        return editActionListener;
661
    }
662

    
663
    /**
664
     * @param editActionListener the editActionListener to set
665
     */
666
    public void setEditActionListener(EntityEditorActionListener editActionListener) {
667
        this.editActionListener = editActionListener;
668
    }
669

    
670
}
(14-14/17)