/*
* Copyright (C) 2014 Jan Pokorsky
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.cas.lib.proarc.webapp.client.widget.form;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.smartgwt.client.data.DataSource;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.types.DateDisplayFormat;
import com.smartgwt.client.types.ReadOnlyDisplayAppearance;
import com.smartgwt.client.types.TitleOrientation;
import com.smartgwt.client.util.JSOHelper;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.FormItemValueFormatter;
import com.smartgwt.client.widgets.form.FormItemValueParser;
import com.smartgwt.client.widgets.form.ValuesManager;
import com.smartgwt.client.widgets.form.fields.ComboBoxItem;
import com.smartgwt.client.widgets.form.fields.DateItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.RadioGroupItem;
import com.smartgwt.client.widgets.form.fields.SelectItem;
import com.smartgwt.client.widgets.form.fields.TextAreaItem;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import cz.cas.lib.proarc.webapp.client.ds.ValueMapDataSource;
import cz.cas.lib.proarc.webapp.client.widget.mods.AbstractModelForm;
import cz.cas.lib.proarc.webapp.client.widget.mods.RepeatableFormItem;
import cz.cas.lib.proarc.webapp.client.widget.mods.RepeatableFormItem.CustomFormFactory;
import cz.cas.lib.proarc.webapp.client.widget.mods.RepeatableFormItem.FormWidget;
import cz.cas.lib.proarc.webapp.client.widget.mods.RepeatableFormItem.FormWidgetFactory;
import cz.cas.lib.proarc.webapp.client.widget.mods.StringFormFactory;
import cz.cas.lib.proarc.webapp.shared.form.Field;
import cz.cas.lib.proarc.webapp.shared.form.Form;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Generates the form hierarchy from {@link Form} declaration.
*
* @author Jan Pokorsky
*/
public class FormGenerator {
private static final Logger LOG = Logger.getLogger(FormGenerator.class.getName());
private final Form formDeclaration;
private final String activeLocale;
public int defaultHoverWidth = 300;
private int defaultTextLength = 1000;
private String defaultWidth = "400";
public FormGenerator(Form f, String activeLocale) {
this.formDeclaration = f;
this.activeLocale = activeLocale;
if (f.getItemWidth() != null) {
defaultWidth = f.getItemWidth();
}
}
/**
* Builds the SmartGWT form.
* @return the form
*/
public final DynamicForm generateForm() {
List<Field> fields = formDeclaration.getFields();
ArrayList<FormItem> formItems = new ArrayList<FormItem>(fields.size());
for (Field child : fields) {
FormItem formItem = createItem(child, activeLocale);
if (formItem != null) {
formItems.add(formItem);
}
}
DynamicForm df = createDefaultForm();
addFormItems(df, formItems);
return df;
}
// enum is not extensible :-(
protected enum ItemType {
/** array of simple type values */
ARRAY,
/** simple type value */
PLAIN,
/** nested form */
FORM,
/** nested form that will be customized with {@link FormGenerator#customizeNestedForm } */
CUSTOM_FORM
}
public FormItem createItem(final Field f, final String lang) {
ItemType itemType = getType(f);
FormItem formItem = null;
switch (itemType) {
case PLAIN:
formItem = getFormItem(f, lang);
break;
case ARRAY:
// XXX replace StringFormFactory with a generic type solution
formItem = new RepeatableFormItem(f,
new StringFormFactory(f.getName(), f.getTitle(lang), false));
break;
case FORM:
formItem = createNestedFormItem(f, lang);
break;
case CUSTOM_FORM:
formItem = createNestedCustomFormItem(f, lang);
break;
}
return customizeFormItem(formItem, f);
}
protected ItemType getType(Field f) {
ItemType itemType;
// fType != null -> simple field
// fType maxOccurencies > 1 -> repeatable simple field
String fType = f.getType();
List<Field> fields = f.getFields();
if (fType != null) {
itemType = (f.getMaxOccurrences() > 1) ? ItemType.ARRAY : ItemType.PLAIN;
if (Field.CUSTOM_FORM.equals(fType)) {
itemType = ItemType.CUSTOM_FORM;
}
} else if (!fields.isEmpty()) {
itemType = ItemType.FORM;
} else {
throw new UnsupportedOperationException(String.valueOf(f));
}
return itemType;
}
public DynamicForm createNestedForm(Field f, String lang) {
List<Field> fields = f.getFields();
ArrayList<FormItem> formItems = new ArrayList<FormItem>(fields.size());
for (Field child : fields) {
FormItem formItem = createItem(child, lang);
if (formItem != null) {
formItems.add(formItem);
}
}
DynamicForm df = createDefaultForm();
addFormItems(df, formItems);
return df;
}
private RepeatableFormItem createNestedCustomFormItem(final Field f, final String lang) {
RepeatableFormItem rfi = new RepeatableFormItem(f, new FormWidgetFactory() {
@Override
public DynamicForm create() {
throw new UnsupportedOperationException();
}
@Override
public FormWidget createFormWidget(Field formField) {
DynamicForm df = createNestedForm(f, lang);
ValuesManager vm = new ValuesManager();
vm.addMember(df);
return customizeNestedForm(new FormWidget(df, vm), f);
}
});
oneRow(rfi);
return rfi;
}
/** Implement to attach special stuff to the custom form. */
protected FormWidget customizeNestedForm(final FormWidget fw, Field f) {
return fw;
}
public TextItem getTextFormItem(Field f, String lang) {
TextItem item = new TextItem(f.getName(), f.getTitle(activeLocale));
item.setLength(f.getLength() != null ? f.getLength() : defaultTextLength);
item.setWidth(defaultWidth);
item.setReadOnlyDisplay(ReadOnlyDisplayAppearance.STATIC);
item.setDefaultValue(f.getDefaultValue());
return item;
}
public TextAreaItem getTextAreaFormItem(Field f, String lang) {
TextAreaItem item = new TextAreaItem(f.getName(), f.getTitle(activeLocale));
item.setLength(f.getLength());
item.setWidth(defaultWidth);
return item;
}
public DateItem getDateFormItem(Field f, String lang) {
DateItem item = new DateItem(f.getName(), f.getTitle(activeLocale));
item.setDateFormatter(DateDisplayFormat.TOEUROPEANSHORTDATE);
item.setUseTextField(true);
// item.setEnforceDate(true);
return item;
}
public TextItem getDateYearFormItem(Field f, String lang) {
TextItem item = getTextFormItem(f, lang);
item.setWidth("150");
DateEditorValue yearEditorValue = DateEditorValue.gYear();
item.setEditorValueFormatter(yearEditorValue);
item.setEditorValueParser(yearEditorValue);
return item;
}
public ComboBoxItem getComboBoxItem(Field f, String lang) {
ComboBoxItem item = new ComboBoxItem(f.getName(), f.getTitle(lang));
if (f.getOptionDataSource() != null) {
setOptionDataSource(item, f, lang);
} else {
item.setValueMap(f.getValueMap());
}
item.setDefaultValue(f.getDefaultValue());
return item;
}
public SelectItem getSelectItem(Field f, String lang) {
SelectItem item = new SelectItem(f.getName(), f.getTitle(lang));
if (f.getOptionDataSource() != null) {
setOptionDataSource(item, f, lang);
} else {
item.setValueMap(f.getValueMap());
}
item.setDefaultValue(f.getDefaultValue());
return item;
}
private void setOptionDataSource(FormItem item, Field f, String lang) {
Field optionField = f.getOptionDataSource();
DataSource ds = ValueMapDataSource.getInstance().getOptionDataSource(optionField.getName());
item.setValueField(f.getOptionValueField()[0]);
item.setOptionDataSource(ds);
setPickListValueMapping(item, f);
Integer pickListWidth = getWidthAsInteger(optionField.getWidth());
if (item instanceof SelectItem) {
SelectItem selectItem = (SelectItem) item;
selectItem.setPickListFields(getPickListFields(optionField, lang));
if (pickListWidth != null) {
selectItem.setPickListWidth(pickListWidth);
}
} else if (item instanceof ComboBoxItem) {
ComboBoxItem cbi = (ComboBoxItem) item;
cbi.setPickListFields(getPickListFields(optionField, lang));
if (pickListWidth != null) {
cbi.setPickListWidth(pickListWidth);
}
}
}
private static ListGridField[] getPickListFields(Field optionField, String lang) {
List<Field> columns = optionField.getFields();
ListGridField[] listFields = new ListGridField[columns.size()];
int i = 0;
for (Field field : optionField.getFields()) {
listFields[i++] = new ListGridField(field.getName(), field.getTitle(lang));
}
return listFields;
}
/**
* Listens to item value changes and and propagate selection to sibling fields
* according to Field.getOptionValueFieldMap and Field.getOptionValueField.
* @param item select or combo
* @param field field with option data source
*/
private void setPickListValueMapping(FormItem item, Field field) {
final Map<String, String> nameMap = field.getOptionValueFieldMap();
final String[] valueFields = field.getOptionValueField();
if (nameMap == null && valueFields.length == 1) {
return ;
}
item.addChangedHandler(new ChangedHandler() {
@Override
public void onChanged(ChangedEvent event) {
FormItem item = event.getItem();
ListGridRecord selectedRecord = null;
if (item instanceof SelectItem) {
SelectItem selectItem = (SelectItem) item;
selectedRecord = selectItem.getSelectedRecord();
} else if (item instanceof ComboBoxItem) {
ComboBoxItem cbi = (ComboBoxItem) item;
selectedRecord = cbi.getSelectedRecord();
}
if (selectedRecord != null) {
if (nameMap != null) {
for (Entry<String, String> entry : nameMap.entrySet()) {
String pickName = entry.getKey();
String fieldName = entry.getValue();
if (item.getName().equals(fieldName)) {
continue;
}
String value = selectedRecord.getAttribute(pickName);
ValuesManager vm = item.getForm().getValuesManager();
setFieldValue(vm, fieldName, value);
}
} else {
for (String fieldName : valueFields) {
if (item.getName().equals(fieldName)) {
continue;
}
String value = selectedRecord.getAttribute(fieldName);
ValuesManager vm = item.getForm().getValuesManager();
vm.setValue(fieldName, value);
}
}
}
}
});
}
/**
* Sets a value to field of the values manager.
* @param vm values manager
* @param fieldName field name or relative path to nested field; '/' as a separator
* @param value value to set
*/
private void setFieldValue(ValuesManager vm, String fieldName, String value) {
String[] path = fieldName.split("/");
// ClientUtils.severe(LOG, "#setValue: %s, %s, %s", fieldName, path.length, value);
if (path.length <= 1) {
vm.setValue(fieldName, value);
} else {
int index = 0;
Object vmValue = vm.getValue(path[index]);
Record vmValueRecord = fillValueForPath(path, 0, vmValue, value);
if (vmValueRecord != null) {
// ClientUtils.warning(LOG, "## vm.setValue %s", path[index]);
vm.setValue(path[index], vmValueRecord);
}
}
}
/**
* Fills a new value of the nested field. It traverses the path of field names
* to the leaf recursively.
*
* @param path path to field; '/' as a separator
* @param pathIndex field name index in path to update
* @param pathValue set of attributes of selected field; see pathIndex
* @param value value to set
* @return the record with updated attributes or {@code null}
*/
private Record fillValueForPath(String[] path, int pathIndex, Object pathValue, String value) {
Record pathRecord = null;
// ClientUtils.warning(LOG, "##setValue: %s, %s\n%s",
// path[pathIndex], ClientUtils.safeGetClass(pathValue), ClientUtils.dump(pathValue));
// Record[], RecordList, Object[]??
if (pathValue instanceof JavaScriptObject) {
JavaScriptObject jso = (JavaScriptObject) pathValue;
// ClientUtils.warning(LOG, "## isArray %s", JSOHelper.isArray(jso));
if (JSOHelper.isArray(jso)) {
// RecordList ??
} else {
pathRecord = new Record(jso);
// ClientUtils.warning(LOG, "## vmValueRecord.setAttribute %s", pathIndex + 2 == path.length);
// is .../field1/field2 leaf?
if (pathIndex + 2 == path.length) {
// ClientUtils.warning(LOG, "## vm.setValue %s", path[pathIndex + 1]);
pathRecord.setAttribute(path[pathIndex + 1], value);
} else {
Object nextValueInPath = pathRecord.getAttributeAsObject(path[pathIndex + 1]);
// recursion
Record newValueForPath = fillValueForPath(path, pathIndex + 1, nextValueInPath, value);
if (newValueForPath != null) {
pathRecord.setAttribute(path[pathIndex + 1], newValueForPath);
}
}
}
} else if (pathValue == null) {
// no value yet; let's create new values for remaining path
pathRecord = new Record();
// is .../field1/field2 leaf?
if (pathIndex + 2 == path.length) {
// ClientUtils.warning(LOG, "## vm.setValue %s", path[pathIndex + 1]);
pathRecord.setAttribute(path[pathIndex + 1], value);
} else {
Record newValueForPath = fillValueForPath(path, pathIndex + 1, null, value);
if (newValueForPath != null) {
pathRecord.setAttribute(path[pathIndex + 1], newValueForPath);
} else {
// unknown new value, do not create anything
pathRecord = null;
}
}
}
return pathRecord;
}
protected static Integer getWidthAsInteger(String width) {
if (width != null) {
try {
return Integer.parseInt(width);
} catch (NumberFormatException ex) {
// no-op
}
}
return null;
}
public RadioGroupItem getRadioGroupItem(Field f, String lang) {
RadioGroupItem item = new RadioGroupItem(f.getName(), f.getTitle(lang));
item.setVertical(false);
item.setValueMap(f.getValueMap());
item.setWrap(false);
item.setWrapTitle(false);
return item;
}
/**
* Adds common properties to simple {@link #getFormItem items}.
*/
public FormItem customizeFormItem(FormItem item, Field f) {
if (item.getTitle() == null) {
item.setShowTitle(false);
}
item.setRequired(f.getRequired());
item.setPrompt(f.getHint(activeLocale));
item.setHoverWidth(defaultHoverWidth);
if (f.getHidden() != null && f.getHidden()) {
item.setVisible(false);
}
String width = f.getWidth();
if (width != null) {
item.setWidth(width);
}
if (f.getHeight() != null) {
item.setHeight(f.getHeight());
}
if (f.getReadOnly() != null && f.getReadOnly()) {
item.setCanEdit(!f.getReadOnly());
}
return item;
}
public FormItem getFormItem(Field f, String lang) {
FormItem formItem;
String type = f.getType();
if (Field.TEXT.equals(type)) {
formItem = getTextFormItem(f, lang);
} else if (Field.TEXTAREA.equals(type)) {
formItem = getTextAreaFormItem(f, lang);
} else if (Field.G_YEAR.equals(type)) {
formItem = getDateYearFormItem(f, lang);
} else if (Field.DATE.equals(type)) {
formItem = getDateFormItem(f, lang);
} else if (Field.COMBO.equals(type)) {
formItem = getComboBoxItem(f, lang);
} else if (Field.SELECT.equals(type)) {
formItem = getSelectItem(f, lang);
} else if (Field.RADIOGROUP.equals(type)) {
formItem = getRadioGroupItem(f, lang);
} else { // fallback
formItem = getTextFormItem(f, lang);
}
return formItem;
}
public void oneRow(FormItem fi) {
fi.setEndRow(true);
fi.setStartRow(true);
fi.setColSpan("*");
}
public RepeatableFormItem createNestedFormItem(final Field f, final String lang) {
RepeatableFormItem rfi = new RepeatableFormItem(f, new CustomFormFactory() {
@Override
public DynamicForm create() {
return createNestedForm(f, lang);
}
});
rfi.setPrompt(f.getHint(lang));
oneRow(rfi);
return rfi;
}
private RepeatableFormItem createFormItem(Field f, final DynamicForm df) {
RepeatableFormItem rfi = new RepeatableFormItem(f, new CustomFormFactory() {
@Override
public DynamicForm create() {
return df;
}
});
oneRow(rfi);
return rfi;
}
public DynamicForm createDefaultForm() {
AbstractModelForm df = new AbstractModelForm() {};
df.setBrowserSpellCheck(false);
df.setTitleOrientation(TitleOrientation.TOP);
df.setNumCols(1);
df.setWrapItemTitles(false);
df.setWidth100();
df.setHoverWrap(false);
df.setItemHoverWidth(defaultHoverWidth);
df.setHoverWidth(defaultHoverWidth);
return df;
}
public static void addFormItems(DynamicForm df, List<? extends FormItem> t) {
FormItem[] items = t.toArray(new FormItem[t.size()]);
df.setFields(items);
}
public static class DateEditorValue implements FormItemValueFormatter, FormItemValueParser {
private static final DateEditorValue GYEAR = new DateEditorValue(PredefinedFormat.YEAR);
private static final DateEditorValue DATE_VALUE = new DateEditorValue(PredefinedFormat.DATE_SHORT);
public static DateEditorValue date() {
return DATE_VALUE;
}
public static DateEditorValue gYear() {
return GYEAR;
}
private static final DateTimeFormat ISO_FORMAT = DateTimeFormat.getFormat(PredefinedFormat.ISO_8601);
private final DateTimeFormat displayFormat;
public DateEditorValue(PredefinedFormat displayFormat) {
this.displayFormat = DateTimeFormat.getFormat(displayFormat);
}
@Override
public String formatValue(Object value, Record record, DynamicForm form, FormItem item) {
// ClientUtils.severe(LOG, "format: class: %s, value: %s", ClientUtils.safeGetClass(value), value);
if (value == null) {
return null;
}
try {
Date date = value instanceof Date
? (Date) value : ISO_FORMAT.parse((String) value);
return displayFormat.format(date);
} catch (IllegalArgumentException ex) {
String toString = String.valueOf(value);
LOG.log(Level.WARNING, toString, ex);
return toString;
}
}
@Override
public Object parseValue(String value, DynamicForm form, FormItem item) {
// ClientUtils.severe(LOG, "parse: value: %s", value);
Object result = null;
if (value != null && !value.isEmpty()) {
try {
Date date = displayFormat.parse(value);
result = ISO_FORMAT.format(date);
} catch (IllegalArgumentException ex) {
String toString = String.valueOf(value);
LOG.log(Level.WARNING, toString, ex);
result = null;
}
}
return result;
}
}
}