From 9779ca191c1be81cf336fad8d592fa6f20d90734 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 22 Feb 2011 16:13:04 +0000 Subject: [PATCH] SPR-7976 Add MvcInterceptors features. --- .../InterceptorsBeanDefinitionParser.java | 59 +++---- .../web/servlet/config/MvcInterceptors.java | 158 ++++++++++++++++++ .../config/MvcInterceptorsExecutor.java | 60 +++++++ .../servlet/config/MvcInterceptorsTests.java | 126 ++++++++++++++ 4 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptors.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptorsExecutor.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcInterceptorsTests.java diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/InterceptorsBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/InterceptorsBeanDefinitionParser.java index 90a3afebff..55f95bb9e6 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/InterceptorsBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/InterceptorsBeanDefinitionParser.java @@ -18,59 +18,54 @@ package org.springframework.web.servlet.config; import java.util.List; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.config.AbstractSpecificationBeanDefinitionParser; +import org.springframework.context.config.FeatureSpecification; import org.springframework.util.xml.DomUtils; import org.springframework.web.servlet.handler.MappedInterceptor; import org.w3c.dom.Element; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a {@code interceptors} element to register - * a set of {@link MappedInterceptor} definitions. + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses + * a {@code interceptors} element to register set of {@link MappedInterceptor} + * definitions. * * @author Keith Donald + * @author Rossen Stoyanchev + * * @since 3.0 */ -class InterceptorsBeanDefinitionParser implements BeanDefinitionParser { +class InterceptorsBeanDefinitionParser extends AbstractSpecificationBeanDefinitionParser { + + /** + * Parses the {@code } tag. + */ + public FeatureSpecification doParse(Element element, ParserContext parserContext) { + MvcInterceptors mvcInterceptors = new MvcInterceptors(); - public BeanDefinition parse(Element element, ParserContext parserContext) { - CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); - parserContext.pushContainingComponent(compDefinition); - List interceptors = DomUtils.getChildElementsByTagName(element, new String[] { "bean", "interceptor" }); for (Element interceptor : interceptors) { - RootBeanDefinition mappedInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); - mappedInterceptorDef.setSource(parserContext.extractSource(interceptor)); - mappedInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String[] pathPatterns; - BeanDefinitionHolder interceptorDef; if ("interceptor".equals(interceptor.getLocalName())) { List paths = DomUtils.getChildElementsByTagName(interceptor, "mapping"); - pathPatterns = new String[paths.size()]; + String[] pathPatterns = new String[paths.size()]; for (int i = 0; i < paths.size(); i++) { pathPatterns[i] = paths.get(i).getAttribute("path"); } - Element interceptorBean = DomUtils.getChildElementByTagName(interceptor, "bean"); - interceptorDef = parserContext.getDelegate().parseBeanDefinitionElement(interceptorBean); - interceptorDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(interceptorBean, interceptorDef); + Element beanElement = DomUtils.getChildElementByTagName(interceptor, "bean"); + mvcInterceptors.interceptor(pathPatterns, parseBeanElement(parserContext, beanElement)); } else { - pathPatterns = null; - interceptorDef = parserContext.getDelegate().parseBeanDefinitionElement(interceptor); - interceptorDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(interceptor, interceptorDef); + mvcInterceptors.interceptor(null, parseBeanElement(parserContext, interceptor)); } - mappedInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, pathPatterns); - mappedInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, interceptorDef); - String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedInterceptorDef); - parserContext.registerComponent(new BeanComponentDefinition(mappedInterceptorDef, mappedInterceptorName)); } - - parserContext.popAndRegisterContainingComponent(); - return null; + + return mvcInterceptors; } - + + private BeanDefinitionHolder parseBeanElement(ParserContext parserContext, Element interceptor) { + BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(interceptor); + beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(interceptor, beanDef); + return beanDef; + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptors.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptors.java new file mode 100644 index 0000000000..8169fdcfd0 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptors.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.ProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptor; + +/** + * Specifies the Spring MVC "interceptors" container feature. The feature + * registers one or more {@link MappedInterceptor} bean definitions. A + * MappedInterceptor encapsulates an interceptor and one or more (optional) + * path patterns to which the interceptor is mapped. The interceptor can be + * of type {@link HandlerInterceptor} or {@link WebRequestInterceptor}. + * An interceptor can also be provided without path patterns in which case + * it applies globally to all handler invocations. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcInterceptors extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcInterceptorsExecutor.class; + + private Map interceptorMappings = new LinkedHashMap(); + + /** + * Creates an MvcInterceptors instance. + */ + public MvcInterceptors() { + super(EXECUTOR_TYPE); + } + + /** + * Add one or more {@link HandlerInterceptor HandlerInterceptors} that should + * intercept all handler invocations. + * + * @param interceptors one or more interceptors + */ + public MvcInterceptors globalInterceptors(HandlerInterceptor... interceptors) { + addInterceptorMappings(null, interceptors); + return this; + } + + /** + * Add one or more {@link WebRequestInterceptor WebRequestInterceptors} that should + * intercept all handler invocations. + * + * @param interceptors one or more interceptors + */ + public MvcInterceptors globalInterceptors(WebRequestInterceptor... interceptors) { + addInterceptorMappings(null, interceptors); + return this; + } + + /** + * Add one or more interceptors by bean name that should intercept all handler + * invocations. + * + * @param interceptors interceptor bean names + */ + public MvcInterceptors globalInterceptors(String... interceptors) { + addInterceptorMappings(null, interceptors); + return this; + } + + /** + * Add one or more {@link HandlerInterceptor HandlerInterceptors} and map + * them to the specified path patterns. + * + * @param pathPatterns the pathPatterns to map the interceptor to + * @param interceptors the interceptors + */ + public MvcInterceptors mappedInterceptors(String[] pathPatterns, HandlerInterceptor... interceptors) { + addInterceptorMappings(pathPatterns, interceptors); + return this; + } + + /** + * Add one or more {@link WebRequestInterceptor WebRequestInterceptors} and + * map them to the specified path patterns. + * + * @param pathPatterns the pathPatterns to map the interceptor to + * @param interceptors the interceptors + */ + public MvcInterceptors mappedInterceptors(String[] pathPatterns, WebRequestInterceptor... interceptors) { + addInterceptorMappings(pathPatterns, interceptors); + return this; + } + + /** + * Add one or more interceptors by bean name and map them to the specified + * path patterns. + * + * @param pathPatterns the pathPatterns to map to + * @param interceptors the interceptors + */ + public MvcInterceptors mappedInterceptors(String[] pathPatterns, String... interceptors) { + addInterceptorMappings(pathPatterns, interceptors); + return this; + } + + void interceptor(String[] pathPatterns, BeanDefinitionHolder interceptor) { + addInterceptorMappings(pathPatterns, new Object[] { interceptor }); + } + + Map interceptorMappings() { + return Collections.unmodifiableMap(interceptorMappings); + } + + private void addInterceptorMappings(String[] pathPatterns, Object[] interceptors) { + for (Object interceptor : interceptors) { + interceptorMappings.put(interceptor, pathPatterns); + } + } + + @Override + protected void doValidate(ProblemCollector problems) { + if (interceptorMappings.size() == 0) { + problems.error("No interceptors defined."); + } + for (Object interceptor : interceptorMappings.keySet()) { + if (interceptor == null) { + problems.error("Null interceptor provided."); + } + if (interceptorMappings.get(interceptor) != null) { + for (String pattern : interceptorMappings.get(interceptor)) { + if (!StringUtils.hasText(pattern)) { + problems.error("Empty path pattern specified for " + interceptor); + } + } + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptorsExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptorsExecutor.java new file mode 100644 index 0000000000..029caaf24f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcInterceptorsExecutor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.SpecificationContext; +import org.springframework.web.servlet.handler.MappedInterceptor; + +/** + * Executes {@link MvcInterceptors} specification, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Rossen Stoyanchev + * + * @since 3.1 + */ +final class MvcInterceptorsExecutor extends AbstractSpecificationExecutor { + + @Override + protected void doExecute(MvcInterceptors spec, SpecificationContext specContext) { + ComponentRegistrar registrar = specContext.getRegistrar(); + Object source = spec.source(); + + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(spec.sourceName(), source); + + for (Object interceptor : spec.interceptorMappings().keySet()) { + RootBeanDefinition beanDef = new RootBeanDefinition(MappedInterceptor.class); + beanDef.setSource(source); + beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, + spec.interceptorMappings().get(interceptor)); + beanDef.getConstructorArgumentValues().addIndexedArgumentValue(1, interceptor); + + String beanName = registrar.registerWithGeneratedName(beanDef); + compDefinition.addNestedComponent(new BeanComponentDefinition(beanDef, beanName)); + } + + registrar.registerComponent(compDefinition); + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcInterceptorsTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcInterceptorsTests.java new file mode 100644 index 0000000000..6f3e42cf10 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcInterceptorsTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2011 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.web.servlet.config; + +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Iterator; + +import org.easymock.Capture; +import org.junit.Test; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.theme.ThemeChangeInterceptor; + +/** + * Test fixture for {@link MvcInterceptors}. + * @author Rossen Stoyanchev + */ +public class MvcInterceptorsTests { + + @Test + public void testInterceptors() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcInterceptorsFeature.class); + ctx.refresh(); + + Iterator itr = ctx.getBeansOfType(MappedInterceptor.class).values().iterator(); + + MappedInterceptor interceptor = itr.next(); + assertTrue(interceptor.getInterceptor() instanceof UserRoleAuthorizationInterceptor); + assertNull(interceptor.getPathPatterns()); + + interceptor = itr.next(); + assertTrue(interceptor.getInterceptor() instanceof LocaleChangeInterceptor); + assertArrayEquals(new String[] { "/locale", "/locale/**" }, interceptor.getPathPatterns()); + + interceptor = itr.next(); + assertTrue(interceptor.getInterceptor() instanceof ThemeChangeInterceptor); + assertArrayEquals(new String[] { "/theme", "/theme/**" }, interceptor.getPathPatterns()); + + } + + @Test + public void validateNoInterceptors() { + ProblemReporter reporter = createMock(ProblemReporter.class); + Capture captured = new Capture(); + reporter.error(capture(captured)); + replay(reporter); + + boolean result = new MvcInterceptors().validate(reporter); + + assertFalse(result); + assertEquals("No interceptors defined.", captured.getValue().getMessage()); + } + + @Test + public void validateNullHandler() { + ProblemReporter reporter = createMock(ProblemReporter.class); + Capture captured = new Capture(); + reporter.error(capture(captured)); + replay(reporter); + + HandlerInterceptor[] interceptors = new HandlerInterceptor[] { null }; + boolean result = new MvcInterceptors().globalInterceptors(interceptors).validate(reporter); + + assertFalse(result); + assertTrue(captured.getValue().getMessage().contains("Null interceptor")); + } + + @Test + public void validateEmptyPath() { + ProblemReporter reporter = createMock(ProblemReporter.class); + Capture captured = new Capture(); + reporter.error(capture(captured)); + replay(reporter); + + HandlerInterceptor[] interceptors = new HandlerInterceptor[] { new LocaleChangeInterceptor() }; + String[] patterns = new String[] { "" }; + boolean result = new MvcInterceptors().mappedInterceptors(patterns, interceptors).validate(reporter); + + assertFalse(result); + assertTrue(captured.getValue().getMessage().startsWith("Empty path pattern specified for ")); + } + + @FeatureConfiguration + private static class MvcInterceptorsFeature { + + @SuppressWarnings("unused") + @Feature + public MvcInterceptors interceptors() { + return new MvcInterceptors() + .globalInterceptors(new UserRoleAuthorizationInterceptor()) + .mappedInterceptors(new String[] { "/locale", "/locale/**" }, new LocaleChangeInterceptor()) + .mappedInterceptors(new String[] { "/theme", "/theme/**"}, new ThemeChangeInterceptor()); + } + + } + +}