package org.vaadin.viritin.fields;
import com.vaadin.data.BeanValidationBinder;
import com.vaadin.data.Binder;
import com.vaadin.data.HasValue;
import com.vaadin.data.StatusChangeEvent;
import com.vaadin.data.StatusChangeListener;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomField;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.util.ReflectTools;
import com.vaadin.v7.ui.DefaultFieldFactory;
import java.util.Optional;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* NOTE, this V8 compatible version of this class should still be considered experimental.
* <p>
* A superclass for fields suitable for editing collection of referenced objects
* tied to parent object only. E.g. OneToMany/ElementCollection fields in JPA
* world.
* <p>
* Some features/restrictions:
* <ul>
* <li>The field is valid when all elements are valid.
* <li>The field is always non buffered
* <li>The element type needs to have an empty paremeter constructor or user
* must provide an Instantiator.
* </ul>
*
* @author Matti Tahvonen
* @param <CT>
* @param <ET> The type in the entity collection. The type must have empty
* paremeter constructor or you have to provide Instantiator.
*/
public abstract class AbstractElementCollection<ET, CT extends Collection<ET>> extends CustomField<CT> {
private static final long serialVersionUID = 7785110162928180695L;
private static ValidatorFactory factory;
private transient Validator javaxBeanValidator;
public static class ElementAddedEvent<ET> extends Component.Event {
private static final long serialVersionUID = 2263765199849601501L;
private final ET element;
public ElementAddedEvent(AbstractElementCollection source, ET element) {
super(source);
this.element = element;
}
public ET getElement() {
return element;
}
}
public static class ElementRemovedEvent<ET> extends Component.Event {
private static final long serialVersionUID = 574545902966053269L;
private final ET element;
public ElementRemovedEvent(AbstractElementCollection source, ET element) {
super(source);
this.element = element;
}
public ET getElement() {
return element;
}
}
public interface ElementAddedListener<ET> extends Serializable {
void elementAdded(ElementAddedEvent<ET> e);
}
public interface ElementRemovedListener<ET> extends Serializable {
void elementRemoved(ElementRemovedEvent<ET> e);
}
private static final Method addedMethod;
private static final Method removedMethod;
static {
addedMethod = ReflectTools.findMethod(ElementAddedListener.class,
"elementAdded", ElementAddedEvent.class);
removedMethod = ReflectTools.findMethod(ElementRemovedListener.class,
"elementRemoved", ElementRemovedEvent.class);
}
public AbstractElementCollection<ET, CT> addElementAddedListener(
ElementAddedListener<ET> listener) {
addListener(ElementAddedEvent.class, listener, addedMethod);
return this;
}
public AbstractElementCollection<ET, CT> removeElementAddedListener(
ElementAddedListener listener) {
removeListener(ElementAddedEvent.class, listener, addedMethod);
return this;
}
public AbstractElementCollection<ET, CT> addElementRemovedListener(
ElementRemovedListener<ET> listener) {
addListener(ElementRemovedEvent.class, listener, removedMethod);
return this;
}
public AbstractElementCollection<ET, CT> removeElementRemovedListener(
ElementRemovedListener listener) {
removeListener(ElementRemovedEvent.class, listener, removedMethod);
return this;
}
private Instantiator<ET> instantiator;
private Instantiator<?> oldEditorInstantiator;
private EditorInstantiator<?, ET> newEditorInstantiator;
private final Class<ET> elementType;
private final Class<?> editorType;
protected ET newInstance;
private CT value;
StatusChangeListener scl = new StatusChangeListener() {
@Override
public void statusChange(StatusChangeEvent event) {
ET bean = (ET) event.getBinder().getBean();
if (bean == newInstance) {
// God dammit, you can't rely on BeanValidationBinder.isValid !
// Returns true here even if some notnull fields are not set :-(
// Thus, check also with own BeanValidation logi, this should
// also give a bare bones cross field validation support for
// elements
final Binder<?> binder = event.getBinder();
final boolean valid = binder.isValid();
if (!valid || !isValidBean(bean)) {
return;
}
getAndEnsureValue().add(newInstance);
fireEvent(new ElementAddedEvent(AbstractElementCollection.this,
newInstance));
setPersisted(newInstance, true);
onElementAdded();
}
// TODO
fireValueChange();
}
private void fireValueChange() {
// TODO FFS, old value with mutable object, eh
// TODO FFS, how to detect
fireEvent(new ValueChangeEvent(AbstractElementCollection.this, null, true));
}
};
private List<String> visibleProperties;
private boolean allowNewItems = true;
private boolean allowRemovingItems = true;
private boolean allowEditItems = true;
public boolean isAllowNewItems() {
return allowNewItems;
}
public boolean isAllowRemovingItems() {
return allowRemovingItems;
}
public boolean isAllowEditItems() {
return allowEditItems;
}
public AbstractElementCollection<ET, CT> setAllowEditItems(boolean allowEditItems) {
this.allowEditItems = allowEditItems;
return this;
}
public AbstractElementCollection<ET, CT> setAllowRemovingItems(boolean allowRemovingItems) {
this.allowRemovingItems = allowRemovingItems;
return this;
}
public AbstractElementCollection<ET, CT> withCaption(String caption) {
setCaption(caption);
return this;
}
// TODO figure out if replacement for this is needed?
// @Override
// public void validate() throws Validator.InvalidValueException {
// super.validate();
// Collection v = getValue();
// if(v != null) {
// for (Object o : v) {
// for (Field f : getFieldGroupFor((ET) o).getFields()) {
// f.validate();
// }
// }
// }
// }
private Collection<ET> getAndEnsureValue() {
CT value = getValue();
if (value == null) {
// TODO Can't be done anymore !? No?
//Class fieldType = getPropertyDataSource().getType();
Class fieldType = List.class;
if (fieldType.isInterface()) {
if (fieldType == List.class) {
value = (CT) new ArrayList<ET>();
} else { // Set
value = (CT) new HashSet<ET>();
}
} else {
try {
value = (CT) fieldType.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
throw new RuntimeException(
"Could not instantiate the used colleciton type", ex);
}
}
setValue(value);
}
return value;
}
/**
* @param allowNewItems true if a new element row should be automatically
* added
* @return the configured field instance
*/
public AbstractElementCollection<ET, CT> setAllowNewElements(
boolean allowNewItems) {
this.allowNewItems = allowNewItems;
return this;
}
public interface Instantiator<ET> extends Serializable {
ET create();
}
public interface EditorInstantiator<T, ET> extends Serializable {
T create(ET entity);
}
public AbstractElementCollection(Class<ET> elementType,
Class<?> formType) {
this.elementType = elementType;
this.editorType = formType;
}
public AbstractElementCollection(Class<ET> elementType, Instantiator i,
Class<?> formType) {
this.elementType = elementType;
this.instantiator = i;
this.editorType = formType;
}
public Class<ET> getElementType() {
return elementType;
}
protected ET createInstance() {
if (instantiator != null) {
return instantiator.create();
} else {
try {
return elementType.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
throw new RuntimeException(ex);
}
}
}
protected Object createEditorInstance(ET pojo) {
if (newEditorInstantiator != null) {
return newEditorInstantiator.create(pojo);
} else {
if (oldEditorInstantiator != null) {
return oldEditorInstantiator.create();
} else {
try {
return editorType.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
throw new RuntimeException(ex);
}
}
}
}
public EditorInstantiator<?, ET> getNewEditorInstantiator() {
return newEditorInstantiator;
}
public void setNewEditorInstantiator(
EditorInstantiator<?, ET> editorInstantiator) {
this.newEditorInstantiator = editorInstantiator;
}
public Instantiator<?> getEditorInstantiator() {
return oldEditorInstantiator;
}
public void setEditorInstantiator(
Instantiator<?> editorInstantiator) {
this.oldEditorInstantiator = editorInstantiator;
}
private class EditorStuff implements Serializable {
private static final long serialVersionUID = 5132645136059482705L;
Binder<ET> bfg;
Object editor;
private EditorStuff(Binder<ET> editor, Object o) {
this.bfg = editor;
this.editor = o;
}
}
private final Map<ET, EditorStuff> pojoToEditor = new IdentityHashMap<>();
protected final Binder<ET> getFieldGroupFor(ET pojo) {
EditorStuff es = pojoToEditor.get(pojo);
if (es == null) {
Object o = createEditorInstance(pojo);
Binder<ET> binder = new BeanValidationBinder<>(elementType);
binder.bindInstanceFields(o);
binder.setBean(pojo);
binder.addStatusChangeListener(scl);
es = new EditorStuff(binder, o);
// TODO listen for all changes for proper modified/validity changes
pojoToEditor.put(pojo, es);
}
return es.bfg;
}
protected final Component getComponentFor(ET pojo, String property) {
EditorStuff editorsstuff = pojoToEditor.get(pojo);
if (editorsstuff == null) {
Object o = createEditorInstance(pojo);
Binder<ET> binder = new BeanValidationBinder<>(elementType);
binder.bindInstanceFields(o);
binder.addStatusChangeListener(scl);
editorsstuff = new EditorStuff(binder, o);
// TODO listen for all changes for proper modified/validity changes
pojoToEditor.put(pojo, editorsstuff);
}
Component c = null;
Optional<Binder.Binding<ET, ?>> binding = editorsstuff.bfg.getBinding(property);
if (binding.isPresent()) {
c = (Component) binding.get().getField();
} else {
try {
// property that is not a property editor field but custom UI "column"
java.lang.reflect.Field f = editorType.getDeclaredField(property);
f.setAccessible(true);
c = (Component) f.get(editorsstuff.editor);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
Logger.getLogger(AbstractElementCollection.class.getName()).
log(Level.SEVERE, null, ex);
}
if (c == null) {
c = new Label("");
}
}
return c;
}
public void addElement(ET instance) {
getAndEnsureValue().add(instance);
addInternalElement(instance);
fireEvent(new ValueChangeEvent(this, null, true));
fireEvent(new ElementAddedEvent<>(this, instance));
}
public void removeElement(ET elemnentToBeRemoved) {
removeInternalElement(elemnentToBeRemoved);
getAndEnsureValue().remove(elemnentToBeRemoved);
fireEvent(new ValueChangeEvent(this, null, true));
fireEvent(new ElementRemovedEvent<>(this, elemnentToBeRemoved));
}
public AbstractElementCollection<ET, CT> setVisibleProperties(
List<String> properties) {
visibleProperties = properties;
return this;
}
public List<String> getVisibleProperties() {
if (visibleProperties == null) {
visibleProperties = new ArrayList<>();
for (java.lang.reflect.Field f : editorType.getDeclaredFields()) {
// field order can be counted since jdk6
if (HasValue.class.isAssignableFrom(f.getType())) {
visibleProperties.add(f.getName());
}
}
}
return visibleProperties;
}
public AbstractElementCollection<ET, CT> setVisibleProperties(
List<String> properties, List<String> propertyHeaders) {
visibleProperties = properties;
Iterator<String> it = propertyHeaders.iterator();
for (String prop : visibleProperties) {
setPropertyHeader(prop, it.next());
}
return this;
}
private final Map<String, String> propertyToHeader = new HashMap<>();
public AbstractElementCollection<ET, CT> setPropertyHeader(String propertyName,
String propertyHeader) {
propertyToHeader.put(propertyName, propertyHeader);
return this;
}
protected String getPropertyHeader(String propertyName) {
String header = propertyToHeader.get(propertyName);
if (header == null) {
// TODO figure out a way to do without deprecated class
header = DefaultFieldFactory.createCaptionByPropertyId(propertyName);
}
return header;
}
@Override
protected Component initContent() {
return getLayout();
}
@Override
protected void doSetValue(CT newValue) {
clear();
value = newValue;
if (value != null) {
for (Object v : value) {
addInternalElement((ET) v);
}
}
onElementAdded();
}
abstract protected void addInternalElement(ET v);
abstract protected void setPersisted(ET v, boolean persisted);
abstract protected void removeInternalElement(ET v);
abstract protected Layout getLayout();
abstract protected void onElementAdded();
@Override
public CT getValue() {
return value;
}
public boolean isValidBean(Object bean) {
Set<ConstraintViolation<Object>> violations = getJavaxBeanValidator().validate(bean);
return violations.isEmpty();
}
/**
* Returns the underlying JSR-303 bean validator factory used. A factory is
* created using {@link Validation} if necessary.
*
* @return {@link ValidatorFactory} to use
*/
protected static ValidatorFactory getJavaxBeanValidatorFactory() {
if (factory == null) {
factory = Validation.buildDefaultValidatorFactory();
}
return factory;
}
/**
* Returns a shared Validator instance to use. An instance is created using
* the validator factory if necessary and thereafter reused by the
* {@link BeanValidator} instance.
*
* @return the JSR-303 {@link javax.validation.Validator} to use
*/
protected Validator getJavaxBeanValidator() {
if (javaxBeanValidator == null) {
javaxBeanValidator = getJavaxBeanValidatorFactory().getValidator();
}
return javaxBeanValidator;
}
}