Project: amplafi-sworddance
/*
 * 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 com.sworddance.beans; 
 
import java.lang.reflect.Method; 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.Collection; 
import java.util.Collections; 
import java.util.Iterator; 
import java.util.List; 
import java.util.Map; 
import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.ConcurrentMap; 
import java.util.regex.Matcher; 
import java.util.regex.Pattern; 
 
import static com.sworddance.util.CUtilities.*; 
 
import com.sworddance.util.ApplicationIllegalArgumentException; 
import com.sworddance.util.NotNullIterator; 
 
import org.apache.commons.lang.StringUtils; 
 
/**
 * Provides some general utility methods so that bean operations can be used more easily. 
 * 
 * Note that an instance of a BeanWorker is not linked to a class. This allows "duck-typing" operations. 
 * @author patmoore 
 * 
 */
 
public class BeanWorker { 
 
    private static final Pattern PROPERTY_METHOD_PATTERN = Pattern.compile("(is|set|get)(([A-Z])(\\w*))$"); 
    private static final Pattern GET_METHOD_PATTERN = Pattern.compile("(is|get)(([A-Z])(\\w*))$"); 
    private static final Pattern SET_METHOD_PATTERN = Pattern.compile("(set)(([A-Z])(\\w*))$"); 
 
    /**
     * This list of property names is the list of the only properties that the BeanWorker is allowed to modify. 
     * Specifically, "foo.goo" does not mean the BeanWorker is allowed to modify the "foo" property - only "foo"'s "goo" property can be modified. 
     */
 
    private List<String> propertyNames = new ArrayList<String>(); 
    // key = class, key = (each element in) propertyNames value = chain of methods to get to value. 
    // TODO in future cache into a second map. 
    private final MapByClass<ConcurrentMap<String,PropertyMethodChain>> methodsMap = new MapByClass<ConcurrentMap<String,PropertyMethodChain>>(); 
 
    /**
     * Allow intermediate Properties to be accessed. 
     * 
     * For example, if the property list is "child.grandchild" then "child" could be accessed directly. Useful for deep copying. 
     */
 
    private final boolean allowIntermediateProperties; 
    public BeanWorker() { 
        this.allowIntermediateProperties = false
    } 
    public BeanWorker(String... propertyNames) { 
        this(false, propertyNames); 
    } 
    /**
     * @param propertyNames 
     */
 
    public BeanWorker(Collection<String> propertyNames) { 
        this(false, propertyNames); 
    } 
    public BeanWorker(boolean allowIntermediateProperties, String... propertyNames) { 
        this.allowIntermediateProperties = allowIntermediateProperties; 
     addPropertyNames(propertyNames); 
    } 
    public BeanWorker(boolean allowIntermediateProperties, Collection<String> propertyNames) { 
        this.allowIntermediateProperties = allowIntermediateProperties; 
        addPropertyNames(propertyNames); 
    } 
    /**
     * @param propertyNames the propertyNames to set 
     */
 
    public void setPropertyNames(List<String> propertyNames) { 
        this.propertyNames.clear(); 
        this.addPropertyNames(propertyNames); 
    } 
    public void addPropertyNames(String... additionalPropertyNames) { 
        addPropertyNames(Arrays.asList(additionalPropertyNames)); 
    } 
    public void addPropertyNames(Collection<String> additionalPropertyNames) { 
        if ( isNotEmpty(additionalPropertyNames)) { 
            this.propertyNames.addAll(additionalPropertyNames); 
            // sorted so that when creating intermediate propertyChains we can create them read-only because we know that all the 
            // explicit read/write properties have been created already. 
            // See getMethodMap() 
            Collections.sort(this.propertyNames); 
        } 
    } 
    /**
     * @return the propertyNames 
     */
 
    public List<String> getPropertyNames() { 
        return propertyNames; 
    } 
    public String getPropertyName(int index) { 
        return isNotEmpty(this.propertyNames) && index < this.propertyNames.size()?this.propertyNames.get(index) : null
    } 
 
    /**
     * Follows the propertyPath starting at base until null or until the end. 
     * @param <T> 
     * @param base 
     * @param property 
     * @return null or the property 
     */
 
    @SuppressWarnings("unchecked"
    public <T> T getValue(Object base, String property) { 
        T result = null
        if ( base != null && property != null ) { 
            // TODO: ideally readOnly = true but this would screw up later code that did need to write value. 
            PropertyMethodChain methodChain = getPropertyMethodChain(base.getClass(), property); 
            if ( methodChain != null ) { 
                result = (T) methodChain.getValue(base); 
            } 
        } 
        return result; 
    } 
 
    public void setValue(Object base, String property, Object value) { 
        if ( base != null && property != null ) { 
            PropertyMethodChain methodChain = getPropertyMethodChain(base.getClass(), property); 
            if ( methodChain != null ) { 
                methodChain.setValue(base, value); 
            } 
        } 
    } 
    protected PropertyMethodChain getPropertyMethodChain(Class<?> clazz, String property) { 
        Map<String, PropertyMethodChain> classMethodMap = getMethodMap(clazz); 
        PropertyMethodChain methodChain = classMethodMap.get(property); 
        return methodChain; 
    } 
    /**
     * For example, "grandparent.parent.child" will return a Method 
     * chain of length 3 ( "getGrandparent().getParent().getChild()" ) 
     * 
     * @param clazz 
     * @param property "grandparent.parent.child" 
     * @param readOnly 
     * @return a chain of {@link Method}s that when sequentially called will return a result. 
     */
 
    protected PropertyMethodChain getPropertyMethodChainAddIfAbsent(Class<?> clazz, String property, boolean readOnly) { 
        ConcurrentMap<String, PropertyMethodChain> classMethodMap = getMethodMap(clazz); 
        PropertyMethodChain methodChain = addPropertyMethodChainIfAbsent(clazz, classMethodMap, property, readOnly); 
        return methodChain; 
    } 
 
    public Class<?> getPropertyType(Class<?> clazz, String property) { 
        PropertyMethodChain chain = getPropertyMethodChain(clazz, property); 
        if ( chain == null) { 
            chain = getFirst(newPropertyMethodChain(clazz, property, truefalse)); 
            // TODO should put in the methodChain 
        } 
        return chain.getReturnType(); 
    } 
    /**
     * Each class has its own version of the PropertyMethodChain map. 
     * @param clazz 
     * @return PropertyMethodChain map for the passed class. 
     */
 
    protected ConcurrentMap<String, PropertyMethodChain> getMethodMap(Class<?> clazz) { 
        ConcurrentMap<String, PropertyMethodChain> propMap; 
        if ( !methodsMap.containsKey(clazz)) { 
            propMap = new ConcurrentHashMap<String, PropertyMethodChain>(); 
            for(String property: NotNullIterator.<String>newNotNullIterator( getPropertyNames())) { 
                addPropertyMethodChainIfAbsent(clazz, propMap, property, false); 
            } 
            methodsMap.putIfAbsent(clazz, propMap); 
        } 
        propMap = methodsMap.get(clazz); 
 
        return propMap; 
    } 
    /**
     * @param clazz 
     * @param propMap 
     * @param propertyName 
     * @param readOnly if true and if propertyMethodChain has not been found then only the get method is searched for. 
     * @return the propertyMethodChain 
     * @throws ApplicationIllegalArgumentException if the propertyName is not actually a property. 
     */
 
    protected PropertyMethodChain addPropertyMethodChainIfAbsent(Class<?> clazz, ConcurrentMap<String, PropertyMethodChain> propMap, String propertyName, boolean readOnly) 
        throws ApplicationIllegalArgumentException { 
        if (!propMap.containsKey(propertyName)) { 
            List<PropertyMethodChain> propertyMethodChains = newPropertyMethodChain(clazz, propertyName, readOnly, allowIntermediateProperties); 
            ApplicationIllegalArgumentException.valid(isNotEmpty(propertyMethodChains), clazz, " has no property named '",propertyName,"'"); 
            for(PropertyMethodChain propertyMethodChain: propertyMethodChains) { 
                propMap.putIfAbsent(propertyMethodChain.getProperty(), propertyMethodChain); 
            } 
        } else { 
            // TODO: check to see if readOnly is false 
        } 
        return propMap.get(propertyName); 
    } 
 
 
    /**
     * @param clazz 
     * @param property 
     * @param readOnly 
     * @return the propertyMethodChain 
     */
 
    protected List<PropertyMethodChain> newPropertyMethodChain(Class<?> clazz, String property, boolean readOnly, boolean expanded) { 
        try { 
            String[] splitProps = property.split("\\."); 
            List<PropertyAdaptor> completePropertyMethodList = getMethods(clazz, splitProps, readOnly); 
            List<PropertyMethodChain> propertyMethodChains; 
            if ( !expanded) { 
                propertyMethodChains = Arrays.asList(new PropertyMethodChain(clazz, property, readOnly, completePropertyMethodList)); 
            } else { 
                propertyMethodChains = new ArrayList<PropertyMethodChain>(); 
                List<PropertyAdaptor> propertyMethodList = new ArrayList<PropertyAdaptor>(); 
                StringBuilder sb = new StringBuilder(); 
                for(int i = 0; i < splitProps.length; i++ ) { 
                    sb.append(splitProps[i]); 
                    propertyMethodList.add(completePropertyMethodList.get(i)); 
                    boolean notLast = i < splitProps.length-1
                    // make readOnly if readOnly parameter or if intermediate PropertyMethodChain. 
                    propertyMethodChains.add(new PropertyMethodChain(clazz, sb.toString(), readOnly||notLast, propertyMethodList)); 
                    sb.append("."); 
                } 
            } 
            return propertyMethodChains; 
        } catch (IllegalArgumentException e) { 
            return null
        } 
    } 
    /**
     * collects a chain of property methods that are called sequentially to get the final result. 
     * @param clazz 
     * @param propertyNamesList 
     * @param readOnly only look for a getter 
     * @return the chain of methods. 
     */
 
    protected List<PropertyAdaptor> getMethods(Class<?> clazz, String[] propertyNamesList, boolean readOnly) { 
        Class<?>[] parameterTypes = new Class<?>[0]; 
        List<PropertyAdaptor> propertyMethodChain = new ArrayList<PropertyAdaptor>(); 
        for(Iterator<String> iter = Arrays.asList(propertyNamesList).iterator(); iter.hasNext();) { 
            String propertyName = iter.next(); 
            PropertyAdaptor propertyAdaptor = new PropertyAdaptor(propertyName); 
            propertyAdaptor.setGetter(clazz, parameterTypes); 
            if ( !iter.hasNext() && !readOnly) { 
                // only get the setter on the last iteration because PropertyMethodChain is only allowed to set the property at the 
                // end of the chain. No other property along the way can be set. 
                propertyAdaptor.initSetter(clazz); 
            } 
            if ( propertyAdaptor.isExists()) { 
                clazz = propertyAdaptor.getReturnType(); 
                propertyMethodChain.add(propertyAdaptor); 
            } else { 
                throw new IllegalArgumentException(StringUtils.join(propertyNamesList)+" has bad property " + propertyName); 
            } 
        } 
        return propertyMethodChain; 
    } 
 
    protected String getPropertyName(Method method) { 
        String methodName = method.getName(); 
        return this.getPropertyName(methodName); 
    } 
    protected String getPropertyName(String methodName) { 
        Matcher matcher = PROPERTY_METHOD_PATTERN.matcher(methodName); 
        String propertyName; 
        if (matcher.find()) { 
            propertyName = matcher.group(3).toLowerCase()+matcher.group(4); 
        } else { 
            propertyName = null
        } 
        return propertyName; 
    } 
    protected String getGetterPropertyName(String methodName) { 
        Matcher matcher = GET_METHOD_PATTERN.matcher(methodName); 
        String propertyName; 
        if (matcher.find()) { 
            propertyName = matcher.group(3).toLowerCase()+matcher.group(4); 
        } else { 
            propertyName = null
        } 
        return propertyName; 
    } 
    protected String getSetterPropertyName(String methodName) { 
        Matcher matcher = SET_METHOD_PATTERN.matcher(methodName); 
        String propertyName; 
        if (matcher.find()) { 
            propertyName = matcher.group(3).toLowerCase()+matcher.group(4); 
        } else { 
            propertyName = null
        } 
        return propertyName; 
    } 
}