BeanUtils.java
package fr.sii.ogham.core.util;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.NestedNullException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.sii.ogham.core.exception.util.BeanException;
import fr.sii.ogham.core.util.bean.MapBeanReadWrapper;
import fr.sii.ogham.core.util.converter.EmailAddressConverter;
import fr.sii.ogham.core.util.converter.SmsSenderConverter;
import fr.sii.ogham.email.message.EmailAddress;
import fr.sii.ogham.sms.message.Sender;
/**
* Helper class for bean management:
* <ul>
* <li>Converts an object into a map</li>
* <li>Fills a bean with values provided in a map</li>
* </ul>
* <p>
* This work can be done by several libraries. The aim of this class is to be
* able to change the implementation easily to use another library for example.
* </p>
* <p>
* For example, we could find which library is available in the classpath and
* use this library instead of forcing users to include Apache Commons BeanUtils
* library.
* </p>
*
* @author Aurélien Baudet
*
*/
public final class BeanUtils {
private static final Logger LOG = LoggerFactory.getLogger(BeanUtils.class);
static {
registerDefaultConverters();
}
/**
* Register the following converters:
* <ul>
* <li>{@link EmailAddressConverter}</li>
* <li>{@link SmsSenderConverter}</li>
* </ul>
*
* If a conversion error occurs it throws an exception.
*/
public static void registerDefaultConverters() {
// TODO: auto-detect converters in the classpath ?
// Add converter for being able to convert string address into
// EmailAddress
ConvertUtils.register(new EmailAddressConverter(), EmailAddress.class);
// Add converter for being able to convert string into
// SMS sender
ConvertUtils.register(new SmsSenderConverter(), Sender.class);
BeanUtilsBean.getInstance().getConvertUtils().register(true, false, 0);
}
/**
* <p>
* Convert a Java object into a map. Each property of the bean is added to
* the map. The key of each entry is the name of the property. The value of
* each entry is the value of the property.
*
* <p>
* There is no copy of values into a new map. Actually, the bean is wrapped
* and access to properties is done lazily. Then a special map
* implementation decorates the bean wrapper.
*
* @param bean
* the bean to convert into a map
* @return the bean as map
*/
public static Map<String, Object> convert(Object bean) {
return new MapBeanReadWrapper(bean);
}
/**
* <p>
* Fills a Java object with the provided values. The key of the map
* corresponds to the name of the property to set. The value of the map
* corresponds to the value to set on the Java object.
* </p>
* <p>
* The keys can contain '.' to set nested values.
* </p>
* Override parameter allows to indicate which source has higher priority:
* <ul>
* <li>If true, then all values provided in the map will be always set on
* the bean</li>
* <li>If false then there are two cases:
* <ul>
* <li>If the property value of the bean is null, then the value that comes
* from the map is used</li>
* <li>If the property value of the bean is not null, then this value is
* unchanged and the value in the map is not used</li>
* </ul>
* </li>
* </ul>
* Skip unknown parameter allows to indicate if execution should fail or
* not:
* <ul>
* <li>If true and a property provided in the map doesn't exist, then there
* is no failure and no change is applied to the bean</li>
* <li>If false and a property provided in the map doesn't exist, then the
* method fails immediately.</li>
* </ul>
*
* @param bean
* the bean to populate
* @param values
* the name/value pairs
* @param options
* options used to
* @throws BeanException
* when the bean couldn't be populated
*/
public static void populate(Object bean, Map<String, Object> values, Options options) throws BeanException {
for (Entry<String, Object> entry : values.entrySet()) {
populate(bean, entry, options);
}
}
/**
* <p>
* Fills a Java object with the provided value. The key of the entry
* corresponds to the name of the property to set. The value of the entry
* corresponds to the value to set on the Java object.
* </p>
* <p>
* The keys can contain '.' to set nested values.
* </p>
* Override parameter allows to indicate which source has higher priority:
* <ul>
* <li>If true, then the value provided in the entry will be always set on
* the bean</li>
* <li>If false then there are two cases:
* <ul>
* <li>If the property value of the bean is null, then the value that comes
* from the entry is used</li>
* <li>If the property value of the bean is not null, then this value is
* unchanged and the value in the entry is not used</li>
* </ul>
* </li>
* </ul>
* Skip unknown parameter allows to indicate if execution should fail or
* not:
* <ul>
* <li>If true and a property provided in the entry doesn't exist, then
* there is no failure and no change is applied to the bean</li>
* <li>If false and a property provided in the entry doesn't exist, then the
* method fails immediately.</li>
* </ul>
*
* @param bean
* the bean to populate
* @param entry
* the name/value pair
* @param options
* options used to
* @throws BeanException
* when the bean couldn't be populated
*/
public static void populate(Object bean, Entry<String, Object> entry, Options options) throws BeanException {
try {
String property = org.apache.commons.beanutils.BeanUtils.getProperty(bean, entry.getKey());
if (options.isOverride() || property == null) {
org.apache.commons.beanutils.BeanUtils.setProperty(bean, entry.getKey(), entry.getValue());
}
} catch (NestedNullException | NoSuchMethodException e) {
handleUnknown(bean, options, entry, e);
} catch (ConversionException e) {
handleConversion(bean, options, entry, e);
} catch (IllegalAccessException | InvocationTargetException e) {
handleInvocation(bean, options, entry, e);
}
}
/**
* <p>
* Fills a Java object with the provided values. The key of the map
* corresponds to the name of the property to set. The value of the map
* corresponds to the value to set on the Java object.
* </p>
* <p>
* The keys can contain '.' to set nested values.
* </p>
* It doesn't override the value of properties of the bean that are not
* null. For example, if the bean looks like:
*
* <pre>
* public class SampleBean {
* private String foo = "foo";
* private String bar = null;
*
* // ...
* // getters and setters
* // ...
* }
* </pre>
*
* If the map is:
*
* <pre>
* Map<String, Object> map = new HashMap<>();
* map.put("foo", "newfoo");
* map.put("bar", "newbar");
* </pre>
*
* Then the bean will be:
*
* <pre>
* System.out.println(bean.getFoo());
* // foo
* System.out.println(bean.getBar());
* // newbar
* </pre>
*
* <p>
* It doesn't fail if a property doesn't exist or if the new value can't be
* converted or property can't be accessed or set. The new value is just not
* set.
* </p>
*
* @param bean
* the bean to populate
* @param values
* the name/value pairs
* @throws BeanException
* when the bean couldn't be populated
*/
public static void populate(Object bean, Map<String, Object> values) throws BeanException {
populate(bean, values, new Options(false, true, true, true));
}
private static void handleUnknown(Object bean, Options options, Entry<String, Object> entry, Exception e) throws BeanException {
if (options.isSkipUnknown()) {
LOG.debug("skipping property {}: it doesn't exist or is not accessible", entry.getKey(), e);
} else {
throw new BeanException("Failed to populate bean due to unknown property", bean, e);
}
}
private static void handleConversion(Object bean, Options options, Entry<String, Object> entry, ConversionException e) throws BeanException {
if (options.isSkipConversionError()) {
LOG.debug("skipping property {}: can't convert value", entry.getKey(), e);
} else {
throw new BeanException("Failed to populate bean due to conversion error", bean, e);
}
}
private static void handleInvocation(Object bean, Options options, Entry<String, Object> entry, ReflectiveOperationException e) throws BeanException {
if (options.isSkipInvocationError()) {
LOG.debug("skipping property {}: can't set value", entry.getKey(), e);
} else {
throw new BeanException("Failed to populate bean due to invalid setter call or security retrictions", bean, e);
}
}
/**
* Populate options:
* <ul>
* <li><strong>override</strong>: If true it overrides any previously set
* value. If false it set the value only if previous value is not set
* (null)</li>
* <li><strong>skipUnknown</strong>: If true and a property doesn't exist,
* do no fail and log the error. If false and a property doesn't exist, it
* fails</li>
* <li><strong>skipConversionError</strong>: If true and a property value
* can't be set because of invalid value/type, do not fail and log the
* error. If false and a property value can't be set because of invalid
* value/type, it fails</li>
* </ul>
*
* @author Aurélien Baudet
*
*/
public static class Options {
private final boolean override;
private final boolean skipUnknown;
private final boolean skipConversionError;
private final boolean skipInvocationError;
public Options(boolean override, boolean skipUnknown, boolean skipConversionError, boolean skipInvocationError) {
super();
this.override = override;
this.skipUnknown = skipUnknown;
this.skipConversionError = skipConversionError;
this.skipInvocationError = skipInvocationError;
}
public boolean isOverride() {
return override;
}
public boolean isSkipUnknown() {
return skipUnknown;
}
public boolean isSkipConversionError() {
return skipConversionError;
}
public boolean isSkipInvocationError() {
return skipInvocationError;
}
}
private BeanUtils() {
super();
}
}