package com.constellio.app.ui.framework.components;
import com.constellio.app.ui.handlers.OnEnterKeyHandler;
import com.constellio.app.ui.util.MessageUtils;
import com.constellio.model.frameworks.validation.ValidationError;
import com.constellio.model.frameworks.validation.ValidationErrors;
import com.constellio.model.frameworks.validation.ValidationException;
import com.vaadin.data.Buffered.SourceException;
import com.vaadin.data.Item;
import com.vaadin.data.Validator;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.util.BeanItem;
import com.vaadin.data.validator.AbstractValidator;
import com.vaadin.server.Page;
import com.vaadin.server.Resource;
import com.vaadin.ui.*;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.themes.ValoTheme;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*;
import static com.constellio.app.ui.i18n.i18n.$;
@SuppressWarnings("serial")
public abstract class BaseForm<T> extends CustomComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseForm.class);
public static final String BASE_FORM = "base-form";
public static final String STYLE_FIELD = "base-form-field";
public static final String BUTTONS_LAYOUT = "base-form-buttons-layout";
public static final String SAVE_BUTTON = "base-form-save";
public static final String CANCEL_BUTTON = "base-form_cancel";
protected T viewObject;
protected Item item;
protected VerticalLayout formLayout;
protected HorizontalLayout buttonsLayout;
protected Button saveButton;
protected Button cancelButton;
protected List<Field<?>> fields = new ArrayList<Field<?>>();
protected FieldGroup fieldGroup;
protected TabSheet tabSheet;
private Map<String, VerticalLayout> tabs = new HashMap<>();
private boolean useTabSheet;
public BaseForm(final T viewObject, Serializable objectWithMemberFields, Field<?>... fields) {
this(viewObject, new MemberFieldBinder(objectWithMemberFields), fields);
}
public BaseForm(final T viewObject, List<FieldAndPropertyId> fieldsAndPropertyIds) {
this(viewObject, new FieldAndPropertyIdBinder(fieldsAndPropertyIds), toFields(fieldsAndPropertyIds));
}
private BaseForm(final T viewObject, FieldBinder binder, Field<?>... fields) {
super();
this.viewObject = viewObject;
for (Field<?> field : fields) {
this.fields.add(field);
}
setSizeFull();
addStyleName(BASE_FORM);
item = newItem(viewObject);
formLayout = new VerticalLayout();
formLayout.setSpacing(true);
fieldGroup = new FieldGroup(item) {
@Override
protected void configureField(Field<?> field) {
if (field instanceof AbstractField) {
AbstractField<?> abstractField = (AbstractField<?>) field;
abstractField.setValidationVisible(false);
}
field.setBuffered(isBuffered());
// Do not alter the readOnly and enabled states of the field
}
};
// fieldGroup.setBuffered(false);
binder.bind(fieldGroup);
tabSheet = new TabSheet();
// First pass to see if all the fields have the same tab status
boolean tabFound = false;
for (Field<?> field : fields) {
Object propertyId = fieldGroup.getPropertyId(field);
if (StringUtils.isNotBlank(getTabCaption(field, propertyId))) {
tabFound = true;
break;
}
}
if (tabFound) {
useTabSheet = true;
}
boolean firstField = true;
for (Field<?> field : fields) {
if (firstField) {
field.focus();
firstField = false;
}
field.addStyleName(STYLE_FIELD);
OnEnterKeyHandler onEnterHandler = new OnEnterKeyHandler() {
@Override
public void onEnterKeyPressed() {
trySave();
}
};
if (field instanceof TextField) {
onEnterHandler.installOn((TextField) field);
} else if (field instanceof DateField) {
onEnterHandler.installOn((DateField) field);
} else if (field instanceof ComboBox) {
onEnterHandler.installOn((ComboBox) field);
}
addToDefaultLayoutOrTabSheet(field);
}
buttonsLayout = new HorizontalLayout();
buttonsLayout.addStyleName(BUTTONS_LAYOUT);
buttonsLayout.setSpacing(true);
saveButton = new Button(getSaveButtonCaption());
saveButton.addStyleName(SAVE_BUTTON);
saveButton.addStyleName(ValoTheme.BUTTON_PRIMARY);
saveButton.addClickListener(new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
trySave();
}
});
cancelButton = new Button(getCancelButtonCaption());
cancelButton.addStyleName(CANCEL_BUTTON);
cancelButton.addClickListener(new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
cancelButtonClick(viewObject);
}
});
setCompositionRoot(formLayout);
if (tabSheet.iterator().hasNext()) {
formLayout.addComponent(tabSheet);
}
formLayout.addComponent(buttonsLayout);
buttonsLayout.addComponents(saveButton, cancelButton);
}
protected String getSaveButtonCaption() {
return $("save");
}
protected String getCancelButtonCaption() {
return $("cancel");
}
private void addToDefaultLayoutOrTabSheet(Field<?> field) {
VerticalLayout fieldLayout;
if (useTabSheet) {
Object propertyId = fieldGroup.getPropertyId(field);
String groupLabel = getTabCaption(field, propertyId);
String tabCaption;
if (StringUtils.isBlank(groupLabel)) {
tabCaption = $("BaseForm.defaultTab");
} else if (!groupLabel.matches("\\w.*")) {
tabCaption = groupLabel;
} else {
tabCaption = $("BaseForm.defaultTabIcon") + " " + groupLabel;
}
Resource tabIcon = getTabIcon(tabCaption);
fieldLayout = tabs.get(tabCaption);
if (fieldLayout == null) {
fieldLayout = new VerticalLayout();
fieldLayout.setWidth("100%");
tabs.put(tabCaption, fieldLayout);
fieldLayout.setSpacing(true);
if (tabIcon != null) {
tabSheet.addTab(fieldLayout, tabCaption, tabIcon);
} else {
tabSheet.addTab(fieldLayout, tabCaption);
}
}
} else {
fieldLayout = formLayout;
}
addFieldToLayout(field, fieldLayout);
}
protected void addFieldToLayout(Field<?> field, VerticalLayout fieldLayout) {
fieldLayout.addComponent(field);
}
public void commit() {
for (Field<?> field : fieldGroup.getFields()) {
try {
field.commit();
} catch (SourceException | InvalidValueException e) {
// Ignore the error
}
}
}
/**
* If this method is overriden and doesn't return null anymore, a tab sheet will be used to display the fields.
*
* @param field The field that will be added under the tab
* @param propertyId The property id attached to the field
* @return The caption of the tab under which the field will be added
*/
protected String getTabCaption(Field<?> field, Object propertyId) {
return null;
}
protected Resource getTabIcon(String tabCaption) {
return null;
}
public List<Field<?>> getFields() {
return fields;
}
public Field<?> getField(String id) {
for (Field<?> field : fields) {
if (id.equals(field.getId())) {
return field;
}
}
return null;
}
public T getViewObject() {
return viewObject;
}
protected Item newItem(T viewObject) {
return new BeanItem<T>(viewObject);
}
final public void callTrySave() {
trySave();
}
private void trySave() {
clearBackendValidators();
for (Field<?> field : fields) {
if (field instanceof AbstractField) {
AbstractField<?> abstractField = (AbstractField<?>) field;
abstractField.setValidationVisible(true);
}
}
if (fieldGroup.isValid()) {
try {
fieldGroup.commit();
try {
saveButtonClick(viewObject);
} catch (Exception e) {
ValidationErrors errors = MessageUtils.getValidationErrors(e);
if (errors != null) {
showBackendValidationException(errors);
} else {
showErrorMessage(MessageUtils.toMessage(e));
LOGGER.warn(e.getMessage(), e);
}
}
} catch (CommitException e) {
showErrorMessage(MessageUtils.toMessage(e));
LOGGER.warn(e.getMessage(), e);
}
} else {
Field<?> firstFieldWithError = null;
StringBuilder missingRequiredFields = new StringBuilder();
for (Field<?> field : fieldGroup.getFields()) {
if (!field.isValid() && field.isRequired() && isEmptyValue(field.getValue())) {
field.setRequiredError($("requiredField"));
if(missingRequiredFields.length() != 0) {
missingRequiredFields.append("<br/>");
}
missingRequiredFields.append($("requiredFieldWithName", "\"" + field.getCaption() + "\""));
if (firstFieldWithError == null) {
firstFieldWithError = field;
}
}
}
if (firstFieldWithError != null) {
firstFieldWithError.focus();
showErrorMessage(missingRequiredFields.toString());
}
}
}
private boolean isEmptyValue(Object value) {
boolean emptyValue;
if (value == null) {
emptyValue = true;
} else if (value instanceof String) {
emptyValue = StringUtils.isBlank((String) value);
} else if (value instanceof Collection) {
emptyValue = ((Collection<?>) value).isEmpty();
} else {
emptyValue = false;
}
return emptyValue;
}
protected void showBackendValidationException(ValidationErrors validationErrors) {
Set<String> globalErrorMessages = new HashSet<String>();
for (ValidationError validationError : validationErrors.getValidationErrors()) {
String errorMessage = $(validationError);
globalErrorMessages.add(errorMessage);
}
if (!globalErrorMessages.isEmpty()) {
StringBuffer globalErrorMessagesSB = new StringBuffer();
for (String globalErrorMessage : globalErrorMessages) {
globalErrorMessage = $(globalErrorMessage);
if (globalErrorMessagesSB.length() != 0) {
globalErrorMessagesSB.append("<br />");
}
globalErrorMessagesSB.append(globalErrorMessage);
}
showErrorMessage(globalErrorMessagesSB.toString());
}
}
protected void clearBackendValidators() {
for (Field<?> field : fields) {
for (Validator validator : new ArrayList<Validator>(field.getValidators())) {
if (validator instanceof BackendValidator) {
field.removeValidator(validator);
}
}
}
}
protected void showErrorMessage(String message) {
Notification notification = new Notification(message + "<br/><br/>" + $("clickToClose"), Type.WARNING_MESSAGE);
notification.setHtmlContentAllowed(true);
notification.show(Page.getCurrent());
}
protected abstract void saveButtonClick(T viewObject)
throws ValidationException;
protected abstract void cancelButtonClick(T viewObject);
public static class FieldAndPropertyId implements Serializable {
public final Field<?> field;
public final Object propertyId;
public FieldAndPropertyId(Field<?> field, Object propertyId) {
super();
this.field = field;
this.propertyId = propertyId;
}
}
private interface FieldBinder extends Serializable {
void bind(FieldGroup fieldGroup);
}
private static class MemberFieldBinder implements FieldBinder {
private Serializable objectWithMemberFields;
public MemberFieldBinder(Serializable objectWithMemberFields) {
this.objectWithMemberFields = objectWithMemberFields;
}
public void bind(FieldGroup fieldGroup) {
fieldGroup.bindMemberFields(objectWithMemberFields);
}
}
private static class FieldAndPropertyIdBinder implements FieldBinder {
private List<FieldAndPropertyId> fieldsAndPropertyIds;
public FieldAndPropertyIdBinder(List<FieldAndPropertyId> fieldsAndPropertyIds) {
this.fieldsAndPropertyIds = fieldsAndPropertyIds;
}
public void bind(FieldGroup fieldGroup) {
for (FieldAndPropertyId fieldAndPropertyId : fieldsAndPropertyIds) {
Field<?> field = fieldAndPropertyId.field;
Object propertyId = fieldAndPropertyId.propertyId;
fieldGroup.bind(field, propertyId);
}
}
}
private static Field<?>[] toFields(List<FieldAndPropertyId> fieldsAndPropertyIds) {
Field<?>[] fields = new Field<?>[fieldsAndPropertyIds.size()];
for (int i = 0; i < fieldsAndPropertyIds.size(); i++) {
FieldAndPropertyId fieldAndPropertyId = fieldsAndPropertyIds.get(i);
fields[i] = fieldAndPropertyId.field;
}
return fields;
}
protected static class BackendValidator extends AbstractValidator<Object> {
public BackendValidator(String errorMessage) {
super(errorMessage);
}
@Override
protected boolean isValidValue(Object value) {
return false;
}
@Override
public Class<Object> getType() {
return Object.class;
}
}
}