org.jsimpledb.vaadin.JObjectContainer Maven / Gradle / Ivy
Show all versions of jsimpledb-vaadin Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.vaadin;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SortedMap;
import org.dellroad.stuff.vaadin7.PropertyDef;
import org.dellroad.stuff.vaadin7.PropertyExtractor;
import org.dellroad.stuff.vaadin7.ProvidesPropertyScanner;
import org.dellroad.stuff.vaadin7.SimpleItem;
import org.dellroad.stuff.vaadin7.SimpleKeyedContainer;
import org.jsimpledb.CopyState;
import org.jsimpledb.JCollectionField;
import org.jsimpledb.JCounterField;
import org.jsimpledb.JField;
import org.jsimpledb.JFieldSwitchAdapter;
import org.jsimpledb.JMapField;
import org.jsimpledb.JObject;
import org.jsimpledb.JSimpleDB;
import org.jsimpledb.JSimpleField;
import org.jsimpledb.JTransaction;
import org.jsimpledb.ValidationMode;
import org.jsimpledb.core.DeletedObjectException;
import org.jsimpledb.core.ObjId;
import org.jsimpledb.core.UnknownFieldException;
import org.jsimpledb.core.util.ObjIdSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Vaadin {@link com.vaadin.data.Container} backed by {@link JSimpleDB} Java model objects.
*
*
* Automatically creates container properties for object ID, database type, schema version, and all fields, as well as any custom
* properties defined by {@link org.dellroad.stuff.vaadin7.ProvidesProperty @ProvidesProperty}-annotated methods in
* Java model classes. The properties of each {@link com.vaadin.data.Item} in the container are derived from a corresponding
* {@link JObject} which is usually stored in an in-memory {@link org.jsimpledb.SnapshotJTransaction} (which may contain other
* related objects, allowing an {@link com.vaadin.data.Item}'s properties to be derived from those related objects as well).
*
*
* Instances are configured with a type, which can be any Java type. The container will then be restricted to
* database objects that are instances of the configured type. The type may be null, in which case there is no restriction.
*
*
* Instances are loaded by invoking {@link #load load()} with an iteration of backing {@link JObject}s.
* Normally these {@link JObject}s are contained in a {@link org.jsimpledb.SnapshotJTransaction}.
*
*
* Instances implement {@link org.dellroad.stuff.vaadin7.Connectable} and therefore must be {@link #connect connect()}'ed
* prior to use and {@link #disconnect disconnect()}'ed after use (usually done in the associated widget's
* {@link com.vaadin.ui.Component#attach attach()} and {@link com.vaadin.ui.Component#detach detach()} methods).
*
*
* Container Properties
*
*
* Instances have the following container properties:
*
* - {@link #OBJECT_ID_PROPERTY}: Object {@link ObjId}
* - {@link #TYPE_PROPERTY}: Object type name (JSimpleDB type name, not Java type name, though the former
* is by default the simple Java type name)
* - {@link #VERSION_PROPERTY}: Object schema version
* - {@link #REFERENCE_LABEL_PROPERTY}: Object reference label, which is a short description identifying the
* object. Reference labels are used to provide "names" for objects that are more meaningful than object ID's
* and are used as such in other {@link JSimpleDB} GUI classes, for example when displaying the object in a list.
* To customize the reference label for a Java model class,
* annotate a method with {@link org.dellroad.stuff.vaadin7.ProvidesProperty @ProvidesProperty}{@code (}{@link
* JObjectContainer#REFERENCE_LABEL_PROPERTY REFERENCE_LABEL_PROPERTY}{@code )};
* otherwise, the value of this property will be the same as {@link #OBJECT_ID_PROPERTY}. Note that objects with
* customized reference labels will need to be included in the snapshot transaction also if they are referenced
* by an object actually in the container.
*
* - A property for every {@link JSimpleDB} field that is common to all object types that sub-type
* this containers's configured type. The property's ID is the field name; its value is as follows:
*
* - For simple fields, their {@linkplain org.jsimpledb.core.FieldType#toString(Object) string form}
* - For reference fields, the {@link #REFERENCE_LABEL_PROPERTY} of the referred-to object, or "Null"
* if the reference is null
* - For set, list, and map fields, the first few entries in the collection
*
*
* - A property for each {@link org.dellroad.stuff.vaadin7.ProvidesProperty @ProvidesProperty}-annotated method
* in the specified type. These properties will add to (or override) the properties listed above.
*
*/
@SuppressWarnings("serial")
public class JObjectContainer extends SimpleKeyedContainer {
/**
* Container property name for the reference label property, which has type {@link Component}.
*/
public static final String REFERENCE_LABEL_PROPERTY = "$label";
/**
* Container property name for the object ID property.
*/
public static final String OBJECT_ID_PROPERTY = "$objId";
/**
* Container property name for the object type property.
*/
public static final String TYPE_PROPERTY = "$type";
/**
* Container property name for the object schema version property.
*/
public static final String VERSION_PROPERTY = "$version";
protected final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* The associated {@link JSimpleDB}.
*/
protected final JSimpleDB jdb;
private final ObjIdPropertyDef objIdPropertyDef = new ObjIdPropertyDef();
private final ObjTypePropertyDef objTypePropertyDef = new ObjTypePropertyDef();
private final ObjVersionPropertyDef objVersionPropertyDef = new ObjVersionPropertyDef();
private final RefLabelPropertyDef refLabelPropertyDef = new RefLabelPropertyDef();
private Class> type;
private ProvidesPropertyScanner> propertyScanner;
private List orderedPropertyNames;
/**
* Constructor.
*
* @param jdb {@link JSimpleDB} database
* @param type type restriction, or null for no restriction
* @throws IllegalArgumentException if {@code jdb} is null
*/
protected JObjectContainer(JSimpleDB jdb, Class> type) {
Preconditions.checkArgument(jdb != null, "null jdb");
this.jdb = jdb;
this.setType(type);
this.setPropertyExtractor(this);
}
/**
* Get the type restriction associated with this instance.
*
* @return Java type restriction, or null if there is none
*/
public Class> getType() {
return this.type;
}
/**
* Change the type restriction associated with this instance.
* Triggers a {@link com.vaadin.data.Container.PropertySetChangeEvent} and typically requires a reload.
*
* @param type Java type restriction, or null for none
* @param Java type
*/
public void setType(Class type) {
this.type = type;
this.propertyScanner = this.type != null ? new ProvidesPropertyScanner(/*this.*/type) : null;
final ArrayList> propertyDefs = new ArrayList<>(this.buildPropertyDefs());
this.orderedPropertyNames = Collections.unmodifiableList(Lists.transform(propertyDefs, PropertyDef::getName));
this.setProperties(propertyDefs);
this.fireContainerPropertySetChange();
}
/**
* Get the properties of this container in preferred order.
*
* @return property names
*/
public List getOrderedPropertyNames() {
return this.orderedPropertyNames;
}
@Override
public ObjId getKeyFor(JObject jobj) {
return jobj.getObjId();
}
/**
* Load this container using the supplied backing {@link JObject}s.
*
*
* A container {@link com.vaadin.data.Item} will be created wrapping each iterated {@link JObject};
* {@link com.vaadin.data.Item} properties are accessible only while the containing transaction remains open.
*
* @param jobjs backing {@link JObject}s
*/
@Override
public void load(Iterable extends JObject> jobjs) {
this.load(jobjs.iterator());
}
/**
* Load this container using the supplied backing {@link JObject}s.
*
*
* A container {@link com.vaadin.data.Item} will be created wrapping each iterated {@link JObject};
* {@link com.vaadin.data.Item} properties are accessible only while the containing transaction remains open.
*
* @param jobjs backing {@link JObject}s
*/
@Override
public void load(Iterator extends JObject> jobjs) {
// Filter out any instances of the wrong type
if (this.type != null)
jobjs = Iterators.filter(jobjs, this.type::isInstance);
// Filter out nulls and duplicates
final ObjIdSet seenIds = new ObjIdSet();
jobjs = Iterators.filter(jobjs, jobj -> jobj != null && seenIds.add(jobj.getObjId()));
// Proceed
super.load(jobjs);
}
/**
* Update a single item in this container by updating its backing object.
*
*
* This updates the backing object with the same object ID as {@code jobj}, if any, and then fires
* {@link com.vaadin.data.Property.ValueChangeEvent}s for all properties of the corresponding
* {@link com.vaadin.data.Item}.
*
* @param jobj updated database object
*/
public void updateItem(JObject jobj) {
Preconditions.checkArgument(jobj != null, "null jobj");
final SimpleItem item = (SimpleItem)this.getItem(jobj.getObjId());
if (item != null) {
jobj.copyTo(item.getObject().getTransaction(), new CopyState());
item.fireValueChange();
}
}
/**
* Perform the given action within a new {@link JTransaction}.
*
*
* The implementation in {@link JObjectContainer} performs {@code action} within a new read-only transaction.
* Note that {@code action} should be idempotent because the transaction will be retried if needed.
*
* @param action the action to perform
*/
protected void doInTransaction(Runnable action) {
final JTransaction jtx = this.jdb.createTransaction(false, ValidationMode.DISABLED);
jtx.getTransaction().setReadOnly(true);
try {
jtx.performAction(action);
} finally {
jtx.commit();
}
}
// Property derivation
private Collection> buildPropertyDefs() {
final PropertyDefHolder pdefs = new PropertyDefHolder();
// Add properties shared by all JObjects
pdefs.setPropertyDef(this.refLabelPropertyDef);
pdefs.setPropertyDef(this.objIdPropertyDef);
pdefs.setPropertyDef(this.objTypePropertyDef);
pdefs.setPropertyDef(this.objVersionPropertyDef);
// Add properties for all fields common to all sub-types of our configured type
final SortedMap jfields = Util.getCommonJFields(this.jdb.getJClasses(this.type));
if (jfields != null) {
for (JField jfield : jfields.values())
pdefs.setPropertyDef(new ObjFieldPropertyDef(jfield.getStorageId(), jfield.getName()));
}
// Apply any @ProvidesProperty-annotated method properties, possibly overridding jfields
if (this.propertyScanner != null) {
for (PropertyDef> propertyDef : this.propertyScanner.getPropertyDefs())
pdefs.setPropertyDef(propertyDef);
}
// Done
return pdefs.values();
}
// PropertyExtractor
@Override
@SuppressWarnings("unchecked")
public V getPropertyValue(JObject jobj, PropertyDef propertyDef) {
if (propertyDef instanceof ObjPropertyDef)
return (V)((ObjPropertyDef>)propertyDef).extract(jobj);
if (this.propertyScanner == null)
throw new IllegalArgumentException("unknown property: " + propertyDef.getName());
return JObjectContainer.extractProperty(this.propertyScanner.getPropertyExtractor(), propertyDef, jobj);
}
@SuppressWarnings("unchecked")
private static V extractProperty(PropertyExtractor> propertyExtractor, PropertyDef propertyDef, JObject jobj) {
try {
return ((PropertyExtractor)propertyExtractor).getPropertyValue(jobj, propertyDef);
} catch (DeletedObjectException e) {
try {
return propertyDef.getType().cast(new SizedLabel("Unavailable", ContentMode.HTML));
} catch (ClassCastException e2) {
try {
return propertyDef.getType().cast("(Unavailable)");
} catch (ClassCastException e3) {
return null;
}
}
}
}
// ObjPropertyDef
/**
* Support superclass for {@link PropertyDef} implementations that derive the property value from a {@link JObject}.
*/
public abstract static class ObjPropertyDef extends PropertyDef {
protected ObjPropertyDef(String name, Class type) {
super(name, type);
}
public abstract T extract(JObject jobj);
}
// ObjIdPropertyDef
/**
* Implements the {@link JObjectContainer#OBJECT_ID_PROPERTY} property.
*/
public static class ObjIdPropertyDef extends ObjPropertyDef {
public ObjIdPropertyDef() {
super(OBJECT_ID_PROPERTY, SizedLabel.class);
}
@Override
public SizedLabel extract(JObject jobj) {
return new SizedLabel("" + jobj.getObjId() + "
", ContentMode.HTML);
}
}
// ObjTypePropertyDef
/**
* Implements the {@link JObjectContainer#TYPE_PROPERTY} property.
*/
public static class ObjTypePropertyDef extends ObjPropertyDef {
public ObjTypePropertyDef() {
super(TYPE_PROPERTY, SizedLabel.class);
}
@Override
public SizedLabel extract(JObject jobj) {
return new SizedLabel(jobj.getTransaction().getTransaction().getSchemas()
.getVersion(jobj.getSchemaVersion()).getObjType(jobj.getObjId().getStorageId()).getName());
}
}
// ObjVersionPropertyDef
/**
* Implements the {@link JObjectContainer#VERSION_PROPERTY} property.
*/
public static class ObjVersionPropertyDef extends ObjPropertyDef {
public ObjVersionPropertyDef() {
super(VERSION_PROPERTY, SizedLabel.class);
}
@Override
public SizedLabel extract(JObject jobj) {
return new SizedLabel("" + jobj.getSchemaVersion());
}
}
// RefLabelPropertyDef
/**
* Implements the {@link JObjectContainer#REFERENCE_LABEL_PROPERTY} property.
*/
public static class RefLabelPropertyDef extends ObjPropertyDef {
public RefLabelPropertyDef() {
super(REFERENCE_LABEL_PROPERTY, Component.class);
}
@Override
public Component extract(JObject jobj) {
final ReferenceMethodInfoCache.PropertyInfo> propertyInfo
= ReferenceMethodInfoCache.getInstance().getReferenceMethodInfo(jobj.getClass());
if (propertyInfo == ReferenceMethodInfoCache.NOT_FOUND)
return new ObjIdPropertyDef().extract(jobj);
final Object value = JObjectContainer.extractProperty(
propertyInfo.getPropertyExtractor(), propertyInfo.getPropertyDef(), jobj);
if (value instanceof Component)
return (Component)value;
return new SizedLabel(String.valueOf(value));
}
}
// ObjFieldPropertyDef
/**
* Implements a property reflecting the value of a {@link JSimpleDB} field.
*/
public class ObjFieldPropertyDef extends ObjPropertyDef {
private static final int MAX_ITEMS = 3;
private final int storageId;
public ObjFieldPropertyDef(int storageId, String name) {
super(name, Component.class);
this.storageId = storageId;
}
@Override
public Component extract(final JObject jobj) {
final JField jfield = JObjectContainer.this.jdb.getJClass(jobj.getObjId()).getJField(this.storageId, JField.class);
try {
return jfield.visit(new JFieldSwitchAdapter() {
@Override
public Component caseJSimpleField(JSimpleField field) {
return ObjFieldPropertyDef.this.handleValue(field.getValue(jobj));
}
@Override
public Component caseJCounterField(JCounterField field) {
return ObjFieldPropertyDef.this.handleValue(field.getValue(jobj).get());
}
@Override
protected Component caseJCollectionField(JCollectionField field) {
return ObjFieldPropertyDef.this.handleCollectionField(field.getValue(jobj));
}
@Override
public Component caseJMapField(JMapField field) {
return ObjFieldPropertyDef.this.handleMultiple(Iterables.transform(
field.getValue(jobj).entrySet(),
entry -> {
final HorizontalLayout layout = new HorizontalLayout();
layout.setMargin(false);
layout.setSpacing(false);
layout.addComponent(ObjFieldPropertyDef.this.handleValue(entry.getKey()));
layout.addComponent(new SizedLabel(" \u21d2 ")); // RIGHTWARDS DOUBLE ARROW
layout.addComponent(ObjFieldPropertyDef.this.handleValue(entry.getValue()));
return layout;
}));
}
});
} catch (UnknownFieldException e) {
return new SizedLabel("NA", ContentMode.HTML);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!super.equals(obj))
return false;
final ObjFieldPropertyDef that = (ObjFieldPropertyDef)obj;
return this.storageId == that.storageId;
}
@Override
public int hashCode() {
return super.hashCode() ^ this.storageId;
}
private Component handleCollectionField(Collection> col) {
return this.handleMultiple(Iterables.transform(col, this::handleValue));
}
private Component handleMultiple(Iterable components) {
final HorizontalLayout layout = new HorizontalLayout();
layout.setMargin(false);
layout.setSpacing(false);
int count = 0;
for (Component component : components) {
if (count >= MAX_ITEMS) {
layout.addComponent(new SizedLabel("..."));
break;
}
if (count > 0)
layout.addComponent(new SizedLabel(", ", ContentMode.HTML));
layout.addComponent(component);
count++;
}
return layout;
}
@SuppressWarnings("unchecked")
private Component handleValue(Object value) {
if (value == null)
return new SizedLabel("Null", ContentMode.HTML);
if (value instanceof JObject)
return new RefLabelPropertyDef().extract((JObject)value);
return new SizedLabel(String.valueOf(value));
}
}
// PropertyDefHolder
private static class PropertyDefHolder extends LinkedHashMap> {
public void setPropertyDef(PropertyDef> propertyDef) {
this.put(propertyDef.getName(), propertyDef);
}
}
}