/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.widget.attribute;
import com.smartgwt.client.data.DataSourceField;
import com.smartgwt.client.data.fields.DataSourceBooleanField;
import com.smartgwt.client.data.fields.DataSourceDateField;
import com.smartgwt.client.data.fields.DataSourceFloatField;
import com.smartgwt.client.data.fields.DataSourceImageFileField;
import com.smartgwt.client.data.fields.DataSourceIntegerField;
import com.smartgwt.client.data.fields.DataSourceTextField;
import com.smartgwt.client.widgets.Img;
import com.smartgwt.client.widgets.form.fields.BooleanItem;
import com.smartgwt.client.widgets.form.fields.CanvasItem;
import com.smartgwt.client.widgets.form.fields.DateItem;
import com.smartgwt.client.widgets.form.fields.FloatItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.IntegerItem;
import com.smartgwt.client.widgets.form.fields.LinkItem;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.validator.DateRangeValidator;
import com.smartgwt.client.widgets.form.validator.FloatPrecisionValidator;
import com.smartgwt.client.widgets.form.validator.FloatRangeValidator;
import com.smartgwt.client.widgets.form.validator.IntegerRangeValidator;
import com.smartgwt.client.widgets.form.validator.IsFloatValidator;
import com.smartgwt.client.widgets.form.validator.LengthRangeValidator;
import com.smartgwt.client.widgets.form.validator.RegExpValidator;
import com.smartgwt.client.widgets.form.validator.Validator;
import org.geomajas.annotation.Api;
import org.geomajas.configuration.AbstractEditableAttributeInfo;
import org.geomajas.configuration.AbstractReadOnlyAttributeInfo;
import org.geomajas.configuration.AssociationAttributeInfo;
import org.geomajas.configuration.AssociationType;
import org.geomajas.configuration.PrimitiveAttributeInfo;
import org.geomajas.configuration.PrimitiveType;
import org.geomajas.configuration.SyntheticAttributeInfo;
import org.geomajas.configuration.validation.ConstraintInfo;
import org.geomajas.configuration.validation.DecimalMaxConstraintInfo;
import org.geomajas.configuration.validation.DecimalMinConstraintInfo;
import org.geomajas.configuration.validation.DigitsConstraintInfo;
import org.geomajas.configuration.validation.FutureConstraintInfo;
import org.geomajas.configuration.validation.MaxConstraintInfo;
import org.geomajas.configuration.validation.MinConstraintInfo;
import org.geomajas.configuration.validation.NotNullConstraintInfo;
import org.geomajas.configuration.validation.PastConstraintInfo;
import org.geomajas.configuration.validation.PatternConstraintInfo;
import org.geomajas.configuration.validation.SizeConstraintInfo;
import org.geomajas.configuration.validation.ValidatorInfo;
import org.geomajas.gwt.client.map.layer.VectorLayer;
import org.geomajas.gwt.client.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* Factory that creates {@link FormItem}s and {@link DataSourceField}s from attribute meta-data. It is also possible to
* register custom form field definitions using {@link AbstractReadOnlyAttributeInfo#formInputType} field.
* </p>
* <p>
* When defining custom implementations of the {@link FeatureFormFactory}, you are strongly encouraged to use this class
* to create the actual fields within the forms, and to use both a {@link DataSourceField} and a {@link FormItem} for
* each attribute you want to display in the form.<br/>
* The form item will provide the view on the form field, while the data source field will provide the underlying data
* control (with validators).
* </p>
*
* @author Pieter De Graef
* @since 1.10.0
*/
@Api(allMethods = true)
public final class AttributeFormFieldRegistry {
private static final Map<String, FormItemFactory> FORM_ITEMS;
private static final Map<String, DataSourceFieldFactory> DATA_SOURCE_FIELDS;
private static final Map<String, List<Validator>> FIELD_VALIDATORS;
// Create the default types for the known attribute types:
static {
FORM_ITEMS = new HashMap<String, FormItemFactory>();
DATA_SOURCE_FIELDS = new HashMap<String, DataSourceFieldFactory>();
FIELD_VALIDATORS = new HashMap<String, List<Validator>>();
// Type: BOOLEAN
registerCustomFormItem(PrimitiveType.BOOLEAN.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceBooleanField();
}
}, new FormItemFactory() {
public FormItem create() {
BooleanItem item = new BooleanItem();
item.setValue(false); // Avoid null value
return item;
}
}, null);
// TYPE: STRING
registerCustomFormItem(PrimitiveType.STRING.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceTextField();
}
}, new FormItemFactory() {
public FormItem create() {
return new TextItem();
}
}, null);
// TYPE: SHORT
Validator shortValidator = new IntegerRangeValidator();
((IntegerRangeValidator) shortValidator).setMin(Short.MIN_VALUE);
((IntegerRangeValidator) shortValidator).setMax(Short.MAX_VALUE);
registerCustomFormItem(PrimitiveType.SHORT.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceIntegerField();
}
}, new FormItemFactory() {
public FormItem create() {
return new IntegerItem();
}
}, Collections.singletonList(shortValidator));
// TYPE: INTEGER
registerCustomFormItem(PrimitiveType.INTEGER.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceIntegerField();
}
}, new FormItemFactory() {
public FormItem create() {
return new IntegerItem();
}
}, null);
// TYPE: LONG
registerCustomFormItem(PrimitiveType.LONG.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceIntegerField();
}
}, new FormItemFactory() {
public FormItem create() {
return new IntegerItem();
}
}, null);
// TYPE: FLOAT
registerCustomFormItem(PrimitiveType.FLOAT.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceFloatField();
}
}, new FormItemFactory() {
public FormItem create() {
return new FloatItem();
}
}, null);
// TYPE: DOUBLE
registerCustomFormItem(PrimitiveType.DOUBLE.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceFloatField();
}
}, new FormItemFactory() {
public FormItem create() {
return new FloatItem();
}
}, null);
// TYPE: DATE
registerCustomFormItem(PrimitiveType.DATE.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceDateField();
}
}, new FormItemFactory() {
public FormItem create() {
return new DateItem();
}
}, null);
// TYPE: URL
registerCustomFormItem(PrimitiveType.URL.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceTextField();
}
}, new FormItemFactory() {
public FormItem create() {
return new EnableToggleFormItem(new TextItem(), new LinkItem());
}
}, null);
// TYPE: IMGURL
registerCustomFormItem(PrimitiveType.IMGURL.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceImageFileField();
}
}, new FormItemFactory() {
public FormItem create() {
final Img image = new Img();
image.setMaxHeight(200);
image.setMaxWidth(300);
image.setShowDisabled(false);
CanvasItem imgItem = new CanvasItem() {
public void setValue(String value) {
image.setSrc(value);
}
};
imgItem.setCanvas(image);
return new EnableToggleFormItem(new TextItem(), imgItem);
}
}, null);
// TYPE: CURRENCY
Validator currencyValidator = new IsFloatValidator();
registerCustomFormItem(PrimitiveType.CURRENCY.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceTextField();
}
}, new FormItemFactory() {
public FormItem create() {
return new TextItem();
}
}, Collections.singletonList(currencyValidator));
// TYPE: MANY_TO_ONE
registerCustomFormItem(AssociationType.MANY_TO_ONE.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
// Don't use a DataSourceEnumField, as it doesn't work together with the SelectItem's OptionDataSource.
return new DataSourceTextField();
}
}, new FormItemFactory() {
public FormItem create() {
DefaultManyToOneItem manyToOneItem = new DefaultManyToOneItem();
manyToOneItem.getItem().setAttribute(AssociationItem.ASSOCIATION_ITEM_ATTRIBUTE_KEY, manyToOneItem);
return manyToOneItem.getItem();
}
}, null);
// TYPE: ONE_TO_MANY
registerCustomFormItem(AssociationType.ONE_TO_MANY.name(), new DataSourceFieldFactory() {
public DataSourceField create() {
return new DataSourceField();
}
}, new FormItemFactory() {
public FormItem create() {
DefaultOneToManyItem oneToMany = new DefaultOneToManyItem();
oneToMany.getItem().setAttribute(AssociationItem.ASSOCIATION_ITEM_ATTRIBUTE_KEY, oneToMany);
return oneToMany.getItem();
}
}, null);
}
private AttributeFormFieldRegistry() {
// Utility class: hide constructor.
}
/**
* <p>
* Register a new type of {@link FormItem} and {@link DataSourceField} for a certain kind of attribute or key. If
* there already was a definition registered for the given key, than it will be replaced by the new one. For all
* known attribute types (integer, string, date, url, float, many-to-one, ...) there are default definitions within
* this registry. New types can be added or these existing types can be overwritten, that's up to you.
* </p>
* <p>
* In order to use completely new custom defined form item types, you can always define the
* <code>formInputType</code> in the attribute definitions of the vector layers, and then register a form item with
* the same key here in this factory.
* </p>
*
* @param key The key associated with the given {@link FormItemFactory} and {@link DataSourceFieldFactory}. This key
* is either the name of an attribute type (i.e. <code>PrimitiveType.DATE.name()</code>) to overwrite the
* default definitions, or a completely new type which can be configured in the attribute definitions with
* the <code>formInputType</code> field.
* @param fieldType The type of {@link DataSourceFieldFactory} associated with the given key. This factory will
* create the correct {@link DataSourceField} for the given key.
* @param editorType The type of {@link FormItemFactory} associated with the given key. This factory will create the
* correct {@link FormItem} for the given key.
* @param validators A list of validators that can be applied to the {@link DataSourceField}. This is optional and
* can be null. These validators protect the data, and can for example make sure that a user does not use any
* letters while filling an integer type field.
*/
public static void registerCustomFormItem(String key, DataSourceFieldFactory fieldType, FormItemFactory editorType,
List<Validator> validators) {
if (key == null || fieldType == null || editorType == null) {
throw new IllegalArgumentException("Cannot provide null values when registering new form items.");
}
DATA_SOURCE_FIELDS.put(key, fieldType);
FORM_ITEMS.put(key, editorType);
FIELD_VALIDATORS.put(key, null == validators ? new ArrayList<Validator>() : validators);
}
/**
* Create a new {@link DataSourceField} instance for the given attribute info. This field can provide additional
* validators on the field type (if they are registered), to protect the data.<br/>
* If the attribute info object has the <code>formInputType</code> set, than that will be used to search for the
* correct field type, otherwise the attribute TYPE name is used (i.e. PrimitiveType.INTEGER.name()).
*
* @param info The actual attribute info to create a data source field for.
* @return The new data source field instance associated with the type of attribute.
*/
public static DataSourceField createDataSourceField(AbstractReadOnlyAttributeInfo info) {
DataSourceField field = null;
List<Validator> validators = new ArrayList<Validator>();
if (info.getFormInputType() != null) {
String formInputType = info.getFormInputType();
DataSourceFieldFactory factory = DATA_SOURCE_FIELDS.get(formInputType);
if (null != factory) {
field = factory.create();
List<Validator> fieldValidators = FIELD_VALIDATORS.get(formInputType);
if (null != fieldValidators) {
validators.addAll(fieldValidators);
}
} else {
Log.logWarn("Cannot find data source factory for " + info.getFormInputType() +
", using default instead.");
}
}
if (field == null) {
if (info instanceof PrimitiveAttributeInfo) {
String name = ((PrimitiveAttributeInfo) info).getType().name();
field = DATA_SOURCE_FIELDS.get(name).create();
validators = new ArrayList<Validator>(FIELD_VALIDATORS.get(name));
} else if (info instanceof SyntheticAttributeInfo) {
String name = PrimitiveType.STRING.name();
field = DATA_SOURCE_FIELDS.get(name).create();
validators.addAll(FIELD_VALIDATORS.get(name));
} else if (info instanceof AssociationAttributeInfo) {
String name = ((AssociationAttributeInfo) info).getType().name();
field = DATA_SOURCE_FIELDS.get(name).create();
validators.addAll(FIELD_VALIDATORS.get(name));
} else {
throw new IllegalStateException("Don't know how to handle field " + info.getName() + ", " +
"maybe you need to define the formInputType.");
}
}
if (field != null) {
field.setName(info.getName());
field.setTitle(info.getLabel());
field.setCanEdit(info.isEditable());
field.setRequired(info instanceof AbstractEditableAttributeInfo &&
isRequired(((AbstractEditableAttributeInfo) info).getValidator()));
if (info instanceof PrimitiveAttributeInfo) {
validators.addAll(convertConstraints((PrimitiveAttributeInfo) info));
}
if (validators.size() > 0) {
field.setValidators(validators.toArray(new Validator[validators.size()]));
}
return field;
}
return null;
}
/**
* Create a new {@link FormItem} instance for the given attribute info (top level attribute).<br/>
* If the attribute info object has the <code>formInputType</code> set, than that will be used to search for the
* correct field type, otherwise the attribute TYPE name is used (i.e. PrimitiveType.INTEGER.name()).
*
* @param info The actual attribute info to create a form item for
* @param layer The layer to create a form item for (needed to fetch association values)
* @return The new form item instance associated with the type of attribute.
*/
public static FormItem createFormItem(AbstractReadOnlyAttributeInfo info, VectorLayer layer) {
return createFormItem(info, new DefaultAttributeProvider(layer, info.getName()));
}
/**
* Create a new {@link FormItem} instance for the given attribute info.<br/>
* If the attribute info object has the <code>formInputType</code> set, than that will be used to search for the
* correct field type, otherwise the attribute TYPE name is used (i.e. PrimitiveType.INTEGER.name()).
*
* @param info The actual attribute info to create a form item for.
* @param attributeProvider The attribute value provider for association attributes
* @return The new form item instance associated with the type of attribute.
*/
public static FormItem createFormItem(AbstractReadOnlyAttributeInfo info, AttributeProvider attributeProvider) {
FormItem formItem = null;
if (info.getFormInputType() != null) {
FormItemFactory factory = FORM_ITEMS.get(info.getFormInputType());
if (null != factory) {
formItem = factory.create();
} else {
Log.logWarn("Cannot find form item for " + info.getFormInputType() + ", using default instead.");
}
}
if (formItem == null) {
//Only check if attribute is editable. Non editable attributes can be ignored.
if (info instanceof PrimitiveAttributeInfo) {
String name = ((PrimitiveAttributeInfo) info).getType().name();
formItem = FORM_ITEMS.get(name).create();
} else if (info instanceof SyntheticAttributeInfo) {
String name = PrimitiveType.STRING.name();
formItem = FORM_ITEMS.get(name).create();
} else if (info instanceof AssociationAttributeInfo) {
String name = ((AssociationAttributeInfo) info).getType().name();
formItem = FORM_ITEMS.get(name).create();
} else {
throw new IllegalStateException("Don't know how to create form for field " + info.getName() + ", " +
"maybe you need to define the formInputType.");
}
}
if (formItem != null) {
formItem.setName(info.getName());
formItem.setTitle(info.getLabel());
formItem.setValidateOnChange(true);
formItem.setWidth("*");
// Special treatment for associations
if (info instanceof AssociationAttributeInfo) {
AssociationAttributeInfo associationInfo = (AssociationAttributeInfo) info;
String displayName = associationInfo.getFeature().getDisplayAttributeName();
if (displayName == null) {
displayName = associationInfo.getFeature().getAttributes().get(0).getName();
}
formItem.setDisplayField(displayName);
Object o = formItem.getAttributeAsObject(AssociationItem.ASSOCIATION_ITEM_ATTRIBUTE_KEY);
if (o instanceof OneToManyItem<?>) {
OneToManyItem<?> item = (OneToManyItem<?>) o;
item.init(associationInfo, attributeProvider);
} else if (o instanceof ManyToOneItem<?>) {
ManyToOneItem<?> item = (ManyToOneItem<?>) o;
item.init(associationInfo, attributeProvider);
}
}
return formItem;
}
return null;
}
// -------------------------------------------------------------------------
// Private methods concerning VALIDATORS:
// -------------------------------------------------------------------------
private static boolean isRequired(ValidatorInfo info) {
if (null != info) {
for (ConstraintInfo constraint : info.getConstraints()) {
if (constraint instanceof NotNullConstraintInfo) {
return true;
}
}
}
return false;
}
private static List<Validator> convertConstraints(PrimitiveAttributeInfo info) {
List<Validator> validators = new ArrayList<Validator>();
for (ConstraintInfo constraint : info.getValidator().getConstraints()) {
Validator validator = null;
boolean nullValidator = false;
if (constraint instanceof DecimalMaxConstraintInfo) {
validator = createFromDecimalMax((DecimalMaxConstraintInfo) constraint);
} else if (constraint instanceof DecimalMinConstraintInfo) {
validator = createFromDecimalMin((DecimalMinConstraintInfo) constraint);
} else if (constraint instanceof DigitsConstraintInfo) {
Validator[] v2 = createFromDigits((DigitsConstraintInfo) constraint, info.getType());
for (Validator v : v2) {
addErrorMessage(info, validators, v);
}
} else if (constraint instanceof FutureConstraintInfo) {
validator = createFromFuture();
} else if (constraint instanceof MaxConstraintInfo) {
validator = createFromMax((MaxConstraintInfo) constraint);
} else if (constraint instanceof MinConstraintInfo) {
validator = createFromMin((MinConstraintInfo) constraint);
} else if (constraint instanceof PastConstraintInfo) {
validator = createFromPast();
} else if (constraint instanceof PatternConstraintInfo) {
validator = createFromPattern((PatternConstraintInfo) constraint);
} else if (constraint instanceof SizeConstraintInfo) {
validator = createFromSize((SizeConstraintInfo) constraint, info.getType());
} else if (constraint instanceof NotNullConstraintInfo) {
nullValidator = true; // nothing to do
} else {
throw new IllegalStateException("Unknown constraint type " + constraint);
}
if (null != validator && !nullValidator) {
addErrorMessage(info, validators, validator);
}
}
return validators;
}
private static void addErrorMessage(PrimitiveAttributeInfo info, List<Validator> validators, Validator validator) {
validator.setErrorMessage(info.getValidator().getErrorMessage());
validators.add(validator);
}
private static Validator createFromDecimalMin(DecimalMinConstraintInfo decimalMin) {
FloatRangeValidator floatMin = new FloatRangeValidator();
floatMin.setMin(Float.parseFloat(decimalMin.getValue()));
return floatMin;
}
private static Validator createFromDecimalMax(DecimalMaxConstraintInfo decimalMax) {
FloatRangeValidator floatMax = new FloatRangeValidator();
floatMax.setMax(Float.parseFloat(decimalMax.getValue()));
return floatMax;
}
private static Validator[] createFromDigits(DigitsConstraintInfo digits, PrimitiveType type) {
FloatPrecisionValidator floatPrecision = new FloatPrecisionValidator();
floatPrecision.setPrecision(digits.getFractional());
floatPrecision.setRoundToPrecision(digits.getFractional());
IntegerRangeValidator integerDigit;
FloatRangeValidator floatDigit;
Validator[] validators;
switch (type) {
case SHORT:
case INTEGER:
case LONG:
integerDigit = new IntegerRangeValidator();
integerDigit.setMax((int) Math.pow(10.0, digits.getInteger()) - 1);
validators = new Validator[] { floatPrecision, integerDigit };
break;
case FLOAT:
case DOUBLE:
case CURRENCY:
floatDigit = new FloatRangeValidator();
floatDigit.setMax((int) Math.pow(10.0, digits.getInteger()) - Float.MIN_VALUE);
validators = new Validator[] { floatPrecision, floatDigit };
break;
default:
throw new IllegalStateException("Cannot createFromDigits for type " + type);
}
return validators;
}
private static Validator createFromFuture() {
DateRangeValidator dateFuture = new DateRangeValidator();
dateFuture.setMin(new Date());
return dateFuture;
}
private static Validator createFromMax(MaxConstraintInfo max) {
IntegerRangeValidator integerMax = new IntegerRangeValidator();
integerMax.setMax((int) max.getValue());
return integerMax;
}
private static Validator createFromMin(MinConstraintInfo min) {
IntegerRangeValidator integerMin = new IntegerRangeValidator();
integerMin.setMin((int) min.getValue());
return integerMin;
}
private static Validator createFromPast() {
DateRangeValidator datePast = new DateRangeValidator();
datePast.setMax(new Date());
return datePast;
}
private static Validator createFromPattern(PatternConstraintInfo pattern) {
RegExpValidator regexp = new RegExpValidator();
regexp.setExpression(pattern.getRegexp());
return regexp;
}
private static Validator createFromSize(SizeConstraintInfo size, PrimitiveType type) {
switch (type) {
case STRING:
case URL:
case IMGURL:
LengthRangeValidator lengthRange = new LengthRangeValidator();
lengthRange.setMax(size.getMin());
lengthRange.setMax(size.getMax());
return lengthRange;
}
return null;
}
}