From 3fb3611b414c8ec9df84cd674555cbcab0c74fa9 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 12 Jul 2010 21:53:27 +0000 Subject: [PATCH] INT-1049 Added CycleDetector as transformer package-only visible class for now, added tests --- .../transformer/CycleDetector.java | 161 +++++++++++ .../transformer/CycleDetectorTests.java | 266 ++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/transformer/CycleDetector.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/transformer/CycleDetectorTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/CycleDetector.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/CycleDetector.java new file mode 100644 index 0000000000..0e993bfe8a --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/CycleDetector.java @@ -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> objectReferenceMap = new HashMap>(); + this.doDetect(target, objectReferenceMap, ignorePakages); + } + /* + * + */ + private void doDetect(Object target, Map> 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()); + } + 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 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; + } +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/CycleDetectorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/CycleDetectorTests.java new file mode 100644 index 0000000000..06b8122114 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/CycleDetectorTests.java @@ -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 fooList = new ArrayList(); + fooList.add(fooA); + fooList.add(fooB); + Map> mapOfFoos = new HashMap>(); + 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 fooList = new ArrayList(); + fooList.add(fooA); + fooList.add(fooB); + Map> mapOfFoos = new HashMap>(); + 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 nickNames = new ArrayList(); + 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 nickNames; + private Map> mapOfFoos; + private Address address; + private Parent parent; + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + + public List getNickNames() { + return nickNames; + } + + public void setNickNames(List 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> getMapOfFoos() { + return mapOfFoos; + } + + public void setMapOfFoos(Map> 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; + } + } +}