/*
* JBoss, Home of Professional Open Source
* Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.jboss.ballroom.client.widgets.forms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.google.gwt.autobean.shared.AutoBean;
import com.google.gwt.autobean.shared.AutoBeanCodex;
import com.google.gwt.autobean.shared.AutoBeanFactory;
import com.google.gwt.autobean.shared.AutoBeanUtils;
import com.google.gwt.autobean.shared.AutoBeanVisitor;
import com.google.gwt.autobean.shared.Splittable;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.RowCountChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;
import org.jboss.ballroom.client.spi.Framework;
/**
* Form data binding that works on {@link AutoBean} entities.
*
* @author Heiko Braun
* @date 2/21/11
*/
public class Form<T> implements FormAdapter<T> {
private final static Framework framework = GWT.create(Framework.class);
private static final String EXPR_TAG = "EXPRESSIONS";
private AutoBeanFactory factory;
final static String DEFAULT_GROUP = "default";
private final Map<String, Map<String, FormItem>> formItems = new LinkedHashMap<String, Map<String, FormItem>>();
final Map<String,GroupRenderer> renderer = new HashMap<String, GroupRenderer>();
private int numColumns = 1;
private int nextId = 1;
private T editedEntity = null;
private final Class<?> conversionType;
private List<EditListener> listeners = new ArrayList<EditListener>();
private DeckPanel deck;
private List<PlainFormView> plainViews = new ArrayList<PlainFormView>();
private boolean isEnabled =true; // backwards compatibility
public Form(Class<?> conversionType) {
this.conversionType = conversionType;
this.factory = framework.getBeanFactory();
}
@Override
public Class<?> getConversionType() {
return conversionType;
}
/**
* Number of layout columns.<br>
* Form fields will fill columns in the order they have been specified
* in {@link #setFields(FormItem[])}.
*
* @param columns
*/
public void setNumColumns(int columns)
{
this.numColumns = columns;
}
/**
* Specify the form fields.
* Needs to be called before {@link #asWidget()}.
*
* @param items
*/
public void setFields(FormItem... items) {
setFieldsInGroup(DEFAULT_GROUP, items);
}
public void setFields(FormItem[]... items) {
setFieldsInGroup(DEFAULT_GROUP, flatten(items));
}
int maxTitleLength = 0; // used for auto layout
public void setFieldsInGroup(String group, FormItem... items) {
// create new group
LinkedHashMap<String, FormItem> groupItems = new LinkedHashMap<String, FormItem>();
formItems.put(group, groupItems);
for(FormItem item : items)
{
String title = item.getTitle();
if(title.length()>maxTitleLength)
{
maxTitleLength = title.length();
}
// key maybe be used multiple times
String itemKey = item.getName();
if(groupItems.containsKey(itemKey)) {
groupItems.put(itemKey+"#"+nextId, item);
nextId++;
}
else
{
groupItems.put(itemKey, item);
}
}
}
public void setFieldsInGroup(String group, FormItem[]... items) {
setFieldsInGroup(group, flatten(items));
}
public void setFieldsInGroup(String group, GroupRenderer renderer, FormItem... items) {
this.renderer.put(group, renderer);
setFieldsInGroup(group, items);
}
public void setFieldsInGroup(String group, GroupRenderer renderer, FormItem[]... items) {
setFieldsInGroup(group, renderer, flatten(items));
}
private FormItem<?>[] flatten(FormItem<?>[]... items) {
List<FormItem<?>> l = new ArrayList<FormItem<?>>();
for (FormItem<?> [] fiArray : items) {
for (FormItem<?> fi : fiArray) {
l.add(fi);
}
}
FormItem<?>[] array = l.toArray(new FormItem<?>[l.size()]);
return array;
}
/**
* This method passes the original entity back into the form, removing all changes.
*/
@Override
public void cancel() {
if(editedEntity!=null)
edit(editedEntity);
}
@Override
public void edit(T bean) {
// Needs to be declared (i.e. when creating new instances)
if(null==bean)
throw new IllegalArgumentException("Invalid entity: null");
// Has to be an AutoBean
final AutoBean<T> autoBean = asAutoBean(bean);
this.editedEntity = bean;
final Map<String, String> exprMap = getExpressions(editedEntity);
autoBean.accept(new AutoBeanVisitor() {
private boolean isComplex = false;
@Override
public boolean visitValueProperty(final String propertyName, final Object value, PropertyContext ctx) {
if(isComplex ) return true; // skip complex types
visitItem(propertyName, new FormItemVisitor() {
@Override
public void visit(FormItem item) {
item.resetMetaData();
// expressions
// if(item.doesSupportExpressions())
//{
String exprValue = exprMap.get(propertyName);
if(exprValue!=null)
{
item.setUndefined(false);
item.setExpressionValue(exprValue);
}
//}
// values
else if(value!=null)
{
item.setUndefined(false);
item.setValue(value);
}
else
{
item.setUndefined(true);
item.setModified(true); // don't escape validation
}
}
});
return true;
}
@Override
public void endVisitReferenceProperty(String propertyName, AutoBean<?> value, PropertyContext ctx) {
//System.out.println("end reference "+propertyName);
isComplex = false;
}
@Override
public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, PropertyContext ctx) {
isComplex = true;
//System.out.println("begin reference "+propertyName+ ": "+ctx.getType());
return true;
}
@Override
public boolean visitCollectionProperty(String propertyName, final AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
visitItem(propertyName, new FormItemVisitor() {
@Override
public void visit(FormItem item) {
item.resetMetaData();
if(value!=null)
{
item.setUndefined(false);
item.setValue(value.as());
}
else
{
item.setUndefined(true);
item.setModified(true); // don't escape validation
}
}
});
return true;
}
@Override
public void endVisitCollectionProperty(String propertyName, AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
super.endVisitCollectionProperty(propertyName, value, ctx);
}
});
notifyListeners(bean);
// plain views
refreshPlainView();
}
private void notifyListeners(T bean) {
for (EditListener listener : listeners) {
listener.editingBean(bean);
}
}
@Override
public void addEditListener(EditListener listener) {
this.listeners.add(listener);
}
@Override
public void removeEditListener(EditListener listener) {
this.listeners.remove(listener);
}
void visitItem(final String name, FormItemVisitor visitor) {
String namePrefix = name + "_";
for(Map<String, FormItem> groupItems : formItems.values())
{
for(String key : groupItems.keySet())
{
if(key.equals(name) || key.startsWith(namePrefix))
{
visitor.visit(groupItems.get(key));
}
}
}
}
/**
* Get changed values since last {@link #edit(Object)} ()}
* @return
*/
@Override
public Map<String, Object> getChangedValues() {
final T editedEntity = getEditedEntity();
if(null==editedEntity)
return new HashMap<String, Object>();
final T updatedEntity = getUpdatedEntity();
Map<String, Object> diff = AutoBeanUtils.diff(
AutoBeanUtils.getAutoBean(editedEntity),
AutoBeanUtils.getAutoBean(updatedEntity)
);
Map<String, Object> finalDiff = new HashMap<String,Object>();
// map changes, but skip unmodified fields
for(Map<String, FormItem> groupItems : formItems.values())
{
for(FormItem item : groupItems.values())
{
Object val = diff.get(item.getName());
// expression have precedence over real values
if(item.isExpressionValue())
{
finalDiff.put(item.getName(), item.asExpressionValue());
}
// regular values
else if(val!=null && item.isModified())
{
if(item.isUndefined())
finalDiff.put(item.getName(), FormItem.VALUE_SEMANTICS.UNDEFINED);
else
finalDiff.put(item.getName(), val);
}
}
}
return finalDiff;
}
@Override
public FormValidation validate()
{
FormValidation outcome = new FormValidation();
for(Map<String, FormItem> groupItems : formItems.values())
{
for(FormItem item : groupItems.values())
{
// two cases: empty form (create entity) and updating an existing entity
// we basically force validation on newly created entities
boolean requiresValidation = getEditedEntity()!=null ? item.isModified() : true;
if(requiresValidation)
{
Object value = item.getValue();
// ascii or empty string are ok. the later will be checked in each form item implentation.
String stringValue = String.valueOf(value);
boolean ascii = stringValue.isEmpty() ||
stringValue.matches("^[\\u0020-\\u007e]+$");
if(!ascii)
{
outcome.addError(item.getName());
item.setErroneous(true);
}
else
{
boolean validValue = item.validate(value);
if(validValue)
{
item.setErroneous(false);
}
else
{
outcome.addError(item.getName());
item.setErroneous(true);
}
}
}
}
}
return outcome;
}
/**
* This is what the entity looks like with the user's changes on the form.
*/
@Override
public T getUpdatedEntity() {
Map<String,String> exprMap = new HashMap<String,String>();
StringBuilder builder = new StringBuilder("{");
int g=0;
for(Map<String, FormItem> groupItems : formItems.values())
{
int i=0;
for(FormItem item : groupItems.values())
{
builder.append("\"");
builder.append(item.getName());
builder.append("\"");
builder.append(":");
builder.append(encodeValue(item.getValue()));
if(i<groupItems.size()-1)
builder.append(", ");
i++;
// Expressions
if(item.isExpressionValue())
exprMap.put(item.getName(), item.asExpressionValue());
}
if(g<formItems.size()-1)
builder.append(", ");
g++;
}
builder.append("}");
AutoBean<?> decoded = AutoBeanCodex.decode(
factory,
conversionType,
builder.toString()
);
decoded.setTag(EXPR_TAG, exprMap);
return (T) decoded.as();
}
private String encodeValue(Object object) {
StringBuilder sb = new StringBuilder();
if(object instanceof List) // list objects
{
List listObject = (List)object;
sb.append("[");
int c = 0;
for(Object item : listObject)
{
sb.append(encodeValue(item));
if(c<listObject.size()-1)
sb.append(", ");
c++;
}
sb.append("]");
} else if (AutoBeanUtils.getAutoBean(object) != null) {
Splittable split = AutoBeanCodex.encode(AutoBeanUtils.getAutoBean(object));
sb.append("{ ");
sb.append(encodeValue(split));
sb.append(" }");
} else if (object instanceof Splittable) {
Splittable split = (Splittable)object;
if (split.isString()) return encodeValue(split.asString());
int c = 0;
List<String> keys = split.getPropertyKeys();
for (String key : keys) {
sb.append(encodeValue(key));
sb.append(" : ");
sb.append(encodeValue(split.get(key)));
if(c<keys.size()-1)
sb.append(", ");
c++;
}
} else {
sb.append("\"");
sb.append(object.toString());
sb.append("\"");
}
return sb.toString();
}
@Override
public Widget asWidget() {
return build();
}
private void refreshPlainView() {
for(PlainFormView view : plainViews)
view.refresh(getEditedEntity()!=null);
}
private Widget build() {
deck = new DeckPanel();
deck.setStyleName("fill-layout-width");
// ----------------------
// view panel
VerticalPanel viewPanel = new VerticalPanel();
viewPanel.setStyleName("fill-layout-width");
viewPanel.addStyleName("form-view-panel");
deck.add(viewPanel.asWidget());
// ----------------------
// edit panel
VerticalPanel editPanel = new VerticalPanel();
editPanel.setStyleName("fill-layout-width");
editPanel.addStyleName("form-edit-panel");
RenderMetaData metaData = new RenderMetaData();
metaData.setNumColumns(numColumns);
metaData.setTitleWidth(maxTitleLength);
for(String group : formItems.keySet())
{
Map<String, FormItem> groupItems = formItems.get(group);
GroupRenderer groupRenderer = null;
if(DEFAULT_GROUP.equals(group))
groupRenderer = new DefaultGroupRenderer();
else
groupRenderer = renderer.get(group)!=null ? renderer.get(group) : new FieldsetRenderer();
// edit view
Widget widget = groupRenderer.render(metaData, group, groupItems);
editPanel.add(widget);
// plain view
PlainFormView plainView = new PlainFormView(new ArrayList<FormItem>(groupItems.values()));
plainView.setNumColumns(numColumns);
plainViews.add(plainView);
viewPanel.add(groupRenderer.renderPlain(metaData, group, plainView));
}
deck.add(editPanel);
// toggle default view
toggleViews();
refreshPlainView(); // make sureit's build, even empty...
return deck;
}
/**
* Enable/disable this form.
*
* @param b
*/
@Override
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
if(deck!=null) // might no be created yet (backwards compatibility)
toggleViews();
}
private void toggleViews() {
int index = isEnabled ? 1 :0;
deck.showWidget(index);
}
/**
* Binds a default single selection model to the table
* that displays selected rows in a form.
*
* @param table
*/
@Override
public void bind(CellTable<T> table) {
SingleSelectionModel<T> selectionModel = (SingleSelectionModel<T>)table.getSelectionModel();
if (selectionModel == null) {
selectionModel = new SingleSelectionModel<T>();
}
final SingleSelectionModel<T> finalSelectionModel = selectionModel;
selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
public void onSelectionChange(SelectionChangeEvent event) {
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
T selectedObject = finalSelectionModel.getSelectedObject();
if(selectedObject!=null)
edit(selectedObject);
else
{
clearValues();
}
}
});
}
});
table.setSelectionModel(finalSelectionModel);
table.addRowCountChangeHandler(new RowCountChangeEvent.Handler() {
public void onRowCountChange(RowCountChangeEvent event) {
if(event.getNewRowCount()==0 && event.isNewRowCountExact())
clearValues();
}
});
}
@Override
public void clearValues() {
for(Map<String, FormItem> groupItems : formItems.values())
{
for(FormItem item : groupItems.values())
{
item.resetMetaData();
item.clearValue();
}
}
editedEntity = null;
refreshPlainView();
}
/**
* This is the entity that was originally passed in for editing. It does not include
* changes made by the user.
*
* @return The original entity used for editing.
*/
public T getEditedEntity() {
return editedEntity;
}
interface FormItemVisitor {
void visit(FormItem item);
}
@Override
public List<String> getFormItemNames() {
List<String> result = new ArrayList<String>();
for(Map<String, FormItem> groupItems : formItems.values())
{
for(FormItem item : groupItems.values())
{
result.add(item.getName());
}
}
return result;
}
public static Map<String,String> getExpressions(Object bean)
{
final AutoBean autoBean = asAutoBean(bean);
Map<String, String> exprMap = (Map<String,String>)autoBean.getTag(EXPR_TAG);
if(null==exprMap)
{
exprMap = new HashMap<String,String>();
autoBean.setTag(EXPR_TAG, exprMap);
}
return exprMap;
}
private static AutoBean asAutoBean(Object bean) {
final AutoBean autoBean = AutoBeanUtils.getAutoBean(bean);
if(null==autoBean)
throw new IllegalArgumentException("Not an auto bean: " + bean.getClass());
return autoBean;
}
}