com.siberhus.tdfl.mapping.BeanWrapLineMapper.java Source code

Java tutorial

Introduction

Here is the source code for com.siberhus.tdfl.mapping.BeanWrapLineMapper.java

Source

/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */
package com.siberhus.tdfl.mapping;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.beanutils.ConversionException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import com.siberhus.tdfl.FieldDataException;
import com.siberhus.tdfl.transform.FieldSet;

/**
 * 
 * @author Hussachai Puripunpinyo (http://www.siberhus.com)
 * 
 */
public class BeanWrapLineMapper<T> implements LineMapper<T>, BeanFactoryAware, InitializingBean {

    protected String name;

    protected Class<? extends T> type;

    protected BeanFactory beanFactory;

    private static Map<Class<?>, Map<String, String>> propertiesMatched = new HashMap<Class<?>, Map<String, String>>();

    protected static Map<Class<?>, String[]> propertyNamesCache = new HashMap<Class<?>, String[]>();

    private static int distanceLimit = 5;

    protected boolean strict = false;

    @Override
    public T mapLine(FieldSet fs, FieldDataException fde) throws Exception {
        T copy = getBean();
        Properties properties = fs.getProperties();
        properties = getBeanProperties(copy, properties);
        for (String name : propertyNamesCache.get(getBean().getClass())) {
            String value = properties.getProperty(name);
            try {
                if (org.apache.commons.lang.StringUtils.isEmpty(value)) {
                    continue;
                }
                org.apache.commons.beanutils.BeanUtils.setProperty(copy, name, value);
            } catch (ConversionException e) {
                if (strict) {
                    throw e;
                }
                fde.addError(name, e);
            }
        }
        return copy;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org
     * .springframework.beans.factory.BeanFactory)
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * The bean name (id) for an object that can be populated from the field set
     * that will be passed into {@link #mapFieldSet(FieldSet)}. Typically a
     * prototype scoped bean so that a new instance is returned for each field
     * set mapped.
     * 
     * Either this property or the type property must be specified, but not
     * both.
     * 
     * @param name the name of a prototype bean in the enclosing BeanFactory
     */
    public void setPrototypeBeanName(String name) {
        this.name = name;
    }

    /**
     * Public setter for the type of bean to create instead of using a prototype
     * bean. An object of this type will be created from its default constructor
     * for every call to {@link #mapFieldSet(FieldSet)}.<br/>
     * 
     * Either this property or the prototype bean name must be specified, but
     * not both.
     * 
     * @param type the type to set
     */
    public void setTargetType(Class<? extends T> type) {
        this.type = type;
    }

    /**
     * Check that precisely one of type or prototype bean name is specified.
     * 
     * @throws IllegalStateException if neither is set or both properties are
     * set.
     * 
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        Assert.state(name != null || type != null, "Either name or type must be provided.");
        Assert.state(name == null || type == null, "Both name and type cannot be specified together.");
    }

    /**
     * Public setter for the 'strict' property. If true, then
     * {@link #mapFieldSet(FieldSet)} will fail of the FieldSet contains fields
     * that cannot be mapped to the bean.
     * 
     * @param strict
     */
    public void setStrict(boolean strict) {
        this.strict = strict;
    }

    @SuppressWarnings("unchecked")
    private T getBean() {
        if (name != null) {
            return (T) beanFactory.getBean(name);
        }
        try {
            return type.newInstance();
        } catch (InstantiationException e) {
            ReflectionUtils.handleReflectionException(e);
        } catch (IllegalAccessException e) {
            ReflectionUtils.handleReflectionException(e);
        }
        // should not happen
        throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
    }

    @SuppressWarnings("unchecked")
    protected Properties getBeanProperties(Object bean, Properties properties) throws IntrospectionException {

        Class<?> cls = bean.getClass();

        String[] namesCache = propertyNamesCache.get(cls);
        if (namesCache == null) {
            List<String> setterNames = new ArrayList<String>();
            BeanInfo beanInfo = Introspector.getBeanInfo(cls);
            PropertyDescriptor propDescs[] = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor propDesc : propDescs) {
                if (propDesc.getWriteMethod() != null) {
                    setterNames.add(propDesc.getName());
                }
            }
            propertyNamesCache.put(cls, setterNames.toArray(new String[0]));
        }
        // Map from field names to property names
        Map<String, String> matches = propertiesMatched.get(cls);
        if (matches == null) {
            matches = new HashMap<String, String>();
            propertiesMatched.put(cls, matches);
        }

        @SuppressWarnings("rawtypes")
        Set<String> keys = new HashSet(properties.keySet());
        for (String key : keys) {

            if (matches.containsKey(key)) {
                switchPropertyNames(properties, key, matches.get(key));
                continue;
            }

            String name = findPropertyName(bean, key);

            if (name != null) {
                matches.put(key, name);
                switchPropertyNames(properties, key, name);
            }
        }

        return properties;
    }

    private String findPropertyName(Object bean, String key) {

        if (bean == null) {
            return null;
        }

        Class<?> cls = bean.getClass();

        int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
        String prefix;
        String suffix;

        // If the property name is nested recurse down through the properties
        // looking for a match.
        if (index > 0) {
            prefix = key.substring(0, index);
            suffix = key.substring(index + 1, key.length());
            String nestedName = findPropertyName(bean, prefix);
            if (nestedName == null) {
                return null;
            }

            Object nestedValue = getPropertyValue(bean, nestedName);
            String nestedPropertyName = findPropertyName(nestedValue, suffix);
            return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
        }

        String name = null;
        int distance = 0;
        index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);

        if (index > 0) {
            prefix = key.substring(0, index);
            suffix = key.substring(index);
        } else {
            prefix = key;
            suffix = "";
        }

        while (name == null && distance <= distanceLimit) {
            String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches();
            // If we find precisely one match, then use that one...
            if (candidates.length == 1) {
                String candidate = candidates[0];
                if (candidate.equals(prefix)) { // if it's the same don't
                    // replace it...
                    name = key;
                } else {
                    name = candidate + suffix;
                }
            }
            distance++;
        }
        return name;
    }

    private Object getPropertyValue(Object bean, String nestedName) {
        BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
        Object nestedValue = wrapper.getPropertyValue(nestedName);
        if (nestedValue == null) {
            try {
                nestedValue = wrapper.getPropertyType(nestedName).newInstance();
                wrapper.setPropertyValue(nestedName, nestedValue);
            } catch (InstantiationException e) {
                ReflectionUtils.handleReflectionException(e);
            } catch (IllegalAccessException e) {
                ReflectionUtils.handleReflectionException(e);
            }
        }
        return nestedValue;
    }

    private void switchPropertyNames(Properties properties, String oldName, String newName) {
        String value = properties.getProperty(oldName);
        properties.remove(oldName);
        properties.setProperty(newName, value);
    }

    /**
     * Helper class for calculating bean property matches, according to.
     * Used by BeanWrapperImpl to suggest alternatives for an invalid property name.<br/>
     * 
     * Copied and slightly modified from Spring core,
     *
     * @author Alef Arendsen
     * @author Arjen Poutsma
     * @author Juergen Hoeller
     * @author Dave Syer
     * 
     * @since 1.0
     * @see #forProperty(String, Class)
     */
    final static class PropertyMatches {

        //---------------------------------------------------------------------
        // Static section
        //---------------------------------------------------------------------

        /** Default maximum property distance: 2 */
        public static final int DEFAULT_MAX_DISTANCE = 2;

        /**
         * Create PropertyMatches for the given bean property.
         * @param propertyName the name of the property to find possible matches for
         * @param beanClass the bean class to search for matches
         */
        public static PropertyMatches forProperty(String propertyName, Class<?> beanClass) {
            return forProperty(propertyName, beanClass, DEFAULT_MAX_DISTANCE);
        }

        /**
         * Create PropertyMatches for the given bean property.
         * @param propertyName the name of the property to find possible matches for
         * @param beanClass the bean class to search for matches
         * @param maxDistance the maximum property distance allowed for matches
         */
        public static PropertyMatches forProperty(String propertyName, Class<?> beanClass, int maxDistance) {
            return new PropertyMatches(propertyName, beanClass, maxDistance);
        }

        //---------------------------------------------------------------------
        // Instance section
        //---------------------------------------------------------------------

        private final String propertyName;

        private String[] possibleMatches;

        /**
         * Create a new PropertyMatches instance for the given property.
         */
        private PropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) {
            this.propertyName = propertyName;
            this.possibleMatches = calculateMatches(BeanUtils.getPropertyDescriptors(beanClass), maxDistance);
        }

        /**
         * Return the calculated possible matches.
         */
        public String[] getPossibleMatches() {
            return possibleMatches;
        }

        /**
         * Build an error message for the given invalid property name,
         * indicating the possible property matches.
         */
        public String buildErrorMessage() {
            StringBuffer buf = new StringBuffer();
            buf.append("Bean property '");
            buf.append(this.propertyName);
            buf.append("' is not writable or has an invalid setter method. ");

            if (ObjectUtils.isEmpty(this.possibleMatches)) {
                buf.append("Does the parameter type of the setter match the return type of the getter?");
            } else {
                buf.append("Did you mean ");
                for (int i = 0; i < this.possibleMatches.length; i++) {
                    buf.append('\'');
                    buf.append(this.possibleMatches[i]);
                    if (i < this.possibleMatches.length - 2) {
                        buf.append("', ");
                    } else if (i == this.possibleMatches.length - 2) {
                        buf.append("', or ");
                    }
                }
                buf.append("'?");
            }
            return buf.toString();
        }

        /**
         * Generate possible property alternatives for the given property and
         * class. Internally uses the <code>getStringDistance</code> method, which
         * in turn uses the Levenshtein algorithm to determine the distance between
         * two Strings.
         * @param propertyDescriptors the JavaBeans property descriptors to search
         * @param maxDistance the maximum distance to accept
         */
        private String[] calculateMatches(PropertyDescriptor[] propertyDescriptors, int maxDistance) {
            List<String> candidates = new ArrayList<String>();
            for (int i = 0; i < propertyDescriptors.length; i++) {
                if (propertyDescriptors[i].getWriteMethod() != null) {
                    String possibleAlternative = propertyDescriptors[i].getName();
                    if (calculateStringDistance(this.propertyName, possibleAlternative) <= maxDistance) {
                        candidates.add(possibleAlternative);
                    }
                }
            }
            Collections.sort(candidates);
            return StringUtils.toStringArray(candidates);
        }

        /**
         * Calculate the distance between the given two Strings
         * according to the Levenshtein algorithm.
         * @param s1 the first String
         * @param s2 the second String
         * @return the distance value
         */
        private int calculateStringDistance(String s1, String s2) {
            if (s1.length() == 0) {
                return s2.length();
            }
            if (s2.length() == 0) {
                return s1.length();
            }
            int d[][] = new int[s1.length() + 1][s2.length() + 1];

            for (int i = 0; i <= s1.length(); i++) {
                d[i][0] = i;
            }
            for (int j = 0; j <= s2.length(); j++) {
                d[0][j] = j;
            }

            for (int i = 1; i <= s1.length(); i++) {
                char s_i = s1.charAt(i - 1);
                for (int j = 1; j <= s2.length(); j++) {
                    int cost;
                    char t_j = s2.charAt(j - 1);
                    if (Character.toLowerCase(s_i) == Character.toLowerCase(t_j)) {
                        cost = 0;
                    } else {
                        cost = 1;
                    }
                    d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost);
                }
            }

            return d[s1.length()][s2.length()];
        }

    }
}