Java tutorial
/* * BeanCloner.java created on 2010-10-18 * * Created by Brushing Bits Labs * http://www.brushingbits.org * * 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 org.brushingbits.jnap.common.bean.cloning; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.PredicateUtils; import org.brushingbits.jnap.common.bean.visitor.BeanPropertyFilter; import org.brushingbits.jnap.common.bean.visitor.DefaultTypeResolver; import org.brushingbits.jnap.common.bean.visitor.HibernateTypeResolver; import org.brushingbits.jnap.common.bean.visitor.PropertyTypeResolver; import org.brushingbits.jnap.common.util.StringMatcher; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.util.Assert; /** * TODO make "org.brushingbits.jnap.common.bean.visitor.BeanPropertyVisitor" * generic enough so cloning, property extractor, etc can be subclassed from the * visitor. * * @author Daniel Rochetti * @since 1.0 */ public class BeanCloner { protected BeanPropertyFilter propertyFilter; protected PropertyTypeResolver propertyTypeResolver; private VisitorContext context; private boolean preventCircularVisiting; private List<Class<?>> standardTypes; private Map alreadyCloned; /** * * @param propertyFilter */ public BeanCloner(BeanPropertyFilter propertyFilter, PropertyTypeResolver propertyTypeResolver) { // validating arguments Assert.notNull(propertyFilter); Assert.notNull(propertyTypeResolver); this.propertyFilter = propertyFilter; this.context = new VisitorContext(); this.alreadyCloned = new HashMap<Object, Object>(); this.preventCircularVisiting = true; // init type resolver (fallback to default if necessary) try { propertyTypeResolver.init(); this.propertyTypeResolver = propertyTypeResolver; } catch (Exception e) { this.propertyTypeResolver = new DefaultTypeResolver(); } // standard types (default) this.standardTypes = new ArrayList<Class<?>>(); this.standardTypes.add(String.class); this.standardTypes.add(Number.class); this.standardTypes.add(Boolean.class); this.standardTypes.add(Date.class); this.standardTypes.add(Calendar.class); this.standardTypes.add(URI.class); this.standardTypes.add(URL.class); } /** * * @param propertyFilter */ public BeanCloner(BeanPropertyFilter propertyFilter) { this(propertyFilter, new HibernateTypeResolver()); } /** * */ public BeanCloner() { this(BeanPropertyFilter.getDefault()); } public Object clone(Object source) { Object copy = null; if (source != null) { final String currentPath = getCurrentPath(); final Class<?> type = this.propertyTypeResolver.resolve(source, this.propertyFilter); final boolean excluded = type == null || shouldExcludeProperty(currentPath) || shouldExcludeType(type) || shouldExcludeAssignableType(type); final int currentLevel = context.getCurrentLevel(); final boolean included = currentLevel == -1 || shouldIncludeProperty(currentPath); final boolean validDepth = isValidDepth(); if (included || (!excluded && validDepth)) { if (isStandardType(type)) { copy = source; } else if (type.isArray()) { copy = cloneArray((Object[]) source, type); } else if (Collection.class.isAssignableFrom(type)) { copy = cloneCollection((Collection<?>) source, type); } else if (Map.class.isAssignableFrom(type)) { copy = cloneMap((Map<?, ?>) source, type); } else { // so, we assume it's a nested bean (let's go 'down' another level) if (context.wasAlreadyVisited(source) && this.preventCircularVisiting) { copy = null; } else if (context.wasAlreadyVisited(source) && !this.preventCircularVisiting) { copy = this.alreadyCloned.get(source); } else { context.nextLevel(); if (isValidDepth()) { copy = cloneBean(source, type); } context.prevLevel(); } } } } return copy; } /** * @param currentLevel * @return */ private boolean isValidDepth() { return propertyFilter.getDepth() == -1 || context.getCurrentLevel() <= propertyFilter.getDepth(); } private Object cloneBean(Object bean, Class<?> type) { BeanWrapper source = PropertyAccessorFactory.forBeanPropertyAccess(bean); BeanWrapper copy = PropertyAccessorFactory.forBeanPropertyAccess(BeanUtils.instantiate(type)); // keep instance for circular and multiple references context.setAsVisited(bean); alreadyCloned.put(bean, copy.getWrappedInstance()); PropertyDescriptor[] beanProperties = copy.getPropertyDescriptors(); for (PropertyDescriptor propertyDescriptor : beanProperties) { String name = propertyDescriptor.getName(); context.pushPath(name); if (copy.isReadableProperty(name) && copy.isWritableProperty(name)) { Object value = source.getPropertyValue(name); copy.setPropertyValue(name, clone(value)); } context.popPath(); } Object beanCopy = copy.getWrappedInstance(); source = null; copy = null; return beanCopy; } private Object[] cloneArray(Object[] array, Class<?> type) { Object[] arrayCopy = (Object[]) Array.newInstance(type, array.length); for (int i = 0; i < array.length; i++) { arrayCopy[i] = clone(array[i]); } return arrayCopy; } private Collection<?> cloneCollection(Collection<?> collection, Class<?> type) { Collection<Object> collectionCopy = (Collection<Object>) BeanUtils.instantiate(type); for (Object item : collection) { collectionCopy.add(clone(item)); } CollectionUtils.filter(collectionCopy, PredicateUtils.notNullPredicate()); if (collectionCopy.isEmpty()) { collectionCopy = null; } return collectionCopy; } private Map<Object, Object> cloneMap(Map<?, ?> map, Class<?> type) { Map<Object, Object> mapCopy = (Map<Object, Object>) BeanUtils.instantiate(type); for (Object key : map.keySet()) { Object value = map.get(key); mapCopy.put(clone(key), clone(value)); } return mapCopy; } protected boolean isStandardType(Class<?> type) { boolean standard = type.isPrimitive() || type.isEnum(); if (!standard) { for (Class<?> standardType : this.standardTypes) { standard = standardType.isAssignableFrom(type); if (standard) { break; } } } return standard; } /** * * @param type */ public void registerStandardTypes(Class<?>... type) { this.standardTypes.addAll(Arrays.asList(type)); } protected String getCurrentPath() { StringBuilder path = new StringBuilder(); for (Iterator<String> iter = context.getCurrentPath().descendingIterator(); iter.hasNext();) { String pathElement = iter.next(); path.append(pathElement); if (iter.hasNext()) { path.append("."); } } return path.toString(); } protected boolean shouldExcludeProperty(String path) { return StringMatcher.match(path, propertyFilter.getExcludeProperties().toArray(new String[] {})); } protected boolean shouldExcludeType(Class<?> type) { return StringMatcher.match(type.getName(), propertyFilter.getExcludeTypes().toArray(new String[] {})); } protected boolean shouldExcludeAssignableType(Class<?> type) { boolean exclude = false; List<Class<?>> excludeAssignableTypes = propertyFilter.getExcludeAssignableTypes(); for (Class<?> excludeType : excludeAssignableTypes) { if (excludeType.isAssignableFrom(type)) { exclude = true; break; } } return exclude; } protected boolean shouldIncludeProperty(String path) { return StringMatcher.match(path, propertyFilter.getIncludeProperties().toArray(new String[] {})); } /** * * @author Daniel Rochetti * @since 1.0 */ protected class VisitorContext { private int currentLevel = -1; private Deque<String> currentPath; private List<Object> alreadyVisited; public VisitorContext() { this.currentPath = new LinkedList<String>(); this.alreadyVisited = new ArrayList<Object>(); } public int getCurrentLevel() { return currentLevel; } public void nextLevel() { this.currentLevel += 1; } public void prevLevel() { this.currentLevel -= 1; } public Deque<String> getCurrentPath() { return currentPath; } public String getCurrentPathRepresentation() { StringBuilder path = new StringBuilder(); for (Iterator<String> iter = currentPath.descendingIterator(); iter.hasNext();) { path.append(iter.next()); if (iter.hasNext()) { path.append("."); } } return path.toString(); } public String getCurrentPropertyName() { return currentPath.getFirst(); } public void pushPath(String path) { this.currentPath.addFirst(path); } public void popPath() { this.currentPath.removeFirst(); } public void setAsVisited(Object bean) { this.alreadyVisited.add(bean); } public boolean wasAlreadyVisited(Object bean) { return this.alreadyVisited.contains(bean); } } }