/*
* Copyright (C) 2012 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.mods;
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.data.RecordList;
import com.smartgwt.client.i18n.SmartGwtMessages;
import com.smartgwt.client.util.JSOHelper;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.ValuesManager;
import com.smartgwt.client.widgets.form.fields.CanvasItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.events.FormItemInitHandler;
import com.smartgwt.client.widgets.form.fields.events.ShowValueEvent;
import com.smartgwt.client.widgets.form.fields.events.ShowValueHandler;
import com.smartgwt.client.widgets.form.validator.Validator;
import cz.cas.lib.proarc.webapp.client.ClientUtils;
import cz.cas.lib.proarc.webapp.client.ds.LanguagesDataSource;
import cz.cas.lib.proarc.webapp.client.widget.mods.event.ListChangedEvent;
import cz.cas.lib.proarc.webapp.client.widget.mods.event.ListChangedHandler;
import cz.cas.lib.proarc.webapp.shared.form.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Repeatable dynamic form item. Set {@link #setDataSource(com.smartgwt.client.data.DataSource)
* custom DataSource} to use all its data field. Otherwise set
* {@link CustomFormFactory } or {@link FormWidgetFactory} to define own logic.
*
* @author Jan Pokorsky
*/
public final class RepeatableFormItem extends CanvasItem {
public static final String ATTR_PROFILE = "proarc.profile.form.field";
private static final Logger LOG = Logger.getLogger(RepeatableFormItem.class.getName());
private DataSource dataSource;
private DynamicForm formPrototype;
private CustomFormFactory formFactory;
private String width = "1"; // default width is autoWidth
/** Error messages related to all repetitions of the item. */
private HashMap<Object, String> validationMessages;
public RepeatableFormItem(String name, String title) {
this(name, title, null);
}
public RepeatableFormItem(Field profile, CustomFormFactory formFactory) {
this(profile, profile.getName(), profile.getTitle(LanguagesDataSource.activeLocale()), formFactory);
}
public RepeatableFormItem(String name, String title, CustomFormFactory formFactory) {
this(null, name, title, formFactory);
}
private RepeatableFormItem(Field profile, String name, String title, CustomFormFactory formFactory) {
super(name, title);
if (profile != null) {
setProfile(profile);
}
this.formFactory = formFactory;
// setStartRow(false);
// setEndRow(false);
setShowTitle(false);
setCanFocus(true);
// show errors for nested data fields not for enclosing form
setShowErrorIcon(false);
setValidators();
setAutoWidth();
setShouldSaveValue(true);
setInitHandler(new FormItemInitHandler() {
@Override
public void onInit(FormItem item) {
RepeatableForm editor = new RepeatableForm((RepeatableFormItem) item);
Object value = item.getValue();
if (LOG.isLoggable(Level.FINE)) {
ClientUtils.fine(LOG, "## onInit: %s, dump: %s", value, ClientUtils.dump(value));
}
setData(editor, value);
editor.addListChangedHandler(new ListChangedHandler() {
@Override
public void onListChanged(ListChangedEvent event) {
RepeatableForm editor = (RepeatableForm) event.getSource();
CanvasItem canvasItem = editor.getCanvasItem();
storeValue(editor, canvasItem);
}
});
setCanvas(editor);
addShowValueHandler(new ShowValueHandler() {
@Override
public void onShowValue(ShowValueEvent event) {
RepeatableForm editor = (RepeatableForm) event.getItem().getCanvas();
if (editor != null) {
boolean expectSimpleArray = isSimpleArrayItemField(event.getItem());
Object dataValue = event.getDataValue();
if (dataValue instanceof JavaScriptObject) {
JavaScriptObject jso = (JavaScriptObject) dataValue;
// ClientUtils.warning(LOG, "## onShowValue: name: %s, source: %s, class: %s,"
// + "isArray: %s, isJSO: %s, dump: %s",
// event.getItem().getName(), event.getSource(), ClientUtils.safeGetClass(dataValue),
// JSOHelper.isArray(jso), JSOHelper.isJSO(jso), ClientUtils.dump(dataValue));
if (expectSimpleArray) {
setData(editor, simpleArrayAsRecordList(jso, event.getItem().getName()));
return ;
} else if (!JSOHelper.isArray(jso)) {
Record dataAsRecord = resolveRecordValues(new Record(jso), event.getItem());
setData(editor, new RecordList(new Record[] {dataAsRecord}));
return ;
}
}
// expect array of records
// ClientUtils.severe(LOG, "## onShowValue: name: %s, class: %s, dump: %s",
// event.getItem().getName(), ClientUtils.safeGetClass(dataValue), ClientUtils.dump(dataValue));
dataValue = event.getDataValueAsRecordList();
if (LOG.isLoggable(Level.FINE)) {
ClientUtils.fine(LOG, "## onShowValue: name: %s, source: %s, dump: %s",
event.getItem().getName(), event.getSource(), ClientUtils.dump(dataValue));
}
setData(editor, dataValue);
}
}
});
}
});
}
private static boolean isSimpleArrayItemField(FormItem item) {
Field profile = getProfile(item);
if (profile != null && profile.getType() != null) { // no inner form; just array
if (!Field.CUSTOM_FORM.equals(profile.getType()) && profile.getMaxOccurrences() > 1) {
return true;
}
}
return false;
}
private static RecordList simpleArrayAsRecordList(JavaScriptObject jso, String name) {
RecordList result = new RecordList();
if (JSOHelper.isArray(jso)) {
Object[] values = JSOHelper.convertToArray(jso);
for (Object value : values) {
Record r = new Record();
r.setAttribute(name, value);
result.add(r);
}
} else {
// single value?
}
return result;
}
/**
* Replace string values of record attributes with types declared by item children.
* This is necessary as declarative forms do not use DataSource stuff.
* @param record record to scan for attributes
* @param item item with possible profile
* @return resolved record
*/
private static Record resolveRecordValues(Record record, FormItem item) {
Field f = getProfile(item);
if (f != null) {
for (Field field : f.getFields()) {
String fType = field.getType();
if ("date".equals(fType) || "datetime".equals(fType)) {
// parses ISO dateTime to Date; otherwise DateItem cannot recognize the value!
Object value = record.getAttributeAsObject(field.getName());
if (!(value instanceof String)) {
continue;
}
String sd = (String) value;
// ClientUtils.severe(LOG, "name: %s, is date, %s", field.getName(), sd);
// Date d = DateTimeFormat.getFormat(PredefinedFormat.ISO_8601).parse("1994-11-05T13:15:30Z");
try {
Date d = DateTimeFormat.getFormat(PredefinedFormat.ISO_8601).parse(sd);
record.setAttribute(field.getName(), d);
} catch (IllegalArgumentException ex) {
LOG.log(Level.WARNING, sd, ex);
}
}
}
}
return record;
}
public void setDataSource(DataSource ds) {
this.dataSource = ds;
}
public void setFormPrototype(DynamicForm formPrototype) {
this.formPrototype = formPrototype;
}
public CustomFormFactory getFormFactory() {
if (formFactory == null) {
formFactory = new DefaultCustomForm(formPrototype, dataSource);
}
return formFactory;
}
public void setFormFactory(CustomFormFactory factory) {
this.formFactory = factory;
}
static Field getProfile(FormItem item) {
return (Field) item.getAttributeAsObject(ATTR_PROFILE);
}
public Field getProfile() {
return (Field) getAttributeAsObject(ATTR_PROFILE);
}
public void setProfile(Field profile) {
setAttribute(ATTR_PROFILE, profile);
}
public int getMaxOccurrences() {
Integer maxOccurrences = null;
Field profile = getProfile();
if (profile != null) {
// prefer profile if available
maxOccurrences = profile.getMaxOccurrences();
}
// read attribute?
return maxOccurrences == null || maxOccurrences < 1 ? 5 : maxOccurrences;
}
@Override
public void setWidth(String width) {
this.width = width;
super.setWidth(width);
}
@Override
public void setWidth(int width) {
this.width = String.valueOf(width);
super.setWidth(width);
}
// @Override
// public int getWidth() {
// return super.getWidth();
// }
public String getWidthAsString() {
String width = null;
Field profile = getProfile();
if (profile != null) {
width = profile.getWidth();
}
if (width == null) {
width = this.width;
}
// Do not try super.getWidth to initialize RepeatableForm as it will
// throw exceptions!
return width;
}
// public boolean isAutoWidth(String width) {
// return width != null && ("*".equals(width) || "100%".equals(width));
// }
public boolean isAutoWidth() {
String width = getWidthAsString();
// return width != null && ("*".equals(width) || "100%".equals(width));
return width != null && "1".equals(width);
}
public boolean isWidth100() {
String width = getWidthAsString();
return width != null && ("*".equals(width) || "100%".equals(width));
}
public void setAutoWidth() {
setWidth(1);
}
/**
* Puts default validator in front of the passed ones to validate inner forms.
* @param validators validators
*/
@Override
public void setValidators(Validator... validators) {
Validator[] wrapped;
if (validators != null && validators.length > 0) {
wrapped = new Validator[validators.length + 1];
System.arraycopy(validators, 0, wrapped, 1, validators.length);
} else {
wrapped = new Validator[1];
}
wrapped[0] = new DefaultValidator();
super.setValidators(wrapped);
}
/**
* Validates fields of nested forms.
* @return valid or not
*/
public boolean validateInnerForms(boolean showErrors) {
RepeatableForm editor = (RepeatableForm) getCanvas();
boolean valid = true;
if (editor != null) {
// ClientUtils.severe(LOG, "validateInnerForms: field.name: %s, JSO: %s",
// getName(), ClientUtils.dump(editor.getDataAsRecordList().getJsObj()));
valid &= editor.validate(showErrors);
// call storeValue to propagate values changed by validators
storeValue(editor, this);
}
return valid;
}
@Override
public Boolean validate() {
Boolean validate = super.validate();
RepeatableForm editor = (RepeatableForm) getCanvas();
storeValue(editor, this);
return validate;
}
private static void storeValue(RepeatableForm editor, CanvasItem canvasItem) {
if (editor != null) {
RecordList dataAsRecordList = editor.getDataAsRecordList();
// ClientUtils.severe(LOG, "storeValue: field.name: %s, class: %s, JSO: %s",
// canvasItem.getName(), ClientUtils.safeGetClass(dataAsRecordList),
// ClientUtils.dump(dataAsRecordList.getJsObj()));
if (isSimpleArrayItemField(canvasItem)) {
Object[] values = new Object[dataAsRecordList.getLength()];
String name = canvasItem.getName();
for (int i = 0; i < values.length; i++) {
values[i] = dataAsRecordList.get(i).getAttributeAsObject(name);
}
// XXX check empty array?
canvasItem.storeValue(JSOHelper.arrayConvert(values));
return ;
}
// duplicate the RecordList instance to rewrite the CanvasItem cache
// and propagate new list values to enclosing forms
dataAsRecordList = new RecordList(dataAsRecordList.duplicate());
canvasItem.storeValue(dataAsRecordList);
}
}
public List<Map<Object, Object>> getErrorMap() {
RepeatableForm editor = (RepeatableForm) getCanvas();
return editor.getErrors();
}
/**
* Shows validation errors related to all item repetitions. It expects
* {@code setShowErrorIcon(false)}.
*/
public void showErrors() {
RepeatableForm editor = (RepeatableForm) getCanvas();
if (editor != null) {
Collection<String> errors = validationMessages != null
? validationMessages.values()
: null;
editor.showErrors(errors);
}
}
/**
* Adds a validation error message. It works around missing access
* to validation results.
* @param key key to identify the {@link Validator}
* @param error message
*/
public void addValidationError(Object key, String error) {
if (validationMessages == null) {
validationMessages = new HashMap<Object, String>();
}
validationMessages.put(key, error);
}
public void clearErrors(boolean show) {
validationMessages = null;
RepeatableForm editor = (RepeatableForm) getCanvas();
if (editor != null) {
editor.clearErrors(show);
}
}
/**
* Helps to show error messages related to all repetitions of the item
* as no repetition contains given value.
* It should be called by {@link DynamicForm#showErrors() } implementation.
*
* @param repeateableItemContainer container holding repeatable form items as members
* @see #addValidationError
*/
public static void showErrors(DynamicForm repeateableItemContainer) {
for (FormItem formItem : repeateableItemContainer.getFields()) {
if (formItem instanceof RepeatableFormItem) {
((RepeatableFormItem) formItem).showErrors();
}
}
}
public static void clearErrors(DynamicForm repeateableItemContainer, boolean show) {
// It should help to draw inner form errors properly.
for (FormItem formItem : repeateableItemContainer.getFields()) {
if (formItem instanceof RepeatableFormItem) {
((RepeatableFormItem) formItem).clearErrors(show);
}
}
}
private static void setData(RepeatableForm editor, Object value) {
if (value == null || value instanceof Record[]) {
editor.setData((Record[]) value);
} else if (value instanceof RecordList) {
editor.setData((RecordList) value);
} else {
String msg = "";
if (value instanceof JavaScriptObject) {
msg = ClientUtils.dump((JavaScriptObject) value);
}
throw new IllegalStateException("unsupported value type: " + value.getClass() + ", dump: \n" + msg);
}
}
/**
* Allows custom implementation of a simple repeatable form.
*/
public interface CustomFormFactory {
DynamicForm create();
}
/**
* Allows custom implementation of repeatable form.
*/
public interface FormWidgetFactory extends CustomFormFactory {
FormWidget createFormWidget(Field formField);
}
/**
* Binds widget and the values manager.
*/
public static final class FormWidget {
private Canvas widget;
private ValuesManager values;
private ArrayList<FormWidgetListener> listeners = new ArrayList<FormWidgetListener>();
public FormWidget(Canvas widget, ValuesManager values) {
this.widget = widget;
this.values = values;
}
public Canvas getWidget() {
return widget;
}
public ValuesManager getValues() {
return values;
}
public void addFormWidgetListener(FormWidgetListener l) {
this.listeners.add(l);
}
void fireDataLoad() {
for (FormWidgetListener l : listeners) {
l.onDataLoad();
}
}
}
public interface FormWidgetListener {
/**
* Notifies about loaded (fetch or set) data. For user changes use
* {@link com.smartgwt.client.widgets.form.fields.events.ChangedHandler}
*/
void onDataLoad();
}
private static final class DefaultCustomForm implements CustomFormFactory {
private final DynamicForm formPrototype;
private final DataSource dataSource;
public DefaultCustomForm(DynamicForm formPrototype, DataSource dataSource) {
this.formPrototype = formPrototype != null ? formPrototype : new DynamicForm();
this.dataSource = dataSource;
}
@Override
public DynamicForm create() {
final DynamicForm form = new DynamicForm();
form.setNumCols(formPrototype.getNumCols());
form.setDataSource(dataSource);
if (formPrototype.getUseAllDataSourceFields()) {
form.setUseAllDataSourceFields(true);
}
return form;
}
}
/**
* Validates inner forms.
*/
private static final class DefaultValidator extends RepeatableItemValidator {
private final SmartGwtMessages i18SmartGwt = ClientUtils.createSmartGwtMessages();
@Override
protected boolean condition(RecordList recordList) {
RepeatableFormItem rfItem = (RepeatableFormItem) formItem;
boolean innerFormValid = rfItem.validateInnerForms(true);
if (innerFormValid) {
Object newValue = rfItem.getValue();
setResultingValue(newValue);
return conditionRequired(recordList);
} else {
return false;
}
}
private boolean conditionRequired(RecordList recordList) {
boolean valid = true;
Boolean required = formItem.getRequired();
if (required != null && required) {
boolean isEmpty = recordList == null || recordList.isEmpty();
if (isEmpty) {
setErrorMessage(i18SmartGwt.validator_requiredField());
valid = false;
}
}
return valid;
}
}
}