All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.vaadin.app.JObjectEditorWindow Maven / Gradle / Ivy


/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.vaadin.app;

import com.google.common.reflect.TypeToken;
import com.vaadin.data.Property;
import com.vaadin.data.fieldgroup.BeanFieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.util.AbstractProperty;
import com.vaadin.data.util.MethodProperty;
import com.vaadin.data.util.ObjectProperty;
import com.vaadin.data.util.PropertysetItem;
import com.vaadin.server.Sizeable;
import com.vaadin.server.VaadinSession;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomField;
import com.vaadin.ui.DefaultFieldFactory;
import com.vaadin.ui.Field;
import com.vaadin.ui.FormLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

import java.util.Collection;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.validation.constraints.NotNull;

import org.dellroad.stuff.spring.RetryTransaction;
import org.dellroad.stuff.vaadin7.EnumComboBox;
import org.dellroad.stuff.vaadin7.FieldBuilder;
import org.dellroad.stuff.vaadin7.VaadinUtil;
import org.jsimpledb.CopyState;
import org.jsimpledb.Counter;
import org.jsimpledb.JClass;
import org.jsimpledb.JCounterField;
import org.jsimpledb.JField;
import org.jsimpledb.JFieldSwitchAdapter;
import org.jsimpledb.JListField;
import org.jsimpledb.JMapField;
import org.jsimpledb.JObject;
import org.jsimpledb.JSetField;
import org.jsimpledb.JSimpleField;
import org.jsimpledb.JTransaction;
import org.jsimpledb.ValidationException;
import org.jsimpledb.core.FieldType;
import org.jsimpledb.core.ObjId;
import org.jsimpledb.core.Transaction;
import org.jsimpledb.parse.ParseSession;
import org.jsimpledb.vaadin.ConfirmWindow;
import org.jsimpledb.vaadin.NullableField;
import org.jsimpledb.vaadin.ReloadableJObjectContainer;
import org.jsimpledb.vaadin.SimpleFieldConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.transaction.annotation.Transactional;

/**
 * GUI window for editing a database object.
 */
@SuppressWarnings("serial")
public class JObjectEditorWindow extends ConfirmWindow {

    protected final Logger log = LoggerFactory.getLogger(this.getClass());

    private final JObject jobj;
    private final JClass jclass;
    private final boolean create;
    private final Component titleComponent;
    private final ParseSession session;
    private final FieldGroup fieldGroup = new FieldGroup();
    private final TreeMap> fieldMap = new TreeMap<>();

    private ReloadableJObjectContainer reloadContainer;

    /**
     * Constructor for creating and editing a new object.
     *
     * @param ui associated {@link UI}
     * @param session parse session for {@link org.jsimpledb.vaadin.JObjectChooser}
     * @param jclass database object type
     */
    public JObjectEditorWindow(UI ui, ParseSession session, JClass jclass) {
        this(ui, session, jclass, null, null);
    }

    /**
     * Constructor for editing an existing object.
     *
     * @param ui associated {@link UI}
     * @param session parse session for {@link org.jsimpledb.vaadin.JObjectChooser}
     * @param jclass object type
     * @param jobj object to edit; should be contained in a snapshot transaction
     * @param titleComponent title for edit panel
     */
    @SuppressWarnings("unchecked")
    public JObjectEditorWindow(UI ui, ParseSession session, JClass jclass, JObject jobj, Component titleComponent) {
        super(ui, (jobj != null ? "Edit " : "New ") + jclass.getName());
        this.setWidth(600, Sizeable.Unit.PIXELS);
        this.setHeight(450, Sizeable.Unit.PIXELS);
        this.jclass = jclass;
        this.jobj = jobj != null ? jobj : this.doCreateForEdit();
        this.create = jobj == null;
        this.session = session;
        this.titleComponent = titleComponent;

        // Create PropertysetItem to hold our editable properties
        final PropertysetItem item = new PropertysetItem();

        // Introspect for any @FieldBuilder.* annotations
        final BeanFieldGroup beanFieldGroup = FieldBuilder.buildFieldGroup(this.jobj);
        for (Object id : beanFieldGroup.getBoundPropertyIds()) {
            final String fieldName = (String)id;
            final Field field = beanFieldGroup.getField(id);
            final Property property = beanFieldGroup.getItemDataSource().getItemProperty(id);
            this.fieldMap.put(fieldName, field);
            item.addItemProperty(fieldName, property);
        }

        // Build fields and components for all remaining database properties
        final SortedMap jfieldMap = jclass.getJFieldsByName();
        for (Map.Entry entry : jfieldMap.entrySet()) {
            final String fieldName = entry.getKey();
            final JField jfield = entry.getValue();

            // If a Field already exists for this database field, just use it, otherwise build one
            Field field = this.fieldMap.get(fieldName);
            if (field == null) {
                field = this.buildFieldField(fieldName, jfield);
                this.fieldMap.put(fieldName, field);
            }

            // Build an appropriate Vaadin Property for the field
            item.addItemProperty(fieldName, this.buildFieldProperty(this.jobj, jfield));

            // Set the field's caption
            field.setCaption(this.buildCaption(jfield.getName(), !(field instanceof CheckBox)));
        }

        // Connect fields to properties via FieldGroup
        this.fieldGroup.setItemDataSource(item);

        // Bind fields into FieldGroup
        for (Map.Entry> entry : this.fieldMap.entrySet())
            this.fieldGroup.bind(entry.getValue(), entry.getKey());
    }

    /**
     * Configure a container to be {@link org.jsimpledb.vaadin.ReloadableJObjectContainer#reload reload()}'ed
     * after any successful edit.
     *
     * @param container container to reload after changes
     */
    public void setReloadContainerAfterCommit(ReloadableJObjectContainer container) {
        this.reloadContainer = container;
    }

    @Override
    protected void addContent(VerticalLayout layout) {
        if (this.titleComponent != null)
            layout.addComponent(this.titleComponent);
        final FormLayout formLayout = new FormLayout();
        for (Field field : this.fieldMap.values())
            formLayout.addComponent(field);
        layout.addComponent(formLayout);
    }

    @Override
    protected boolean execute() {

        // Commit fields in field group
        try {
            this.fieldGroup.commit();
        } catch (FieldGroup.CommitException e) {
            Notification.show("Invalid value(s)", null, Notification.Type.WARNING_MESSAGE);
            throw new RuntimeException(e);
        }

        // Commit transaction, if possible
        try {
            return this.writeBack();
        } catch (UnexpectedRollbackException e) {
            this.log.debug("ignoring UnexpectedRollbackException presumably caused by validation failure");
            return false;
        }
    }

    @RetryTransaction
    @Transactional("jsimpledbGuiTransactionManager")
    private JObject doCreateForEdit() {
        return (JObject)JTransaction.getCurrent().getSnapshotTransaction().create(this.jclass);
    }

    @RetryTransaction
    @Transactional("jsimpledbGuiTransactionManager")
    protected boolean writeBack() {

        // Find/create target object in current transaction
        final JTransaction jtx = JTransaction.getCurrent();
        final ObjId id = this.jobj.getObjId();
        final JObject target = this.create ? (JObject)jtx.create(this.jclass) : jtx.get(id);

        // Verify object still exists when editing
        if (!this.create && !target.exists()) {
            Notification.show("Object " + id + " no longer exists", null, Notification.Type.WARNING_MESSAGE);
            return true;
        }

        // Copy fields
        this.jobj.copyTo(jtx, target.getObjId(), new CopyState());

        // Run validation queue
        try {
            jtx.validate();
        } catch (ValidationException e) {
            Notification.show("Validation failed", e.getMessage(), Notification.Type.ERROR_MESSAGE);
            jtx.getTransaction().setRollbackOnly();
            return false;
        }

        // Broadcast update event after successful commit
        if (this.reloadContainer != null)
            this.reloadContainer.reloadAfterCommit();

        // Show notification after successful commit
        final VaadinSession vaadinSession = VaadinUtil.getCurrentSession();
        jtx.getTransaction().addCallback(new Transaction.CallbackAdapter() {
            @Override
            public void afterCommit() {
                VaadinUtil.invoke(vaadinSession, new Runnable() {
                    @Override
                    public void run() {
                        Notification.show((JObjectEditorWindow.this.create ? "Created" : "Updated") + " object " + id);
                    }
                });
            }
        });
        return true;
    }

// Field Builders

    private Field buildFieldField(String fieldName, JField jfield) {
        return jfield.visit(new JFieldSwitchAdapter>() {
            @Override
            public Field caseJSimpleField(JSimpleField jfield) {
                final boolean allowNull = jfield.getGetter().getAnnotation(NotNull.class) == null
                  && !jfield.getTypeToken().isPrimitive();
                return new SimpleFieldFieldBuilder(JObjectEditorWindow.this.jobj.getTransaction(),
                  jfield, JObjectEditorWindow.this.session, allowNull).buildField();
            }
            @Override
            public Field caseJCounterField(JCounterField jfield) {
                final TextField field = new TextField();
                field.setWidth("100%");
                field.setNullSettingAllowed(false);
                field.setConverter(new SimpleFieldConverter(JObjectEditorWindow.this.jclass.getJSimpleDB()
                  .getDatabase().getFieldTypeRegistry().getFieldType(TypeToken.of(long.class))));
                return field;
            }
            @Override
            public Field caseJSetField(JSetField jfield) {
                //return new SetFieldFieldBuilder(jfield, JObjectEditorWindow.this.session).buildField();
                return new PlaceHolderField(jfield);                                       // TODO
            }
            @Override
            public Field caseJListField(JListField jfield) {
                //return new ListFieldFieldBuilder(jfield, JObjectEditorWindow.this.session).buildField();
                return new PlaceHolderField(jfield);                                       // TODO
            }
            @Override
            public Field caseJMapField(JMapField jfield) {
                //return new MapFieldFieldBuilder(jfield, JObjectEditorWindow.this.session).buildField();
                return new PlaceHolderField(jfield);                                       // TODO
            }
        });
    }

    private Property buildFieldProperty(final JObject jobj, JField jfield) {
        return jfield.visit(new JFieldSwitchAdapter>() {
            @Override
            @SuppressWarnings({ "unchecked", "rawtypes" })
            public Property caseJSimpleField(JSimpleField jfield) {
                return new MethodProperty(jfield.getTypeToken().getRawType(), jobj, jfield.getGetter(), jfield.getSetter());
            }
            @Override
            public Property caseJCounterField(JCounterField jfield) {
                return new CounterProperty(jfield.getValue(jobj));
            }
            @Override
            @SuppressWarnings("rawtypes")
            public Property caseJSetField(JSetField jfield) {
                return new CollectionProperty(jfield.getValue(jobj));
            }
            @Override
            @SuppressWarnings("rawtypes")
            public Property caseJListField(JListField jfield) {
                return new CollectionProperty(jfield.getValue(jobj));
            }
            @Override
            public Property caseJMapField(JMapField jfield) {
                return new ObjectProperty(null, Void.class);                          // TODO
            }
        });
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private  EnumComboBox createEnumComboBox(Class enumType, boolean allowNull) {
        return new EnumComboBox(enumType, allowNull);
    }

    // This method exists solely to bind the generic type parameters
    private  SimpleFieldConverter buildSimpleFieldConverter(FieldType fieldType) {
        return new SimpleFieldConverter(fieldType);
    }

    private String buildCaption(String fieldName, boolean includeColon) {
        return DefaultFieldFactory.createCaptionByPropertyId(fieldName) + (includeColon ? ":" : "");
    }

    // This method exists solely to bind the generic type parameters
    private  NullableField addNullButton(Field field) {
        return new NullableField(field);
    }

// CounterProperty

    private static class CounterProperty extends AbstractProperty {

        private final Counter counter;

        CounterProperty(Counter counter) {
            this.counter = counter;
        }

        @Override
        public Class getType() {
            return Long.class;
        }

        @Override
        public Long getValue() {
            return this.counter.get();
        }

        @Override
        public void setValue(Long value) {
            this.counter.set(value != null ? value : 0);
        }
    }

// CollectionProperty

    @SuppressWarnings("rawtypes")
    private static class CollectionProperty extends AbstractProperty {

        private final Collection collection;

        CollectionProperty(Collection collection) {
            this.collection = collection;
        }

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

        @Override
        public Collection getValue() {
            return this.collection;
        }

        @Override
        @SuppressWarnings("unchecked")
        public void setValue(Collection value) {
            throw new UnsupportedOperationException();
            //this.collection.clear();
            //this.collection.addAll(value);
        }
    }

// PlaceHolderField

    @SuppressWarnings("serial")
    private static class PlaceHolderField extends CustomField {

        private final JField jfield;

        PlaceHolderField(JField jfield) {
            this.jfield = jfield;
        }

    // CustomField

        @Override
        public Class getType() {
            return Object.class;
        }

        @Override
        protected Label initContent() {
            return new Label("TODO: editor for field `" + this.jfield.getName() + "'");
        }
    }
}