org.openvpms.web.component.im.edit.IMTableCollectionEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.openvpms.web.component.im.edit.IMTableCollectionEditor.java

Source

/*
 * Version: 1.0
 *
 * The contents of this file are subject to the OpenVPMS License Version
 * 1.0 (the 'License'); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.openvpms.org/license/
 *
 * Software distributed under the License is distributed on an 'AS IS' basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Copyright 2015 (C) OpenVPMS Ltd. All Rights Reserved.
 */

package org.openvpms.web.component.im.edit;

import echopointng.GroupBox;
import nextapp.echo2.app.Column;
import nextapp.echo2.app.Component;
import nextapp.echo2.app.Insets;
import nextapp.echo2.app.Row;
import nextapp.echo2.app.SelectField;
import nextapp.echo2.app.event.ActionEvent;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openvpms.component.business.domain.im.common.IMObject;
import org.openvpms.component.business.service.archetype.helper.DescriptorHelper;
import org.openvpms.web.component.im.filter.FilterHelper;
import org.openvpms.web.component.im.filter.NamedNodeFilter;
import org.openvpms.web.component.im.filter.NodeFilter;
import org.openvpms.web.component.im.layout.LayoutContext;
import org.openvpms.web.component.im.layout.LayoutHelper;
import org.openvpms.web.component.im.list.ShortNameListCellRenderer;
import org.openvpms.web.component.im.list.ShortNameListModel;
import org.openvpms.web.component.im.query.ResultSet;
import org.openvpms.web.component.im.table.IMTableModel;
import org.openvpms.web.component.im.table.PagedIMTable;
import org.openvpms.web.component.im.util.IMObjectCreationListener;
import org.openvpms.web.component.im.util.IMObjectCreator;
import org.openvpms.web.component.im.view.DefaultIMObjectComponent;
import org.openvpms.web.component.im.view.IMObjectComponent;
import org.openvpms.web.component.im.view.Selection;
import org.openvpms.web.component.property.CollectionProperty;
import org.openvpms.web.component.property.DefaultValidator;
import org.openvpms.web.component.property.Modifiable;
import org.openvpms.web.component.property.ModifiableListener;
import org.openvpms.web.echo.button.ButtonRow;
import org.openvpms.web.echo.dialog.ConfirmationDialog;
import org.openvpms.web.echo.dialog.PopupDialogListener;
import org.openvpms.web.echo.event.ActionListener;
import org.openvpms.web.echo.factory.ColumnFactory;
import org.openvpms.web.echo.factory.GroupBoxFactory;
import org.openvpms.web.echo.factory.SelectFieldFactory;
import org.openvpms.web.echo.focus.FocusGroup;
import org.openvpms.web.echo.keyboard.KeyStrokeHelper;
import org.openvpms.web.echo.style.Styles;
import org.openvpms.web.echo.table.SortableTableModel;
import org.openvpms.web.echo.table.TableNavigator;
import org.openvpms.web.resource.i18n.Messages;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * Editor for a collection of {@link IMObject}s. The collection is displayed
 * in a table. When an item is selected, an editor containing it is displayed
 * in a box beneath the table.
 *
 * @author Tim Anderson
 */
public abstract class IMTableCollectionEditor<T> extends AbstractEditableIMObjectCollectionEditor {

    /**
     * The container.
     */
    private Column container;

    /**
     * Collection to edit.
     */
    private PagedIMTable<T> table;

    /**
     * The archetype short name used to create a new object.
     */
    private String shortName;

    /**
     * The edit group box.
     */
    private GroupBox editBox;

    /**
     * The focus group.
     */
    private FocusGroup focusGroup;

    /**
     * The current editor's focus group.
     */
    private FocusGroup editorFocusGroup;

    /**
     * Listener for component change events.
     */
    private final PropertyChangeListener componentListener;

    /**
     * The listener for editor events.
     */
    private final ModifiableListener editorListener;

    /**
     * Determines if the current editor has been modified since being displayed.
     */
    private boolean editorModified;

    /**
     * The no. of rows to display.
     */
    protected static final int ROWS = 15;

    /**
     * The logger.
     */
    private static final Log log = LogFactory.getLog(IMTableCollectionEditor.class);

    /**
     * The 'add' button identifier.
     */
    private static final String ADD_ID = "add";

    /**
     * The 'delete' button identifier.
     */
    private static final String DELETE_ID = "delete";

    /**
     * The 'previous' button identifier.
     */
    private static final String PREVIOUS_ID = "previous";

    /**
     * The 'next' button identifier.
     */
    private static final String NEXT_ID = "next";

    /**
     * The buttons.
     */
    private ButtonRow buttons;

    /**
     * Constructs an {@link IMTableCollectionEditor}.
     *
     * @param editor  the editor
     * @param object  the parent object
     * @param context the layout context
     */
    public IMTableCollectionEditor(CollectionPropertyEditor editor, IMObject object, LayoutContext context) {
        super(editor, object, context);

        context = getContext();

        // filter out the "id" field
        NodeFilter idFilter = new NamedNodeFilter("id");
        NodeFilter filter = FilterHelper.chain(idFilter, context.getDefaultNodeFilter());
        context.setNodeFilter(filter);

        componentListener = new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent event) {
                onComponentChange(event);
            }
        };

        editorListener = new ModifiableListener() {
            public void modified(Modifiable modifiable) {
                onCurrentEditorModified();
            }
        };
    }

    /**
     * Creates a new object, subject to a short name being selected, and
     * current collection cardinality. This must be registered with the
     * collection.
     * <p/>
     * If an {@link IMObjectCreationListener} is registered, it will be
     * notified on successful creation of an object.
     *
     * @return a new object, or {@code null} if the object can't be created
     */
    public IMObject create() {
        IMObject object = null;
        CollectionPropertyEditor editor = getCollectionPropertyEditor();
        if (shortName != null) {
            int maxSize = editor.getMaxCardinality();
            if (maxSize == -1 || editor.getObjects().size() < maxSize) {
                object = IMObjectCreator.create(shortName);
            }
        }
        IMObjectCreationListener creationListener = getCreationListener();
        if (creationListener != null) {
            creationListener.created(object);
        }
        return object;
    }

    /**
     * Adds a new item to the collection, subject to the constraints of {@link #create()}.
     *
     * @return the editor for the item, or {@code null} a new item cannot be created.
     */
    public IMObjectEditor add() {
        IMObjectEditor editor = null;
        if (addCurrentEdits(new DefaultValidator()) && shortName != null) {
            IMObject object = create();
            if (object != null) {
                editor = edit(object);
                addCurrentEdits(new DefaultValidator()); // add the object to the table if it is valid
            }
        }
        return editor;
    }

    /**
     * Removes an object from the collection.
     *
     * @param object the object to remove
     */
    @Override
    public void remove(IMObject object) {
        // remove the current editor if it matches the object being deleted.
        // This won't generate any events.
        IMObjectEditor editor = getCurrentEditor();
        if (editor != null && editor.getObject() == object) {
            removeCurrentEditor();
        }
        // remove the object from the collection. May generate events
        boolean removed = getCollectionPropertyEditor().remove(object);
        refresh();
        if (!removed) {
            // the object was not committed, so no notification has been
            // generated yet
            getListeners().notifyListeners(getProperty());
        }
        // workaround for OVPMS-629
        KeyStrokeHelper.reregisterKeyStrokeListeners(container);
    }

    /**
     * Refreshes the collection display.
     */
    public void refresh() {
        if (table != null) {
            populateTable();
            enableNavigation(true);
        }
    }

    /**
     * Returns the focus group.
     *
     * @return the focus group, or {@code null} if the editor hasn't been
     *         rendered
     */
    public FocusGroup getFocusGroup() {
        return focusGroup;
    }

    /**
     * Create a new table model.
     *
     * @param context the layout context
     * @return a new table model
     */
    protected abstract IMTableModel<T> createTableModel(LayoutContext context);

    /**
     * Selects an object in the table.
     *
     * @param object the object to select
     */
    protected abstract void setSelected(IMObject object);

    /**
     * Returns the selected object.
     *
     * @return the selected object. May be {@code null}
     */
    protected abstract IMObject getSelected();

    /**
     * Returns the target of a selection.
     * <p/>
     * This is to support situations where a selection path from a viewer uses a related object to that used by the
     * editor.
     * <p/>
     * This implementation returns {@code object}.
     *
     * @param object the selected object
     * @return the selection target
     */
    protected IMObject getSelectionTarget(IMObject object) {
        return object;
    }

    /**
     * Selects the object prior to the selected object, if one is available.
     *
     * @return the prior object. May be {@code null}
     */
    protected abstract IMObject selectPrevious();

    /**
     * Selects the object after the selected object, if one is available.
     *
     * @return the next object. May be {@code null}
     */
    protected abstract IMObject selectNext();

    /**
     * Creates a new result set.
     *
     * @return a new result set
     */
    protected abstract ResultSet<T> createResultSet();

    /**
     * Lays out the component.
     *
     * @param context the layout context
     * @return the component
     */
    protected Component doLayout(LayoutContext context) {
        container = new IMObjectCollectionComponent();
        container.setStyleName(Styles.CELL_SPACING);
        focusGroup = new FocusGroup(ClassUtils.getShortClassName(getClass()));

        table = new PagedIMTable<T>(createTableModel(context));
        table.getTable().addActionListener(new ActionListener() {
            public void onAction(ActionEvent event) {
                onEdit();
            }
        });

        doLayout(container, context);
        return container;
    }

    /**
     * Lays out the component in the specified container.
     *
     * @param container the container
     * @param context   the layout context
     */
    protected void doLayout(Component container, LayoutContext context) {
        if (!isCardinalityReadOnly()) {
            Row row = createControls(focusGroup);
            container.add(row);
        }

        refresh();

        focusGroup.add(table);
        container.add(table);
    }

    /**
     * Creates the row of controls.
     *
     * @param focus the focus group
     * @return the row of controls
     */
    protected Row createControls(FocusGroup focus) {
        String[] range = getCollectionPropertyEditor().getArchetypeRange();
        range = DescriptorHelper.getShortNames(range, false); // expand any wildcards

        buttons = new ButtonRow(focus);

        boolean disableShortcut;

        // Only use button shortcuts for the first level of collections
        // as multiple collections may be displayed on the one form
        disableShortcut = getContext().getLayoutDepth() > 1;

        ActionListener addListener = new ActionListener() {
            public void onAction(ActionEvent event) {
                onNew();
            }
        };
        buttons.addButton(ADD_ID, disableShortcut, addListener);

        ActionListener deleteListener = new ActionListener() {
            public void onAction(ActionEvent event) {
                onDelete();
            }
        };
        buttons.addButton(DELETE_ID, disableShortcut, deleteListener);

        int cardinality = getCollection().getMaxCardinality();
        if (cardinality > 1 || cardinality == -1) {
            ActionListener previous = new ActionListener() {
                public void onAction(ActionEvent event) {
                    onPrevious();
                }
            };
            buttons.addButton(PREVIOUS_ID, disableShortcut, previous);

            ActionListener next = new ActionListener() {
                public void onAction(ActionEvent event) {
                    onNext();
                }
            };
            buttons.addButton(NEXT_ID, disableShortcut, next);
        }
        if (range.length == 1) {
            shortName = range[0];
        } else if (range.length > 1) {
            final ShortNameListModel model = new ShortNameListModel(range, false, false);
            final SelectField archetypeNames = SelectFieldFactory.create(model);
            int index = archetypeNames.getSelectedIndex();
            shortName = model.getShortName(index);

            archetypeNames.addActionListener(new ActionListener() {
                public void onAction(ActionEvent event) {
                    int index = archetypeNames.getSelectedIndex();
                    if (index != -1) {
                        shortName = model.getShortName(index);
                    }
                }
            });
            archetypeNames.setCellRenderer(new ShortNameListCellRenderer());
            buttons.add(archetypeNames);
            focus.add(archetypeNames);
        }
        return buttons;
    }

    /**
     * Adds the object being edited to the collection, if it doesn't exist.
     * <p/>
     * The object will be selected if visible in the table.
     *
     * @param editor the editor
     * @return {@code true} if the object was added, otherwise {@code false}
     */
    @Override
    protected boolean addEdited(IMObjectEditor editor) {
        boolean added = super.addEdited(editor);
        if (added || editorModified || editor != getCurrentEditor()) {
            if (editor == getCurrentEditor()) {
                editorModified = false;
            }
            refresh(); // refresh the table
        }
        IMObject object = editor.getObject();
        setSelected(object);
        return added;
    }

    /**
     * Returns the selected archetype short name.
     */
    protected String getShortName() {
        return shortName;
    }

    /**
     * Invoked when the "New" button is pressed. Creates a new instance of the
     * selected archetype, and displays it in an editor.
     */
    protected void onNew() {
        add();
    }

    /**
     * Invoked when the 'delete' button is pressed.
     * If the selected object has been saved, a confirmation dialog will be
     * displayed, prompting to delete it. If the object hasn't been saved,
     * it will be deleted without prompting.
     */
    protected void onDelete() {
        IMObject object;
        IMObjectEditor editor = getCurrentEditor();
        if (editor != null) {
            object = editor.getObject();
        } else {
            object = getSelected();
        }
        if (object != null) {
            if (object.isNew()) {
                remove(object);
            } else {
                confirmDelete(object);
            }
        }
    }

    /**
     * Edits the selected object.
     */
    protected void onEdit() {
        IMObject object = getSelected();
        if (object != null) {
            if (addCurrentEdits(new DefaultValidator())) {
                // need to add any edits after getting the selected object
                // as this may change the order within the table
                setSelected(object);
                edit(object);
            } else {
                enableNavigation(false);
            }
        }
    }

    /**
     * Edit an object.
     *
     * @param object the object to edit
     * @return the editor
     */
    protected IMObjectEditor edit(final IMObject object) {
        IMObjectEditor editor = getCurrentEditor();
        if (editor != null) {
            editor.removePropertyChangeListener(IMObjectEditor.COMPONENT_CHANGED_PROPERTY, componentListener);
            editBox.removeAll();
        } else {
            editBox = GroupBoxFactory.create();
            editBox.setInsets(new Insets(0));
        }
        container.add(editBox); // add even if present due to bug in GroupBox removal code
        editor = getEditor(object);
        Component component = editor.getComponent();
        if (LayoutHelper.needsInset(component)) {
            component = ColumnFactory.create("Inset", component);
        }
        editBox.add(component);
        editBox.setTitle(editor.getTitle());
        editor.addPropertyChangeListener(IMObjectEditor.COMPONENT_CHANGED_PROPERTY, componentListener);
        changeFocusGroup(editor);
        setCurrentEditor(editor);

        // workaround for OVPMS-629
        KeyStrokeHelper.reregisterKeyStrokeListeners(container);

        enableNavigation(editor.isValid());
        return editor;
    }

    /**
     * Selects the previous object.
     */
    protected void onPrevious() {
        if (addCurrentEdits(new DefaultValidator())) {
            IMObject object = selectPrevious();
            if (object != null) {
                edit(object);
            }
        } else {
            enableNavigation(false);
        }
    }

    /**
     * Selects the next object.
     */
    protected void onNext() {
        if (addCurrentEdits(new DefaultValidator())) {
            IMObject object = selectNext();
            if (object != null) {
                edit(object);
            }
        } else {
            enableNavigation(false);
        }
    }

    /**
     * Populates the table.
     */
    protected void populateTable() {
        ResultSet<T> set = createResultSet();
        table.setResultSet(set);
        IMTableModel<T> model = table.getTable().getModel();
        if (model instanceof SortableTableModel) {
            // if no column is currently sorted, sort on the default (if any)
            SortableTableModel sortable = ((SortableTableModel) model);
            if (sortable.getSortColumn() == -1 && sortable.getDefaultSortColumn() != -1) {
                sortable.sort(sortable.getDefaultSortColumn(), true);
            }
        }
    }

    /**
     * Returns the table.
     *
     * @return the table
     */
    protected PagedIMTable<T> getTable() {
        return table;
    }

    /**
     * Sets the current editor.
     * <p/>
     * This registers a listener so that {@link #onCurrentEditorModified()} is invoked when the editor changes.
     * If there is an existing editor, its listener is removed.
     *
     * @param editor the editor. May be {@code null}
     */
    @Override
    protected void setCurrentEditor(IMObjectEditor editor) {
        IMObjectEditor current = getCurrentEditor();
        if (current != null) {
            current.removeModifiableListener(editorListener);
        }
        if (editor != null) {
            editor.addModifiableListener(editorListener);
        }
        super.setCurrentEditor(editor);
    }

    /**
     * Removes the current editor.
     */
    protected void removeCurrentEditor() {
        IMObjectEditor editor = getCurrentEditor();
        focusGroup.remove(editorFocusGroup);
        editorFocusGroup = null;
        editBox.remove(editor.getComponent());
        container.remove(editBox);
        editor.removePropertyChangeListener(IMObjectEditor.COMPONENT_CHANGED_PROPERTY, componentListener);
        editorModified = false;
        editBox = null;

        super.removeCurrentEditor();
    }

    /**
     * Confirms to delete an object.
     *
     * @param object the object to delete
     */
    private void confirmDelete(final IMObject object) {
        String displayName = DescriptorHelper.getDisplayName(object);
        String title = Messages.format("imobject.collection.delete.title", displayName);
        String message = Messages.format("imobject.collection.delete.message", displayName);
        final ConfirmationDialog dialog = new ConfirmationDialog(title, message);
        dialog.addWindowPaneListener(new PopupDialogListener() {
            @Override
            public void onOK() {
                remove(object);
            }
        });
        dialog.show();
    }

    /**
     * Invoked when the editor changes components.
     *
     * @param event the property change event
     */
    protected void onComponentChange(PropertyChangeEvent event) {
        Component oldValue = (Component) event.getOldValue();
        Component newValue = (Component) event.getNewValue();
        editBox.remove(oldValue);
        editBox.add(newValue);
        changeFocusGroup(getCurrentEditor());
    }

    /**
     * Invoked when the current editor is modified.
     */
    protected void onCurrentEditorModified() {
        // flag this as invalid. This is required for collections when incomplete objects can be mapped in and out
        // of the collection
        resetValid(false);

        editorModified = true;
        IMObjectEditor current = getCurrentEditor();
        boolean valid = current != null && current.isValid();
        enableNavigation(valid);
        getListeners().notifyListeners(this);
    }

    /**
     * Enable/disables the buttons.
     * <p/>
     * Note that the delete button is enabled if {@link #getCurrentEditor()} or {@link #getSelected()} return non-null.
     *
     * @param enable if {@code true} enable buttons (subject to criteria), otherwise disable them
     */
    protected void enableNavigation(boolean enable) {
        enableNavigation(enable, enable);
    }

    /**
     * Enable/disables the buttons.
     * <p/>
     * This allows the Add button to be enabled independently of the other buttons.
     * <p/>
     * Note that the delete button is enabled if {@link #getCurrentEditor()} or {@link #getSelected()} return non-null.
     *
     * @param enable    if {@code true}, enable buttons (subject to criteria), otherwise disable them
     * @param enableAdd if {@code true}, enable the add button (subject to criteria), otherwise disable it
     */
    protected void enableNavigation(boolean enable, boolean enableAdd) {
        if (buttons != null) {
            boolean add = enableAdd;
            boolean delete = getCurrentEditor() != null || getSelected() != null;
            boolean previous = enable;
            boolean next = enable;
            if (enable || add) {
                CollectionProperty property = getCollection();
                int maxSize = property.getMaxCardinality();
                add = (maxSize == -1 || property.size() < maxSize);
                if (enable) {
                    TableNavigator navigator = getTable().getNavigator();
                    previous = navigator.hasPreviousRow();
                    next = navigator.hasNextRow();
                }
            }
            buttons.getButtons().setEnabled(ADD_ID, add);
            buttons.getButtons().setEnabled(DELETE_ID, delete);
            buttons.getButtons().setEnabled(PREVIOUS_ID, previous);
            buttons.getButtons().setEnabled(NEXT_ID, next);
        }
    }

    /**
     * Changes the focus group to that belonging to the specified editor.
     * <p/>
     * The focus is moved to the default focus component for the editor.
     *
     * @param editor the editor
     */
    private void changeFocusGroup(IMObjectEditor editor) {
        int index;
        if (editorFocusGroup == null) {
            index = focusGroup.size();
        } else {
            index = focusGroup.indexOf(editorFocusGroup);
            if (index == -1) {
                log.error("Missing focus group for existing editor");
                index = focusGroup.size();
            } else {
                focusGroup.remove(editorFocusGroup);
            }
        }
        editorFocusGroup = editor.getFocusGroup();
        if (editorFocusGroup != null) {
            focusGroup.add(index, editorFocusGroup);
            editorFocusGroup.setFocus();
        } else {
            log.warn("Editor " + editor.getClass().getName() + " for " + editor.getDisplayName() + " has no focus");
        }
    }

    /**
     * The root editor component. This hooks the collection editor into the component hierarchy.
     */
    private class IMObjectCollectionComponent extends Column implements IMObjectComponent {

        @Override
        public IMObject getObject() {
            return null;
        }

        @Override
        public String getNode() {
            return IMTableCollectionEditor.this.getProperty().getName();
        }

        @Override
        public IMObjectComponent getSelected() {
            IMObject object = IMTableCollectionEditor.this.getSelected();
            return object != null ? new DefaultIMObjectComponent(object, editBox) : null;
        }

        @Override
        public boolean select(Selection selection) {
            boolean result = false;
            IMObject object = getSelectionTarget(selection.getObject());
            setSelected(object);
            if (object != null && ObjectUtils.equals(object, IMTableCollectionEditor.this.getSelected())) {
                onEdit();
                result = true;
            }
            return result;
        }

        @Override
        public Component getComponent() {
            return editBox;
        }
    }

}