INT-1049 Added CycleDetector as transformer package-only visible class for now, added tests

This commit is contained in:
Oleg Zhurakousky
2010-07-12 21:53:27 +00:00
parent 4ad2b3c489
commit 3fb3611b41
2 changed files with 427 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* 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.springframework.integration.transformer;
import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.CollectionUtils;
/**
* @author Oleg Zhurakousky
* @since 2.0
*/
class CycleDetector {
private final ExpressionParser parser = new SpelExpressionParser();
private final String[] defaultIgnorePakages = new String[]{
"com.apple",
"com.sun",
"java.awt",
"java",
"javax",
"org.jcp",
"org.omg",
"sun"
};
private boolean ignoreClassAttribute = true;
private boolean ignoreDefaultPackages = true;
/**
*
* @param target
* @param ignorePakages
* @return
*/
public void detectCycle(Object target, String... ignorePakages){
Map<Object, Set<Object>> objectReferenceMap = new HashMap<Object, Set<Object>>();
this.doDetect(target, objectReferenceMap, ignorePakages);
}
/*
*
*/
private void doDetect(Object target, Map<Object, Set<Object>> objectReferenceMap, String[] ignorePakages){
if (propertyInIgnoredPackage(target, ignorePakages)){
return;
}
if (collectionMapOrArray(target)){
Iterable<?> iterable = null;
if (target instanceof Collection<?>) {
iterable = (Iterable<?>) target;
} else if (target instanceof Map<?, ?>){
iterable = ((Map<?, ?>) target).values();
} else if (target.getClass().isArray()){
iterable = CollectionUtils.arrayToList(target);
}
for (Object value : iterable) {
this.doDetect(value, objectReferenceMap, ignorePakages);
}
} else {
if (!objectReferenceMap.containsKey(target)){
objectReferenceMap.put(target, new HashSet<Object>());
}
EvaluationContext context = new StandardEvaluationContext(target);
BeanWrapperImpl bw = new BeanWrapperImpl(target);
PropertyDescriptor[] descriptors = bw.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : descriptors) {
String propertyName = propertyDescriptor.getName();
if (propertyName.equals("class") && ignoreClassAttribute){
continue; // no need to process
}
Expression expression = parser.parseExpression(propertyName);
Object propertyValue = null;
try {
propertyValue = expression.getValue(context);
} catch (Exception e) {/*nothing to do, might only happen when 'ignoreClassAttribute' is set to false ('true' by default)*/}
if (propertyValue != null){
if (!collectionMapOrArray(propertyValue)){
Set<Object> references = objectReferenceMap.get(target);
if (!references.contains(propertyValue)){
references.add(propertyValue);
}
if (objectReferenceMap.containsKey(propertyValue)){
references = objectReferenceMap.get(propertyValue);
if (references.contains(target)){
throw new MessageTransformationException("Cyclic reference detected between: " +
propertyValue.getClass().getSimpleName() + " - " + target.getClass().getSimpleName());
}
}
}
this.doDetect(propertyValue, objectReferenceMap, ignorePakages);
}
}
}
}
/*
*
*/
private boolean collectionMapOrArray(Object elementValue){
return (elementValue instanceof Map<?,?> ||
elementValue instanceof Collection<?> ||
elementValue.getClass().isArray());
}
/*
*
*/
private boolean propertyInIgnoredPackage(Object elementValue, String[] ignorePakagess){
if (this.collectionMapOrArray(elementValue)){
return false;
}
for (String packagePattern : ignorePakagess) {
if (elementValue.getClass().getPackage().getName().startsWith(packagePattern)){
return true;
}
}
if (ignoreDefaultPackages){
for (String packagePattern : defaultIgnorePakages) {
if (elementValue.getClass().getPackage().getName().startsWith(packagePattern)){
return true;
}
}
}
return false;
}
public boolean isIgnoreClassAttribute() {
return ignoreClassAttribute;
}
public void setIgnoreClassAttribute(boolean ignoreClassAttribute) {
this.ignoreClassAttribute = ignoreClassAttribute;
}
public boolean isIgnoreDefaultPackages() {
return ignoreDefaultPackages;
}
public void setIgnoreDefaultPackages(boolean ignoreDefaultPackages) {
this.ignoreDefaultPackages = ignoreDefaultPackages;
}
}

View File

@@ -0,0 +1,266 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* 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.springframework.integration.transformer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
/**
* @author Oleg Zhurakousky
*
*/
public class CycleDetectorTests {
@Test
public void testWithNoIgnoredPackages(){
Parent parent = this.prepare(false);
CycleDetector builder = new CycleDetector();
builder.setIgnoreDefaultPackages(false);
builder.detectCycle(parent);
// should not throw an exception
}
@Test
public void testWithNoIgnoredPackagesAndClassProperty(){
Parent parent = this.prepare(false);
CycleDetector builder = new CycleDetector();
builder.setIgnoreDefaultPackages(false);
builder.setIgnoreClassAttribute(false);
builder.detectCycle(parent);
// should not throw an exception
}
@Test
public void testWithAditionalIgnoredPackages(){
Parent parent = this.prepare(true);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent, "org.springframework.integration.transformer");
// should not throw an exception, however if you remove additional package from
// the above method there is a cycle in the domain model
}
@Test
public void testObjectReferenceMapWithoutCycle(){
Parent parent = this.prepare(false);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
// should not throw an exception
}
@Test(expected=MessageTransformationException.class)
public void testObjectReferenceMapWithCycle(){
Parent parent = this.prepare(true);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
}
@Test
public void testWithNoCyclesInArray(){
Parent parent = this.prepare(false);
Foo[] foos = new Foo[]{new Foo(), new Foo()};
parent.getAddress().setFoos(foos);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
}
@Test(expected=MessageTransformationException.class)
public void testWithCyclesInArray(){
Parent parent = this.prepare(false);
Foo foo = new Foo();
Bar bar = new Bar();
foo.setBar(bar);
bar.setFoo(foo);
Foo[] foos = new Foo[]{foo, new Foo()};
parent.getAddress().setFoos(foos);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
}
@Test
public void testWithNoCyclesInMapWithList(){
Parent parent = this.prepare(false);
Foo fooA = new Foo();
Foo fooB = new Foo();
Foo[] foos = new Foo[]{fooA, fooB};
parent.getAddress().setFoos(foos);
List<Foo> fooList = new ArrayList<Foo>();
fooList.add(fooA);
fooList.add(fooB);
Map<Object, List<Foo>> mapOfFoos = new HashMap<Object, List<Foo>>();
mapOfFoos.put("listOfFoos", fooList);
parent.getChild().setMapOfFoos(mapOfFoos);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
}
@Test(expected=MessageTransformationException.class)
public void testWithCyclesInMapWithList(){
Parent parent = this.prepare(false);
Foo fooA = new Foo();
Bar bar = new Bar();
bar.setFoo(fooA);
fooA.setBar(bar);
Foo fooB = new Foo();
Foo[] foos = new Foo[]{new Foo(), new Foo()};
parent.getAddress().setFoos(foos);
List<Foo> fooList = new ArrayList<Foo>();
fooList.add(fooA);
fooList.add(fooB);
Map<Object, List<Foo>> mapOfFoos = new HashMap<Object, List<Foo>>();
mapOfFoos.put("listOfFoos", fooList);
parent.getChild().setMapOfFoos(mapOfFoos);
CycleDetector builder = new CycleDetector();
builder.detectCycle(parent);
}
//################# Test Classes ###################
public Parent prepare(boolean cycle){
Parent parent = new Parent();
Child child = new Child();
Address address = new Address();
address.setStreet("123 Main st");
child.setAddress(address);
List<String> nickNames = new ArrayList<String>();
nickNames.add("spanky");
nickNames.add("goofy");
child.setNickNames(nickNames);
child.setName("Seva");
if (cycle){
child.setParent(parent);
}
parent.setAddress(address);
parent.setChild(child);
parent.setName("Oleg");
return parent;
}
/*
*
*/
public static class Parent{
private Address address;
private Child child;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Child getChild() {
return child;
}
public void setChild(Child child) {
this.child = child;
}
}
/*
*
*/
public static class Child{
private String name;
private List<String> nickNames;
private Map<Object, List<Foo>> mapOfFoos;
private Address address;
private Parent parent;
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
public List<String> getNickNames() {
return nickNames;
}
public void setNickNames(List<String> nickNames) {
this.nickNames = nickNames;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<Object, List<Foo>> getMapOfFoos() {
return mapOfFoos;
}
public void setMapOfFoos(Map<Object, List<Foo>> mapOfFoos) {
this.mapOfFoos = mapOfFoos;
}
}
/*
*
*/
public static class Address{
private String street;
private Foo[] foos;
public Foo[] getFoos() {
return foos;
}
public void setFoos(Foo[] foos) {
this.foos = foos;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
public static class Foo{
private Bar bar;
public Bar getBar() {
return bar;
}
public void setBar(Bar bar) {
this.bar = bar;
}
}
public static class Bar{
private Foo foo;
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
}
}