/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* * specific language governing permissions and limitations under the License.
*
*/
package uk.q3c.krail.core.ui.form;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.vaadin.data.Item;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.util.BeanItem;
import com.vaadin.ui.Field;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import uk.q3c.krail.core.data.KrailEntity;
import uk.q3c.krail.core.i18n.I18NProcessor;
import uk.q3c.krail.core.option.Option;
import uk.q3c.krail.core.option.OptionContext;
import uk.q3c.krail.core.validation.BeanValidator;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
//import com.vaadin.data.validator.BeanValidator;
/**
* * <p>
* Unfortunately the way the Vaadin implementation ({@link com.vaadin.data.fieldgroup.BeanFieldGroup}) is written makes it impossible to change the {@link
* BeanValidator} implementation. A different {@link BeanValidator} is needed to enable integration with Krail's I18N features.
*
* This class is a replacement for the Vaadin version, and for quite a few methods, is a copy of it.
* <p>
* Sub-class this class, and add fields, with I18N annotations as required. For data to be taken automatically from the bean to the form components, the
* name of the
* form component must match the field name of the bean (in Vaadin terms, this becomes the propertyId).
*
* To load data into this FieldGroup, call {@link #setBean(KrailEntity)}.
*
* To transfer data back to the entity, call {@link #commit()}
* <p>
* Created by David Sowerby on 03/02/15.
*/
public abstract class BeanFieldGroupBase<T extends KrailEntity> extends FieldGroup implements BeanFieldGroup<T>, OptionContext {
private final I18NProcessor i18NProcessor;
private final Map<Field<?>, BeanValidator<T>> defaultValidators;
private Class<T> beanType;
private Provider<BeanValidator> beanValidatorProvider;
private Option option;
@Inject
public BeanFieldGroupBase(I18NProcessor i18NProcessor, Provider<BeanValidator> beanValidatorProvider, Option option) {
this.i18NProcessor = i18NProcessor;
this.beanValidatorProvider = beanValidatorProvider;
this.option = option;
this.defaultValidators = new HashMap<>();
}
@SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
private static java.lang.reflect.Field getField(Class<?> cls, String propertyId) throws NoSuchFieldException {
if (propertyId.contains(".")) {
String[] parts = propertyId.split("\\.", 2);
// Get the type of the field in the "cls" class
java.lang.reflect.Field field1 = getField(cls, parts[0]);
// Find the rest from the sub type
return getField(field1.getType(), parts[1]);
} else {
try {
// Try to find the field directly in the given class
return cls.getDeclaredField(propertyId);
} catch (NoSuchFieldException e) {
// Try super classes until we reach Object
Class<?> superClass = cls.getSuperclass();
if (superClass != null && superClass != Object.class) {
return getField(superClass, propertyId);
} else {
throw e;
}
}
}
}
private static String getFieldName(Class<?> cls, String propertyId) throws NoSuchFieldException {
for (java.lang.reflect.Field field1 : cls.getDeclaredFields()) {
if (propertyId.equals(minifyFieldName(field1.getName()))) {
return field1.getName();
}
}
// Try super classes until we reach Object
Class<?> superClass = cls.getSuperclass();
if (superClass != null && superClass != Object.class) {
return getFieldName(superClass, propertyId);
} else {
throw new NoSuchFieldException();
}
}
@Override
@Nonnull
public Option getOption() {
return option;
}
/**
* Wraps {@code bean} in a new {@link BeanItem} instance and calls {@link #setBeanItem(BeanItem)}
*
* @param bean
*/
public void setBean(T bean) {
BeanItem item = new BeanItem(bean);
setBeanItem(item);
}
/**
* Sets the beanItem to use (and therefore the data), translates I18N annotation captions and descriptions and
* applies them to Fields
*
* @param beanItem
*/
public void setBeanItem(BeanItem<T> beanItem) {
//noinspection unchecked
if (beanType == null) {
beanType = (Class<T>) beanItem.getBean()
.getClass();
buildAndBindMemberFields(this);
i18NProcessor.translate(this);
}
setItemDataSource(beanItem);
}
@Override
protected Class<?> getPropertyType(Object propertyId) {
if (getItemDataSource() != null) {
return super.getPropertyType(propertyId);
} else {
// Data source not set so we need to figure out the type manually
/*
* toString should never really be needed as propertyId should be of
* form "fieldName" or "fieldName.subField[.subField2]" but the
* method declaration comes from parent.
*/
java.lang.reflect.Field f;
try {
f = getField(beanType, propertyId.toString());
return f.getType();
} catch (SecurityException e) {
throw new BindException("Cannot determine type of propertyId '" + propertyId + "'.", e);
} catch (NoSuchFieldException e) {
throw new BindException("Cannot determine type of propertyId '" + propertyId + "'. The propertyId was" +
" not found in " + beanType.getName(), e);
}
}
}
@Override
protected Object findPropertyId(java.lang.reflect.Field memberField) {
String fieldName = memberField.getName();
Item dataSource = getItemDataSource();
if (dataSource != null && dataSource.getItemProperty(fieldName) != null) {
return fieldName;
} else {
String minifiedFieldName = minifyFieldName(fieldName);
try {
return getFieldName(beanType, minifiedFieldName);
} catch (Exception e) {
return null;
}
}
}
/**
* Helper method for setting the data source directly using a bean. This
* method wraps the bean in a {@link BeanItem} and calls
* {@link #setItemDataSource(Item)}.
*
* @param bean
* The bean to use as data source.
*/
public void setItemDataSource(T bean) {
BeanItem<T> beanItem = new BeanItem<T>(bean);
setItemDataSource(beanItem);
}
@Override
public void bind(Field field, Object propertyId) {
ensureNestedPropertyAdded(propertyId);
super.bind(field, propertyId);
}
private void ensureNestedPropertyAdded(Object propertyId) {
if (getItemDataSource() != null) {
// The data source is set so the property must be found in the item.
// If it is not we try to add it.
try {
getItemProperty(propertyId);
} catch (BindException e) {
// Not found, try to add a nested property;
// BeanItem property ids are always strings so this is safe
getItemDataSource().addNestedProperty((String) propertyId);
}
}
}
@Override
public BeanItem<T> getItemDataSource() {
//noinspection unchecked
return (BeanItem<T>) super.getItemDataSource();
}
@Override
public void setItemDataSource(Item item) {
if (!(item instanceof BeanItem)) {
throw new RuntimeException(getClass().getSimpleName() + " only supports BeanItems as item data source");
}
super.setItemDataSource(item);
}
@Override
public Field<?> buildAndBind(String caption, Object propertyId) throws BindException {
ensureNestedPropertyAdded(propertyId);
return super.buildAndBind(caption, propertyId);
}
@Override
public void unbind(Field<?> field) throws BindException {
super.unbind(field);
BeanValidator removed = defaultValidators.remove(field);
if (removed != null) {
field.removeValidator(removed);
}
}
@Override
protected void configureField(Field<?> field) {
super.configureField(field);
// Add Bean validators if there are annotations
if (!defaultValidators.containsKey(field)) {
BeanValidator<T> validator = beanValidatorProvider.get();
validator.init(beanType, getPropertyId(field).toString());
field.addValidator(validator);
defaultValidators.put(field, validator);
}
}
}