/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Copyright (c) 2013, MPL CodeInside http://codeinside.ru
*/
package ru.codeinside.gses.webui.form;
import com.vaadin.data.Property;
import com.vaadin.data.Validator;
import com.vaadin.terminal.CompositeErrorMessage;
import com.vaadin.terminal.ErrorMessage;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.DateField;
import com.vaadin.ui.Field;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Select;
import org.apache.commons.lang.StringUtils;
import ru.codeinside.gses.activiti.FileValue;
import ru.codeinside.gses.activiti.SimpleField;
import ru.codeinside.gses.activiti.forms.FormID;
import ru.codeinside.gses.activiti.forms.api.definitions.BlockNode;
import ru.codeinside.gses.activiti.forms.api.definitions.PropertyCollection;
import ru.codeinside.gses.activiti.forms.api.definitions.PropertyNode;
import ru.codeinside.gses.activiti.forms.api.definitions.PropertyType;
import ru.codeinside.gses.activiti.forms.api.definitions.ToggleNode;
import ru.codeinside.gses.activiti.forms.api.values.PropertyValue;
import ru.codeinside.gses.activiti.ftarchive.DirectoryField;
import ru.codeinside.gses.form.FormEntry;
import ru.codeinside.gses.service.Fn;
import ru.codeinside.gses.vaadin.ScrollableForm;
import ru.codeinside.gses.webui.form.api.FieldValuesSource;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
public class GridForm extends ScrollableForm implements FormDataSource, FieldValuesSource {
public static final String REQUIRED_MESSAGE = "Обязательно к заполнению!";
final public FieldTree fieldTree;
final int colsCount;
final int valueColumn;
final GridLayout gridLayout;
final FormID formID;
SourceException bsex;
public GridForm(FormID formID, FieldTree fieldTree) {
setWriteThrough(false);
setInvalidCommitted(false);
setSizeFull();
setImmediate(true);
this.formID = formID;
this.fieldTree = fieldTree;
colsCount = fieldTree.getCols();
int rowsCount = fieldTree.root.getControlsCount();
if (rowsCount == 0) {
getLayout().addComponent(new Label("Не требуется ввод данных"));
valueColumn = 0;
gridLayout = null;
} else {
valueColumn = colsCount - (fieldTree.hasSignature ? 2 : 1);
gridLayout = new GridLayout(colsCount, rowsCount);
gridLayout.setStyleName("lined-grid");
gridLayout.setMargin(false);
gridLayout.setSpacing(false); // через css
gridLayout.setImmediate(true);
gridLayout.setSizeFull();
setLayout(gridLayout);
fieldTree.updateColumnIndex();
buildControls(fieldTree.root, 0);
buildToggle(fieldTree.propertyTree);
gridLayout.setColumnExpandRatio(valueColumn, 1f);
}
}
static FieldTree.Entry getBlock(final FieldTree.Entry entry) {
FieldTree.Entry parent = entry.parent;
return parent.items.get(parent.items.indexOf(entry) - 1);
}
static GridForm getGridForm(Button.ClickEvent event) {
Component c = event.getButton().getParent();
while (!(c instanceof GridForm)) {
c = c.getParent();
}
return (GridForm) c;
}
void updateExpandRatios() {
// обновление для того чтобы не пропадали кнопки +/-
gridLayout.requestRepaint();
requestRepaint();
}
void buildControls(final FieldTree.Entry entry, int level) {
switch (entry.type) {
case ITEM:
case BLOCK:
if (!entry.readable) break; // если поле не доступно для чтения, то не надо его отображать на форме
if (isNotBlank(entry.caption)) {
Label caption = new Label(entry.caption);
caption.setStyleName("right");
if (entry.type == FieldTree.Type.BLOCK) {
caption.addStyleName("bold");
}
caption.setWidth(300, UNITS_PIXELS);
caption.setHeight(100, UNITS_PERCENTAGE);
gridLayout.addComponent(caption, level, entry.index, valueColumn - 1, entry.index);
gridLayout.setComponentAlignment(caption, Alignment.TOP_RIGHT);
}
final Component sign = entry.sign;
if (sign != null) {
gridLayout.addComponent(sign, valueColumn + 1, entry.index);
gridLayout.setComponentAlignment(sign, Alignment.TOP_LEFT);
if (!entry.readOnly) {
entry.field.addListener(new ValueChangeListener() {
@Override
public void valueChange(Property.ValueChangeEvent event) {
entry.field.removeListener(this);
gridLayout.removeComponent(sign);
entry.sign = null;
}
});
}
}
// регистрируется в форме
addField(entry.path, entry.field);
break;
case CONTROLS:
HorizontalLayout layout = new HorizontalLayout();
layout.setImmediate(true);
layout.setSpacing(true);
layout.setMargin(false, false, true, false);
Button plus = createButton("+");
Button minus = createButton("-");
layout.addComponent(plus);
layout.addComponent(minus);
FieldTree.Entry block = getBlock(entry);
plus.addListener(new AppendAction(entry, minus));
minus.addListener(new RemoveAction(entry, plus));
if (block.field != null) {
final StringBuilder sb = new StringBuilder();
if (!isBlank(block.caption)) {
sb.append(' ')
.append('\'')
.append(block.caption)
.append('\'');
}
if (block.field.getDescription() != null) {
sb.append(' ')
.append('(')
.append(block.field.getDescription())
.append(')');
}
plus.setDescription("Добавить" + sb);
minus.setDescription("Удалить" + sb);
}
updateCloneButtons(plus, minus, block);
gridLayout.addComponent(layout, valueColumn, entry.index, valueColumn, entry.index);
break;
case CLONE:
int y = entry.index;
int dy = entry.getControlsCount() - 1;
Label cloneCaption = new Label(entry.cloneIndex + ")");
cloneCaption.setWidth(20, UNITS_PIXELS);
cloneCaption.setStyleName("right");
cloneCaption.addStyleName("bold");
gridLayout.addComponent(cloneCaption, level - 1, y, level - 1, y + dy);
gridLayout.setComponentAlignment(cloneCaption, Alignment.TOP_RIGHT);
break;
case ROOT:
break;
default:
throw new IllegalStateException("Встретился неизвестный тип поля " + entry.type);
}
if (entry.items != null) { // работаем с подчиненными полями
if (entry.type == FieldTree.Type.BLOCK) {
level++;
}
for (FieldTree.Entry child : entry.items) {
buildControls(child, level);
}
}
}
private Button createButton(String caption) {
Button button = new Button(caption);
button.setImmediate(true);
return button;
}
private void updateCloneButtons(Button plus, Button minus, FieldTree.Entry block) {
minus.setEnabled(block.cloneMin < block.cloneCount);
plus.setEnabled(block.cloneCount < block.cloneMax);
}
@Override
public void validate() throws Validator.InvalidValueException {
scrollTo(null);
for (final Object id : getItemPropertyIds()) {
final Field field = getField(id);
try {
field.validate();
} catch (Validator.InvalidValueException e) {
scrollTo(field);
throw e;
}
}
}
@Override
public void commit() throws SourceException, Validator.InvalidValueException {
bsex = null;
try {
super.commit();
} catch (SourceException e) {
bsex = e;
throw e;
}
}
@Override
public void discard() throws SourceException {
bsex = null;
try {
super.discard();
} catch (SourceException e) {
bsex = e;
throw e;
}
}
@Override
protected void attachField(final Object propertyId, final Field field) {
addToLayout(fieldTree.root.getEntry(field));
}
private void addToLayout(FieldTree.Entry entry) {
Field field = entry.field;
field.setCaption(field.isRequired() ? "" : null);
Component component = entry.underline == null ? field : entry.underline;
gridLayout.addComponent(component, valueColumn, entry.index);
gridLayout.setComponentAlignment(component, Alignment.TOP_LEFT);
}
@Override
protected void detachField(final Field field) {
gridLayout.removeComponent(field);
}
public boolean isAttachedField(final FieldTree.Entry target) {
if (target.underline != null) {
return null != gridLayout.getComponentArea(target.underline);
}
return null != gridLayout.getComponentArea(target.field);
}
private String getCaption(final Field f) {
return fieldTree.root.getEntry(f).getCaption();
}
@Override
public ErrorMessage getErrorMessage() {
final List<ErrorMessage> errors = new ArrayList<ErrorMessage>();
final ErrorMessage formError = getComponentError();
if (formError != null) {
errors.add(formError);
}
if (isValidationVisible()) {
for (final Object id : getItemPropertyIds()) {
final Field field = getField(id);
if (field instanceof AbstractComponent) {
final String caption = StringUtils.trimToEmpty(getCaption(field));
final ErrorMessage validationError = ((AbstractComponent) field).getErrorMessage();
if (validationError != null) {
errors.addAll(convertError(validationError, caption));
} else if (!field.isValid()) {
// считаем что это стандартная ошибка
errors.add(new Validator.InvalidValueException(convertErrorMessage(caption, REQUIRED_MESSAGE)));
}
}
}
}
if (bsex != null) {
errors.add(bsex);
}
if (errors.isEmpty()) {
return null;
}
return new CompositeErrorMessage(errors);
}
private List<ErrorMessage> convertError(final ErrorMessage validationError, final String caption) {
final List<ErrorMessage> converted = new ArrayList<ErrorMessage>();
convertError(validationError, caption, converted);
return converted;
}
private void convertError(ErrorMessage validationError, final String caption, List<ErrorMessage> acc) {
if (validationError instanceof Validator.EmptyValueException) {
final Validator.EmptyValueException original = (Validator.EmptyValueException) validationError;
String text = original.getMessage();
if (isBlank(text)) {
text = REQUIRED_MESSAGE;
}
acc.add(new Validator.InvalidValueException(convertErrorMessage(caption, text)));
} else if (validationError instanceof Validator.InvalidValueException) {
final Validator.InvalidValueException original = (Validator.InvalidValueException) validationError;
acc.add(new Validator.InvalidValueException(convertErrorMessage(caption, original.getMessage())));
} else if (validationError instanceof CompositeErrorMessage) {
final CompositeErrorMessage original = (CompositeErrorMessage) validationError;
final Iterator<ErrorMessage> iterator = original.iterator();
while (iterator.hasNext()) {
convertError(iterator.next(), caption, acc);
}
} else {
acc.add(validationError);
}
}
private String convertErrorMessage(final String caption, final String message) {
final StringBuilder sb = new StringBuilder();
if (!message.contains(caption)) {
sb.append(caption);
if (!caption.endsWith(":")) {
sb.append(':');
}
sb.append(' ');
}
sb.append(message);
return sb.toString();
}
private FormEntry generateFormData(FieldTree.Entry entry) {
FormEntry formEntry = null;
switch (entry.type) {
case ITEM:
case BLOCK:
if (!entry.readable || entry.hidden) {
return null;
}
formEntry = new FormEntry();
formEntry.id = entry.path;
formEntry.name = entry.caption;
formEntry.value = getUserFriendlyContent(entry);
break;
case CONTROLS:
break;
case CLONE:
formEntry = new FormEntry();
formEntry.name = Integer.toString(entry.cloneIndex) + ")";
break;
case ROOT:
formEntry = new FormEntry();
break;
default:
throw new IllegalStateException();
}
if (formEntry != null && entry.items != null) {
List<FormEntry> children = new ArrayList<FormEntry>(entry.items.size());
for (FieldTree.Entry child : entry.items) {
FormEntry childEntry = generateFormData(child);
if (childEntry != null) {
children.add(childEntry);
}
}
if (!children.isEmpty()) {
formEntry.children = children.toArray(new FormEntry[children.size()]);
}
}
return formEntry;
}
String getUserFriendlyContent(FieldTree.Entry entry) {
Field field = entry.field;
Object value;
if (field == null) {
value = null;
} else if (field instanceof SimpleField) {
value = ((SimpleField) field).getFileName();
} else {
value = field.getValue();
}
String result = null;
if (value != null) {
if (field instanceof Select) {
result = ((Select) field).getItemCaption(value);
} else if (field instanceof DirectoryField) {
result = ((DirectoryField) field).getValueCaption();
} else if (field instanceof DateField) {
result = new SimpleDateFormat(((DateField) field).getDateFormat()).format(value);
} else if (value instanceof Boolean) {
result = Boolean.TRUE.equals(value) ? "Да" : "Нет";
} else if (value instanceof FileValue) {
result = ((FileValue) value).getFileName();
} else {
result = value.toString();
}
}
return result;
}
@Override
public FormEntry createFormTree() {
return generateFormData(fieldTree.root);
}
private void buildToggle(final PropertyCollection collection) {
for (final PropertyNode node : collection.getNodes()) {
buildToggle(node);
}
}
private void buildToggle(final PropertyNode node) {
final PropertyType type = node.getPropertyType();
if (type == PropertyType.BLOCK) {
buildToggle((PropertyCollection) node);
} else if (type == PropertyType.TOGGLE || type == PropertyType.VISIBILITY_TOGGLE) {
final ToggleNode toggleDef = (ToggleNode) node;
final List<FieldTree.Entry> sources = fieldTree.getEntries(toggleDef.getToggler().getId());
for (FieldTree.Entry source : sources) {
if (source.togglers == null || !source.togglers.contains(toggleDef)) {
if (source.togglers == null) {
source.togglers = new LinkedList<ToggleNode>();
}
source.togglers.add(toggleDef);
final Field field = source.field;
if (type == PropertyType.TOGGLE) {
final MandatoryToggle toggle = new MandatoryToggle(toggleDef, source, fieldTree);
toggle.toggle(field);
field.addListener(new MandatoryChangeListener(toggle));
} else {
final VisibilityToggle toggle = new VisibilityToggle(toggleDef, source);
toggle.toggle(this, field);
field.addListener(new VisibilityChangeListener(this, toggle));
}
}
}
}
}
@Override
public Map<String, Object> getFieldValues() {
Map<String, Object> values = new LinkedHashMap<String, Object>();
fieldTree.collect(values);
return values;
}
final static class RemoveAction implements Button.ClickListener {
final FieldTree.Entry entry;
final Button plus;
RemoveAction(FieldTree.Entry entry, Button plus) {
this.entry = entry;
this.plus = plus;
}
@Override
public void buttonClick(Button.ClickEvent event) {
FieldTree.Entry block = getBlock(entry);
if (block.items != null && !block.items.isEmpty() && block.cloneCount > block.cloneMin) {
block.cloneCount--;
final FieldTree.Entry clone = block.items.remove(block.cloneCount);
GridForm gridForm = getGridForm(event);
clone.removeFields(gridForm);
int index = clone.index;
int count = clone.getControlsCount();
for (int i = 0; i < count; i++) {
gridForm.gridLayout.removeRow(index);
}
block.field.setValue(block.cloneCount);
gridForm.fieldTree.updateColumnIndex();
gridForm.updateCloneButtons(plus, event.getButton(), block);
gridForm.updateExpandRatios();
}
}
}
static class AppendAction implements Button.ClickListener {
final FieldTree.Entry controls;
private Button minus;
public AppendAction(FieldTree.Entry controls, Button minus) {
this.controls = controls;
this.minus = minus;
}
@Override
public void buttonClick(Button.ClickEvent event) {
final FieldTree.Entry block = getBlock(controls);
if (block.cloneCount < block.cloneMax) {
GridForm gridForm = getGridForm(event);
int cloneIndex = ++block.cloneCount;
String blockSuffix = block.calcSuffix();
String suffix = blockSuffix + "_" + cloneIndex;
List<PropertyValue<?>> clones = Fn.withEngine(new Fetcher(), gridForm.formID, (BlockNode) block.node, suffix);
int insertIndex = controls.index;
gridForm.fieldTree.update(clones, block, cloneIndex);
block.field.setValue(block.cloneCount);
gridForm.fieldTree.updateColumnIndex();
final FieldTree.Entry clone = block.items.get(cloneIndex - 1);
int count = clone.getControlsCount();
// вставка пустого места
for (int i = 0; i < count; i++) {
gridForm.gridLayout.insertRow(insertIndex);
}
int level = clone.getLevel();
gridForm.buildControls(clone, level);
gridForm.buildToggle(gridForm.fieldTree.propertyTree);
gridForm.updateCloneButtons(event.getButton(), minus, block);
gridForm.updateExpandRatios();
}
}
}
}