de.symeda.sormas.ui.caze.AbstractTableField.java Source code

Java tutorial

Introduction

Here is the source code for de.symeda.sormas.ui.caze.AbstractTableField.java

Source

/*******************************************************************************
 * SORMAS - Surveillance Outbreak Response Management & Analysis System
 * Copyright  2016-2018 Helmholtz-Zentrum fr Infektionsforschung GmbH (HZI)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *******************************************************************************/
package de.symeda.sormas.ui.caze;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.CloneFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.icons.VaadinIcons;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;
import com.vaadin.v7.data.Container.ItemSetChangeEvent;
import com.vaadin.v7.data.Container.ItemSetChangeListener;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator.InvalidValueException;
import com.vaadin.v7.data.util.BeanItemContainer;
import com.vaadin.v7.data.util.converter.Converter.ConversionException;
import com.vaadin.v7.event.ItemClickEvent;
import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
import com.vaadin.v7.ui.CustomField;
import com.vaadin.v7.ui.Table;
import com.vaadin.v7.ui.Table.ColumnGenerator;

import de.symeda.sormas.api.i18n.Captions;
import de.symeda.sormas.api.i18n.I18nProperties;
import de.symeda.sormas.ui.utils.CssStyles;

/**
 * TODO replace table with grid?
 * TODO whole component seems to need improvement (e.g. should use setInternalValue instead of setValue)
 * Does probably not make sense, because of future update to Vaadin 8
 * 
 * @author Martin Wahnschaffe
 */
@SuppressWarnings({ "serial", "rawtypes" })
public abstract class AbstractTableField<E> extends CustomField<Collection> {

    private static final Logger logger = LoggerFactory.getLogger(AbstractTableField.class);

    public static final String EDIT_COLUMN_ID = "editColumn";

    protected static final String SCROLLBAR_FIX_CSS = "scrollbarFix";

    private int maxTablePageLength = 10;

    public int getMaxTablePageLength() {
        return maxTablePageLength;
    }

    public void setMaxTablePageLength(int maxTablePageLength) {
        this.maxTablePageLength = maxTablePageLength;
    }

    private VerticalLayout layout;
    private Label captionLabel;
    private Button addButton;
    private Table table;

    private Property<Collection<E>> dataSource;
    private BeanItemContainer<E> container;

    public AbstractTableField() {
        getContent();
    }

    /**
     * If there are different rows in the table 
     * and do not need to be scrolled vertically (<= 10 entries) 
     * (and expandRatio is used?), A horizontal scroll bar appears. 
     * This can be corrected in CSS as follows:
     * <pre>
     * .v-table-scrollbarFix .v-table-body-wrapper,
     * .v-table-scrollbarFix .v-table-body-wrapper:focus {
     *       overflow-y: hidden;
     * }
     * </pre>
     */
    protected void applyScrollbarFix() {
        if (table.getPageLength() == 0) {
            table.addStyleName(SCROLLBAR_FIX_CSS);
        } else {
            table.removeStyleName(SCROLLBAR_FIX_CSS);
        }
    }

    protected void applyTablePageLength() {
        if (getTable().size() <= getMaxTablePageLength()) {
            table.setPageLength(0);
        } else {
            table.setPageLength(getMaxTablePageLength());
        }
        applyScrollbarFix();
    }

    @Override
    protected Component initContent() {

        this.addStyleName(CssStyles.CAPTION_HIDDEN);
        this.addStyleName(CssStyles.VSPACE_2);

        layout = new VerticalLayout();
        layout.setSpacing(false);

        HorizontalLayout headerLayout = new HorizontalLayout();
        {
            headerLayout.setWidth(100, Unit.PERCENTAGE);

            captionLabel = new Label(getCaption());
            captionLabel.setSizeUndefined();
            headerLayout.addComponent(captionLabel);
            headerLayout.setComponentAlignment(captionLabel, Alignment.BOTTOM_LEFT);
            headerLayout.setExpandRatio(captionLabel, 0);

            addButton = createAddButton();
            headerLayout.addComponent(addButton);
            headerLayout.setComponentAlignment(addButton, Alignment.BOTTOM_RIGHT);
            headerLayout.setExpandRatio(addButton, 1);
        }
        layout.addComponent(headerLayout);

        table = createTable();
        table.addItemSetChangeListener(new ItemSetChangeListener() {
            @Override
            public void containerItemSetChange(ItemSetChangeEvent event) {
                applyTablePageLength();
            }
        });
        layout.addComponent(table);

        return layout;
    }

    @Override
    public void setCaption(String caption) {
        super.setCaption(caption);

        captionLabel.setValue(caption);
    }

    @Override
    public Class<Collection> getType() {
        return Collection.class;
    }

    public abstract Class<E> getEntryType();

    protected Table createTable() {

        final Table table = new Table();

        table.setEditable(false);
        table.setSelectable(false);
        table.setSizeFull();

        createEditColumn(table);

        return table;
    }

    protected void createEditColumn(Table table) {

        ColumnGenerator editColumnGenerator = new ColumnGenerator() {

            @Override
            public Object generateCell(Table source, Object itemId, Object columnId) {
                return generateEditCell(source, itemId, columnId);
            }
        };
        table.addGeneratedColumn(EDIT_COLUMN_ID, editColumnGenerator);
        table.setColumnWidth(EDIT_COLUMN_ID, 20);
        table.setColumnHeader(EDIT_COLUMN_ID, "");

        table.addItemClickListener(new ItemClickListener() {

            @SuppressWarnings("unchecked")
            @Override
            public void itemClick(ItemClickEvent event) {
                if (event.isDoubleClick() || EDIT_COLUMN_ID.equals(event.getPropertyId())) {
                    final E entry = (E) event.getItemId();
                    if (entry != null) {
                        editEntry(entry, false, result -> onEntryChanged(result));
                    }
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    protected Object generateEditCell(Table source, Object itemId, Object columnId) {
        Button button = new Button(VaadinIcons.EDIT, e -> {
            editEntry((E) itemId, false, result -> onEntryChanged(result));
        });
        button.setStyleName(ValoTheme.BUTTON_BORDERLESS);
        return button;
    }

    protected Button createAddButton() {

        Button button = new Button(I18nProperties.getCaption(Captions.actionNewEntry));
        button.addStyleName(ValoTheme.BUTTON_LINK);

        button.addClickListener(new ClickListener() {

            @Override
            public void buttonClick(ClickEvent event) {
                addEntry();
            }
        });

        return button;
    }

    /**
     * @see #createEntry()
     * @see #editEntry(Object)
     */
    protected void addEntry() {
        final E entry = createEntry();

        editEntry(entry, true, new Consumer<E>() {

            @Override
            public void accept(E result) {
                table.addItem(result);
                fireValueChange(false);
            }
        });
    }

    protected abstract void editEntry(E entry, boolean create, Consumer<E> commitCallback);

    /**
     * Override in order to do custom sort
     */
    protected void onEntryChanged(E entry) {
        getTable().refreshRowCache();

        fireValueChange(false);
    }

    /**
     * Set visible columns and their behavior. If entries are to be deleted, the "delete" column must be visible.
     */
    protected abstract void updateColumns();

    protected E createEntry() {
        // TODO good way to do it like this?
        try {
            return getEntryType().newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * Copy of an entry. All editing is then done within this copy. At commit, this entry replaces the old one.
     * 
     * @param sourceEntry original
     * @return
     */
    protected E cloneEntry(E sourceEntry) {
        if (sourceEntry == null) {
            return null;
        }
        E clone = ObjectUtils.clone(sourceEntry);
        if (clone == null) {
            throw new CloneFailedException("Entry is not Cloneable");
        }
        return clone;
    }

    /**
     * Delete entry (e.g., called by the user).
     */
    public void removeEntry(E entry) {
        // gewnschten Eintrag lschen
        getTable().removeItem(entry);

        fireValueChange(false);
    }

    /**
     * Check whether the entry is empty, so that such entries can be automatically removed from the list.
     */
    protected abstract boolean isEmpty(E entry);

    /**
     * Check whether the entry has changed in the past. Required for Field.isModified ().
     */
    protected abstract boolean isModified(E oldEntry, E newEntry);

    /**
     * Specifies the possibility to change the entry before it is commited.
     * 
     * @return May the entry be commited?
     */
    protected boolean preEntryCommit(E entry) {
        // don't commit empty entries
        return !isEmpty(entry);
    }

    /**
     * @return Unchanged original entry from the data source
     */
    protected E getUnmodifiedEntry(E entry) {
        Collection<E> oldEntries = dataSource.getValue();
        for (E oldEntry : oldEntries) {
            if (oldEntry.equals(entry)) {
                return oldEntry;
            }
        }
        return null;
    }

    /**
     * Property serves as a source for the items in the table.
     * A copy is created of all entries in the edit, so that the source data is not overwritten until the commit.
     */
    @SuppressWarnings("unchecked")
    @Override
    public void setPropertyDataSource(Property newDataSource) {

        if (newDataSource == dataSource || (newDataSource != null && newDataSource.equals(dataSource))) {
            return;
        }

        dataSource = newDataSource;

        Collection<E> entries = dataSource.getValue();
        if (entries == null) {
            throw new IllegalArgumentException("dataSource cannot be null");
        }

        Collection<E> clonedEntries;
        if (entries instanceof List) {
            clonedEntries = new ArrayList<>(entries.size());
        } else if (entries instanceof Set) {
            clonedEntries = new HashSet<>(entries.size());
        } else {
            throw new IllegalArgumentException("dataSource value must be List or Set: " + entries.getClass());
        }

        // Make a copy of all entries so that they can be freely edited
        // important: all fields must be placed on writeThrough!
        for (E entry : entries) {
            clonedEntries.add(cloneEntry(entry));
        }

        container = new BeanItemContainer<>(getEntryType(), clonedEntries);

        getContent();
        Table tbl = getTable();
        if (tbl.getContainerDataSource() != null) {
            // keep the visible Columns
            Object[] visibleColumns = tbl.getVisibleColumns();
            tbl.setContainerDataSource(container, Arrays.asList(visibleColumns));
        } else {
            tbl.setContainerDataSource(container);
        }
        applyTablePageLength();

        updateColumns();

        fireValueChange(false);

        // not set, we manage our own dataSource
        // super.setPropertyDataSource (newDataSource);
    }

    @Override
    public Property<Collection<E>> getPropertyDataSource() {
        return dataSource;
    }

    @Override
    public boolean isModified() {
        if (dataSource == null) {
            return false;
        }

        ArrayList<E> oldEntries = new ArrayList<>(dataSource.getValue());
        ArrayList<E> newEntries = new ArrayList<>(getValue());

        // go through "new entries": there is in each case an equal old?!
        Iterator<E> iterator = newEntries.iterator();
        int newEntriesCount = 0;
        while (iterator.hasNext()) {
            E newEntry = iterator.next();
            // ignore empty entries
            if (isEmpty(newEntry)) {
                continue;
            }
            // search for entry
            int oldIndex = oldEntries.indexOf(newEntry);
            if (oldIndex == -1) {
                // no entry found -> new -> modified
                return true;
            }
            E oldEntry = oldEntries.get(oldIndex);
            if (isModified(oldEntry, newEntry)) {
                // entry modified -> modified
                return true;
            }
            newEntriesCount++;
        }

        // less entries than before? -> modified
        if (newEntriesCount < oldEntries.size()) {
            return true;
        }

        return false;
    }

    /**
     * Auxiliary method for comparing two objects. Uses equals for comparing.
     */
    public static boolean isModifiedObject(Object oldObject, Object newObject) {
        if (oldObject == newObject) {
            return false;
        }
        if (oldObject == null || newObject == null) {
            return true;
        }
        if (!oldObject.equals(newObject)) {
            return true;
        }
        return false;
    }

    /**
     * Auxiliary method for comparing two collections. Used for equals () to compare.
     */
    public static boolean isModifiedCollection(Collection<?> oldCollection, Collection<?> newCollection) {

        if (oldCollection.size() != newCollection.size()) {
            return true;
        }

        // iterate through all entries in order
        Iterator<?> oldIterator = oldCollection.iterator();
        Iterator<?> newIterator = newCollection.iterator();

        while (newIterator.hasNext()) {
            Object oldObject = oldIterator.next();
            Object newObject = newIterator.next();

            // Eintrag anders?
            if (isModifiedObject(oldObject, newObject)) {
                return true;
            }
        }

        boolean hasNext = !oldIterator.hasNext();
        assert hasNext; // iteration should also be finished

        return false;
    }

    @Override
    public void commit() throws SourceException, InvalidValueException {

        // write new entries to data source
        if (dataSource != null && !dataSource.isReadOnly()) {
            if ((isInvalidCommitted() || isValid())) {

                // creata a copy
                Collection<E> entries = getValue();
                Collection<E> entriesCopy;
                if (dataSource.getValue() instanceof List) {
                    entriesCopy = new ArrayList<>(entries);
                } else {
                    // Set
                    entriesCopy = new LinkedHashSet<>(entries);
                }

                // remove unwanted entries
                for (E entry : entries) {
                    if (!preEntryCommit(entry)) {
                        entriesCopy.remove(entry);
                    }
                }

                // done
                dataSource.setValue(entriesCopy);

                fireValueChange(false);

            } else {
                /* An invalid value and we don't allow them, throw the exception */
                validate();
            }
        }

        // super.commit();
    }

    @Override
    public void discard() throws SourceException {

        // just reread the data source
        Property<Collection<E>> resetDataSource = dataSource;
        dataSource = null;
        setPropertyDataSource(resetDataSource);

        // super.discard();
    }

    @Override
    public Collection<E> getValue() {

        BeanItemContainer<E> container = getContainer();
        if (container == null) {
            return null;
        }

        // return all entries from the container
        Collection<E> entries = container.getItemIds();

        return entries;
    }

    @Override
    public boolean isEmpty() {
        Collection<E> entries = getValue();
        for (E entry : entries) {
            if (!isEmpty(entry)) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void setValue(Collection newFieldValue, boolean repaintIsNotNeeded, boolean ignoreReadOnly)
            throws com.vaadin.v7.data.Property.ReadOnlyException, ConversionException, InvalidValueException {

        BeanItemContainer<E> container = getContainer();
        if (container == null) {
            return;
        }

        container.removeAllItems();
        container.addAll(newFieldValue);
        table.refreshRowCache();

        fireValueChange(repaintIsNotNeeded);
    }

    /**
     * @since Vaadin 7.4
     *       Workaround, because in AbstractField.clear () calls setValue (null).
     */
    public void clear() {
        BeanItemContainer<E> container = getContainer();
        if (container != null) {
            container.removeAllItems();
        }
    }

    protected VerticalLayout getLayout() {
        return layout;
    }

    protected Table getTable() {
        return table;
    }

    protected Button getAddButton() {
        return addButton;
    }

    protected BeanItemContainer<E> getContainer() {
        return container;
    }

    protected void setLayoutSpacing(boolean value) {
        getLayout().setSpacing(value);
    }

    @Override
    public void setReadOnly(boolean readOnly) {
        super.setReadOnly(readOnly);
        getAddButton().setVisible(!readOnly);
        if (readOnly) {
            getTable().removeContainerProperty(EDIT_COLUMN_ID);
        }
    }

}