package org.vaadin.viritin.v7.fields;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator;
import com.vaadin.server.FontAwesome;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.v7.ui.CustomField;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.v7.ui.TextField;
import com.vaadin.util.ReflectTools;
import org.vaadin.viritin.v7.MBeanFieldGroup;
import org.vaadin.viritin.v7.MBeanFieldGroup.FieldGroupListener;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* A field to edit simple map structures like string to Integer/Double/Float
* maps. The field is still EXPERIMENTAL, so the should be considered less
* stable than other Viritin API.
*
* @author Matti Tahvonen
* @param <K> The type of key in the edited map
* @param <V> The type of value in the edited map
*/
public class MapField<K, V> extends CustomField<Map> {
private static final Method ADDED_METHOD;
private static final Method REMOVED_METHOD;
static {
ADDED_METHOD = ReflectTools.findMethod(ElementAddedListener.class,
"elementAdded", ElementAddedEvent.class);
REMOVED_METHOD = ReflectTools.findMethod(ElementRemovedListener.class,
"elementRemoved", ElementRemovedEvent.class);
}
private GridLayout mainLayout = new GridLayout(3, 1);
private Class<K> keyType;
private Class<V> valueType;
private Class<?> editorType;
protected K newInstance;
private final FieldGroupListener fieldGroupListener = new FieldGroupListener() {
private static final long serialVersionUID = 1741634663680831911L;
@Override
public void onFieldGroupChange(MBeanFieldGroup beanFieldGroup) {
if (beanFieldGroup.getItemDataSource().getBean() == newInstance) {
if (!getFieldGroupFor(newInstance).valueEditor.isValid()) {
return;
}
getAndEnsureValue().put(newInstance, null);
fireEvent(new ElementAddedEvent(MapField.this,
newInstance));
setPersisted(newInstance, true);
onElementAdded();
}
// TODO could optimize for only repainting on changed validity
fireValueChange(false);
}
};
private boolean allowNewItems = true;
private boolean allowRemovingItems = true;
private boolean allowEditItems = true;
private final Map<K, EntryEditor> pojoToEditor = new HashMap<>();
private EntryEditor newEntryEditor;
public MapField() {
}
public MapField(Class<K> elementType,
Class<?> formType) {
this.keyType = elementType;
this.editorType = formType;
}
private void ensureInited() {
if (mainLayout.getComponentCount() == 0) {
mainLayout.addComponent(new Label("Key ->"));
mainLayout.addComponent(new Label("Value"));
mainLayout.addComponent(new Label("Delete entry"));
}
}
@Override
public Class<? extends Map> getType() {
return Map.class;
}
public MapField<K, V> addElementAddedListener(
ElementAddedListener<K> listener) {
addListener(ElementAddedEvent.class, listener, ADDED_METHOD);
return this;
}
public MapField<K, V> removeElementAddedListener(
ElementAddedListener listener) {
removeListener(ElementAddedEvent.class, listener, ADDED_METHOD);
return this;
}
public MapField<K, V> addElementRemovedListener(
ElementRemovedListener<K> listener) {
addListener(ElementRemovedEvent.class, listener, REMOVED_METHOD);
return this;
}
public MapField<K, V> removeElementRemovedListener(
ElementRemovedListener listener) {
removeListener(ElementRemovedEvent.class, listener, REMOVED_METHOD);
return this;
}
public boolean isAllowNewItems() {
return allowNewItems;
}
public boolean isAllowRemovingItems() {
return allowRemovingItems;
}
public boolean isAllowEditItems() {
return allowEditItems;
}
public MapField<K, V> setAllowEditItems(boolean allowEditItems) {
this.allowEditItems = allowEditItems;
return this;
}
public MapField<K, V> setAllowRemovingItems(boolean allowRemovingItems) {
this.allowRemovingItems = allowRemovingItems;
return this;
}
public MapField<K, V> withCaption(String caption) {
setCaption(caption);
return this;
}
@Override
public void validate() throws Validator.InvalidValueException {
super.validate();
Map<K, V> v = getValue();
if (v != null) {
for (K o : v.keySet()) {
// TODO, should validate both key and value
// for (Field f : getFieldGroupFor((K) o).getFields()) {
// f.validate();
// }
}
}
}
private Map<K, V> getAndEnsureValue() {
Map<K, V> value = getValue();
if (value == null) {
if (getPropertyDataSource() == null) {
// this should never happen :-)
return new HashMap();
}
Class fieldType = getPropertyDataSource().getType();
if (fieldType.isInterface()) {
value = new HashMap<>();
} else {
try {
value = (Map) fieldType.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
throw new RuntimeException(
"Could not instantiate the used colleciton type", ex);
}
}
getPropertyDataSource().setValue(value);
}
return value;
}
public MapField<K, V> setAllowNewElements(
boolean allowNewItems) {
this.allowNewItems = allowNewItems;
return this;
}
public Class<K> getKeyType() {
return keyType;
}
protected TextField createKeyEditorInstance() {
MTextField tf = new MTextField().withInputPrompt("key");
return tf;
}
protected TextField createValueEditorInstance() {
MTextField tf = new MTextField().withInputPrompt("value");
return tf;
}
private void replaceValue(K key, String value) {
if (key == null) {
// new value without proper key, ignore
return;
}
K tKey;
try {
tKey = key;
} catch (ClassCastException e) {
try {
tKey = keyType.getConstructor(String.class).newInstance(key);
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchMethodException | SecurityException | InvocationTargetException ex) {
throw new RuntimeException("No suitable constructor found", ex);
}
}
V tVal;
try {
tVal = (V) value;
} catch (ClassCastException e) {
try {
tVal = valueType.getConstructor(String.class).newInstance(value);
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchMethodException | SecurityException | InvocationTargetException ex) {
throw new RuntimeException("No suitable constructor found", ex);
}
}
getAndEnsureValue().put(tKey, tVal);
}
private void renameValue(K oldKey, String key) {
K tKey;
try {
tKey = (K) key;
} catch (ClassCastException e) {
try {
tKey = keyType.getConstructor(String.class).newInstance(key);
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchMethodException | SecurityException | InvocationTargetException ex) {
throw new RuntimeException("No suitable constructor found", ex);
}
}
EntryEditor e;
V value;
if (oldKey != null) {
value = getAndEnsureValue().remove(oldKey);
e = pojoToEditor.remove(oldKey);
} else {
e = newEntryEditor;
String strValue = newEntryEditor.valueEditor.getValue();
try {
value = (V) strValue;
} catch (ClassCastException ex) {
try {
value = valueType.getConstructor(String.class).newInstance(strValue);
} catch (IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchMethodException | SecurityException | InvocationTargetException ex1) {
value = null;
}
}
e.delete.setEnabled(true);
// Old editor is used for the new value, create a new one
createNewEntryRow();
}
e.oldKey = tKey;
getAndEnsureValue().put(tKey, value);
pojoToEditor.put(tKey, e);
}
private EntryEditor getFieldGroupFor(K key) {
EntryEditor ee = pojoToEditor.get(key);
if (ee == null) {
final TextField k = createKeyEditorInstance();
final TextField v = createValueEditorInstance();
// TODO fieldgroup listener
final EntryEditor ee1 = ee = new EntryEditor(k, v, key);
// TODO listen for all changes for proper modified/validity changes
pojoToEditor.put(key, ee);
}
return ee;
}
public void addElement(K key, V value) {
getAndEnsureValue().put(key, value);
addInternalElement(key, value);
fireValueChange(false);
fireEvent(new ElementAddedEvent<>(this, key));
}
public void removeElement(K keyToBeRemoved) {
removeInternalElement(keyToBeRemoved);
getAndEnsureValue().remove(keyToBeRemoved);
fireValueChange(false);
fireEvent(new ElementRemovedEvent<>(this, keyToBeRemoved));
}
@Override
protected Component initContent() {
return getLayout();
}
@Override
protected void setInternalValue(Map newValue) {
super.setInternalValue(newValue);
clearCurrentEditors();
Map<K, V> value = newValue;
if (value != null) {
for (Map.Entry<K, V> entry : value.entrySet()) {
K key = entry.getKey();
V value1 = entry.getValue();
addInternalElement(key, value1);
}
}
if (isAllowNewItems()) {
createNewEntryRow();
}
onElementAdded();
}
private void createNewEntryRow() throws ReadOnlyException {
TextField keyEditor = createKeyEditorInstance();
TextField valueEditor = createKeyEditorInstance();
newEntryEditor = new EntryEditor(keyEditor, valueEditor, null);
addRowForEntry(newEntryEditor, null, null);
}
protected void addInternalElement(K k, V v) {
ensureInited();
EntryEditor fieldGroupFor = getFieldGroupFor(k);
addRowForEntry(fieldGroupFor, k, v);
}
private void addRowForEntry(EntryEditor editor, K k, V v) throws ReadOnlyException {
editor.bindValues(k, v);
getLayout().addComponents(editor.keyEditor, editor.valueEditor, editor.delete);
}
protected void setPersisted(K v, boolean persisted) {
// TODO create new "draft row"
}
protected void removeInternalElement(K v) {
// TODO remove from layout
}
protected Layout getLayout() {
return mainLayout;
}
protected void onElementAdded() {
System.out.println("What now!?");
}
private void clearCurrentEditors() {
while (mainLayout.getRows() > 1) {
mainLayout.removeRow(1);
}
}
public static class ElementAddedEvent<K> extends Component.Event {
private final K key;
public ElementAddedEvent(MapField source, K element) {
super(source);
this.key = element;
}
public K getKey() {
return key;
}
}
public static class ElementRemovedEvent<K> extends Component.Event {
private final K key;
public ElementRemovedEvent(MapField source, K element) {
super(source);
this.key = element;
}
public K getKey() {
return key;
}
}
public interface ElementAddedListener<K> extends Serializable {
void elementAdded(ElementAddedEvent<K> e);
}
public interface ElementRemovedListener<K> extends Serializable {
void elementRemoved(ElementRemovedEvent<K> e);
}
public interface Instantiator<ET> extends Serializable {
ET create();
}
private class EntryEditor implements Serializable {
private static final long serialVersionUID = 5710635901082609223L;
TextField keyEditor;
TextField valueEditor;
Button delete;
K oldKey;
EntryEditor(TextField ke, TextField valueEditor, K k) {
this.keyEditor = ke;
this.valueEditor = valueEditor;
delete = new Button(FontAwesome.TRASH);
delete.addClickListener(new Button.ClickListener() {
@Override
public void buttonClick(Button.ClickEvent event) {
Object removed = getValue().remove(oldKey);
pojoToEditor.remove(oldKey);
Iterator<Component> iterator = mainLayout.iterator();
int idx = 0;
while(iterator.next() != keyEditor) {
idx++;
}
mainLayout.removeRow(idx/3);
}
});
this.oldKey = k;
if (oldKey == null) {
delete.setEnabled(false);
}
}
private void bindValues(K k, V v) {
keyEditor.setValue(k == null ? null : k.toString());
valueEditor.setValue(v == null ? null : v.toString());
keyEditor.addValueChangeListener(new Property.ValueChangeListener() {
@Override
public void valueChange(Property.ValueChangeEvent event) {
renameValue(EntryEditor.this.oldKey, EntryEditor.this.keyEditor.getValue());
}
});
valueEditor.addValueChangeListener(new Property.ValueChangeListener() {
@Override
public void valueChange(Property.ValueChangeEvent event) {
replaceValue(EntryEditor.this.oldKey, EntryEditor.this.valueEditor.getValue());
}
});
}
}
}