From 833b13bbbc6a93747dc5f6f39ffc57dd7d9c7083 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 30 Apr 2013 13:46:46 +0100 Subject: [PATCH] [bs-80] Add configurable / switchable web request trace logging (headers etc) * Added a bean post processor for the Spring Security filter chain (so you only get traces by default if security is on) * Every request is logged at trace level if the dump requests flag is on * Requests are also dumped to a TraceRepository for later analysis (very useful for tracing problems in real time when a support call comes in) [Fixes #48976001] --- pom.xml | 9 +- .../spring-bootstrap-service-sample/pom.xml | 4 +- ...ntextServiceBootstrapApplicationTests.java | 2 +- spring-bootstrap-service/pom.xml | 9 + .../service/ManagementAutoConfiguration.java | 10 +- .../service/ServiceAutoConfiguration.java | 23 ++- .../service/TraceAutoConfiguration.java | 69 +++++++ ...urationPropertiesBindingConfiguration.java | 8 + .../properties/ContainerProperties.java | 10 + .../trace/InMemoryTraceRepository.java | 60 ++++++ .../trace/SecurityFilterPostProcessor.java | 185 ++++++++++++++++++ .../bootstrap/service/trace/Trace.java | 46 +++++ .../service/trace/TraceEndpoint.java | 47 +++++ .../service/trace/TraceRepository.java | 34 ++++ .../trace/InMemoryTraceRepositoryTests.java | 44 +++++ .../SecurityFilterPostProcessorTests.java | 45 +++++ .../bind/PropertiesConfigurationFactory.java | 13 ++ .../PropertySourcesBindingPostProcessor.java | 11 ++ .../jetty/JettyEmbeddedServletContainer.java | 1 - .../bind/RelaxedDataBinderTests.java | 119 ++++------- 20 files changed, 652 insertions(+), 97 deletions(-) create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/TraceAutoConfiguration.java create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepository.java create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessor.java create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/Trace.java create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceEndpoint.java create mode 100644 spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceRepository.java create mode 100644 spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepositoryTests.java create mode 100644 spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessorTests.java diff --git a/pom.xml b/pom.xml index 1ffb2412821..599e76aeaa7 100644 --- a/pom.xml +++ b/pom.xml @@ -237,12 +237,17 @@ com.fasterxml.jackson.core jackson-databind - 2.1.4 + 2.2.0 com.fasterxml.jackson.core jackson-core - 2.1.4 + 2.2.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + 2.2.0 org.apache.tomcat.embed diff --git a/spring-bootstrap-samples/spring-bootstrap-service-sample/pom.xml b/spring-bootstrap-samples/spring-bootstrap-service-sample/pom.xml index ab1f1bf3fd6..f7287961336 100644 --- a/spring-bootstrap-samples/spring-bootstrap-service-sample/pom.xml +++ b/spring-bootstrap-samples/spring-bootstrap-service-sample/pom.xml @@ -17,7 +17,7 @@ tomcat - true + false @@ -33,7 +33,7 @@ jetty - false + true diff --git a/spring-bootstrap-samples/spring-bootstrap-service-sample/src/test/java/org/springframework/bootstrap/sample/service/VarzContextServiceBootstrapApplicationTests.java b/spring-bootstrap-samples/spring-bootstrap-service-sample/src/test/java/org/springframework/bootstrap/sample/service/VarzContextServiceBootstrapApplicationTests.java index 86d99e2ec1b..9b06c001643 100644 --- a/spring-bootstrap-samples/spring-bootstrap-service-sample/src/test/java/org/springframework/bootstrap/sample/service/VarzContextServiceBootstrapApplicationTests.java +++ b/spring-bootstrap-samples/spring-bootstrap-service-sample/src/test/java/org/springframework/bootstrap/sample/service/VarzContextServiceBootstrapApplicationTests.java @@ -91,7 +91,7 @@ public class VarzContextServiceBootstrapApplicationTests { @Test public void testHealthz() throws Exception { ResponseEntity entity = getRestTemplate().getForEntity( - "http://localhost:" + managementPort + "healthz", String.class); + "http://localhost:" + managementPort + "/healthz", String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals("ok", entity.getBody()); } diff --git a/spring-bootstrap-service/pom.xml b/spring-bootstrap-service/pom.xml index 3034052053d..e0e04fb0fa2 100644 --- a/spring-bootstrap-service/pom.xml +++ b/spring-bootstrap-service/pom.xml @@ -30,6 +30,11 @@ org.springframework spring-expression + + org.springframework + spring-test + test + javax.servlet javax.servlet-api @@ -65,6 +70,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.datatype + jackson-datatype-joda + org.springframework.security spring-security-javaconfig diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ManagementAutoConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ManagementAutoConfiguration.java index 5988cc36e7b..fe77d1ac7a6 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ManagementAutoConfiguration.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ManagementAutoConfiguration.java @@ -40,16 +40,16 @@ public class ManagementAutoConfiguration implements ApplicationContextAware, private ApplicationContext parent; private ConfigurableApplicationContext context; + @Autowired + private ContainerProperties configuration = new ContainerProperties(); + @ConditionalOnExpression("${container.port:8080} == ${container.management_port:8080}") @Configuration @Import({ VarzAutoConfiguration.class, HealthzAutoConfiguration.class, - ShutdownAutoConfiguration.class }) + ShutdownAutoConfiguration.class, TraceAutoConfiguration.class }) public static class ManagementEndpointsConfiguration { } - @Autowired - private ContainerProperties configuration = new ContainerProperties(); - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -73,7 +73,7 @@ public class ManagementAutoConfiguration implements ApplicationContextAware, context.setParent(this.parent); context.register(ManagementContainerConfiguration.class, VarzAutoConfiguration.class, HealthzAutoConfiguration.class, - ShutdownAutoConfiguration.class); + ShutdownAutoConfiguration.class, TraceAutoConfiguration.class); context.refresh(); this.context = context; } diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServiceAutoConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServiceAutoConfiguration.java index 9e6514a7278..38c92108ce8 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServiceAutoConfiguration.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServiceAutoConfiguration.java @@ -16,11 +16,19 @@ package org.springframework.bootstrap.autoconfigure.service; +import java.util.List; + import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration; import org.springframework.bootstrap.service.annotation.EnableConfigurationProperties; import org.springframework.bootstrap.service.properties.ContainerProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.joda.JodaModule; /** * {@link EnableAutoConfiguration Auto-configuration} for service apps. @@ -31,7 +39,20 @@ import org.springframework.context.annotation.Import; @Import({ ManagementAutoConfiguration.class, MetricAutoConfiguration.class, ContainerConfiguration.class, SecurityAutoConfiguration.class, MetricFilterAutoConfiguration.class }) -public class ServiceAutoConfiguration { +public class ServiceAutoConfiguration extends WebMvcConfigurationSupport { + + @Override + protected void configureMessageConverters(List> converters) { + addDefaultHttpMessageConverters(converters); + for (HttpMessageConverter converter : converters) { + if (converter instanceof MappingJackson2HttpMessageConverter) { + MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter; + jacksonConverter.getObjectMapper().registerModule(new JodaModule()); + jacksonConverter.getObjectMapper().disable( + SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + } + } /* * ContainerProperties has to be declared in a non-conditional bean, so that it gets diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/TraceAutoConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/TraceAutoConfiguration.java new file mode 100644 index 00000000000..b59aaa8ad89 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/TraceAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2013 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.bootstrap.autoconfigure.service; + +import javax.servlet.Servlet; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.bootstrap.context.annotation.ConditionalOnClass; +import org.springframework.bootstrap.context.annotation.ConditionalOnMissingBean; +import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration; +import org.springframework.bootstrap.service.properties.ContainerProperties; +import org.springframework.bootstrap.service.trace.InMemoryTraceRepository; +import org.springframework.bootstrap.service.trace.SecurityFilterPostProcessor; +import org.springframework.bootstrap.service.trace.TraceEndpoint; +import org.springframework.bootstrap.service.trace.TraceRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for /trace endpoint. + * + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) +@ConditionalOnMissingBean({ TraceEndpoint.class }) +public class TraceAutoConfiguration { + + @Autowired + private ContainerProperties configuration = new ContainerProperties(); + + @Autowired(required = false) + private TraceRepository traceRepository = new InMemoryTraceRepository(); + + @Bean + @ConditionalOnMissingBean({ TraceRepository.class }) + protected TraceRepository traceRepository() { + return this.traceRepository; + } + + @Bean + public SecurityFilterPostProcessor securityFilterPostProcessor() { + SecurityFilterPostProcessor processor = new SecurityFilterPostProcessor( + traceRepository()); + processor.setDumpRequests(this.configuration.isDumpRequests()); + return processor; + } + + @Bean + public TraceEndpoint traceEndpoint() { + return new TraceEndpoint(this.traceRepository); + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/annotation/ConfigurationPropertiesBindingConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/annotation/ConfigurationPropertiesBindingConfiguration.java index 9b996b590d4..4d87cc413c8 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/annotation/ConfigurationPropertiesBindingConfiguration.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/annotation/ConfigurationPropertiesBindingConfiguration.java @@ -19,11 +19,14 @@ package org.springframework.bootstrap.service.annotation; import java.lang.reflect.Field; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.bootstrap.bind.PropertySourcesBindingPostProcessor; import org.springframework.bootstrap.context.annotation.ConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.convert.ConversionService; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; @@ -47,6 +50,10 @@ public class ConfigurationPropertiesBindingConfiguration { @Autowired(required = false) private Environment environment; + @Autowired(required = false) + @Qualifier(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME) + private ConversionService conversionService; + /** * Lifecycle hook that binds application properties to any bean whose type is * decorated with {@link ConfigurationProperties} annotation. @@ -72,6 +79,7 @@ public class ConfigurationPropertiesBindingConfiguration { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); processor.setValidator(validator); + processor.setConversionService(this.conversionService); processor.setPropertySources(propertySources); return processor; } diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/ContainerProperties.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/ContainerProperties.java index 8fb50cb1c99..b237caba36f 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/ContainerProperties.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/ContainerProperties.java @@ -38,6 +38,8 @@ public class ContainerProperties { private Tomcat tomcat = new Tomcat(); + private boolean dumpRequests; + public Tomcat getTomcat() { return this.tomcat; } @@ -74,6 +76,14 @@ public class ContainerProperties { this.allowShutdown = allowShutdown; } + public boolean isDumpRequests() { + return this.dumpRequests; + } + + public void setDumpRequests(boolean dumpRequests) { + this.dumpRequests = dumpRequests; + } + public static class Tomcat { private String accessLogPattern; diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepository.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepository.java new file mode 100644 index 00000000000..cf6ab2fb154 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepository.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.joda.time.DateTime; + +/** + * @author Dave Syer + * + */ +public class InMemoryTraceRepository implements TraceRepository { + + private int capacity = 100; + + private List traces = new ArrayList(); + + /** + * @param capacity the capacity to set + */ + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + @Override + public List traces() { + synchronized (this.traces) { + return Collections.unmodifiableList(this.traces); + } + } + + @Override + public void add(Map map) { + Trace trace = new Trace(new DateTime(), map); + synchronized (this.traces) { + while (this.traces.size() >= this.capacity) { + this.traces.remove(0); + } + this.traces.add(trace); + } + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessor.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessor.java new file mode 100644 index 00000000000..e94d4281707 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessor.java @@ -0,0 +1,185 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Bean post processor that adds a filter to Spring Security. The filter (optionally) logs + * request headers at trace level and also sends the headers to a {@link TraceRepository} + * for later analysis. + * + * @author Luke Taylor + * @author Dave Syer + * + */ +public class SecurityFilterPostProcessor implements BeanPostProcessor { + + private final static Log logger = LogFactory + .getLog(SecurityFilterPostProcessor.class); + private boolean dumpRequests = false; + private List ignore = Collections.emptyList(); + + private TraceRepository traceRepository = new InMemoryTraceRepository(); + + /** + * @param traceRepository + */ + public SecurityFilterPostProcessor(TraceRepository traceRepository) { + super(); + this.traceRepository = traceRepository; + } + + /** + * List of filter chains which should be ignored completely. + */ + public void setIgnore(List ignore) { + Assert.notNull(ignore); + this.ignore = ignore; + } + + /** + * Debugging feature. If enabled, and trace logging is enabled + */ + public void setDumpRequests(boolean dumpRequests) { + this.dumpRequests = dumpRequests; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + + if (!this.ignore.contains(beanName)) { + if (bean instanceof FilterChainProxy) { + FilterChainProxy proxy = (FilterChainProxy) bean; + for (SecurityFilterChain filterChain : proxy.getFilterChains()) { + processFilterChain(filterChain, beanName); + } + } + if (bean instanceof SecurityFilterChain) { + processFilterChain((SecurityFilterChain) bean, beanName); + } + } + + return bean; + + } + + private void processFilterChain(SecurityFilterChain filterChain, String beanName) { + logger.info("Processing security filter chain " + beanName); + Filter loggingFilter = new WebRequestLoggingFilter(beanName); + filterChain.getFilters().add(0, loggingFilter); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + class WebRequestLoggingFilter implements Filter { + + final Log logger = LogFactory.getLog(WebRequestLoggingFilter.class); + private final String name; + private ObjectMapper objectMapper = new ObjectMapper(); + + WebRequestLoggingFilter(String name) { + this.name = name; + } + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + Map trace = getTrace(request); + @SuppressWarnings("unchecked") + Map headers = (Map) trace.get("headers"); + SecurityFilterPostProcessor.this.traceRepository.add(trace); + if (this.logger.isTraceEnabled()) { + this.logger.trace("Filter chain '" + this.name + "' processing request " + + request.getMethod() + " " + request.getRequestURI()); + if (SecurityFilterPostProcessor.this.dumpRequests) { + try { + this.logger.trace("Headers: " + + this.objectMapper.writeValueAsString(headers)); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot create JSON", e); + } + } + } + + chain.doFilter(request, response); + } + + protected Map getTrace(HttpServletRequest request) { + + Map map = new LinkedHashMap(); + Enumeration names = request.getHeaderNames(); + + while (names.hasMoreElements()) { + String name = names.nextElement(); + List values = Collections.list(request.getHeaders(name)); + Object value = values; + if (values.size() == 1) { + value = values.get(0); + } else if (values.isEmpty()) { + value = ""; + } + map.put(name, value); + + } + Map trace = new LinkedHashMap(); + trace.put("chain", this.name); + trace.put("method", request.getMethod()); + trace.put("path", request.getRequestURI()); + trace.put("headers", map); + return trace; + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + public void destroy() { + } + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/Trace.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/Trace.java new file mode 100644 index 00000000000..19618431815 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/Trace.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.Map; + +import org.joda.time.DateTime; + +/** + * @author Dave Syer + * + */ +public class Trace { + + private DateTime timestamp; + + private Map info; + + public Trace(DateTime timestamp, Map info) { + super(); + this.timestamp = timestamp; + this.info = info; + } + + public DateTime getTimestamp() { + return this.timestamp; + } + + public Map getInfo() { + return this.info; + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceEndpoint.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceEndpoint.java new file mode 100644 index 00000000000..79059b3f238 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceEndpoint.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.List; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Dave Syer + */ +@Controller +public class TraceEndpoint { + + private TraceRepository tracer; + + /** + * @param tracer + */ + public TraceEndpoint(TraceRepository tracer) { + super(); + this.tracer = tracer; + } + + @RequestMapping("${endpoints.trace.path:/trace}") + @ResponseBody + public List trace() { + return this.tracer.traces(); + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceRepository.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceRepository.java new file mode 100644 index 00000000000..1a857f07237 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/trace/TraceRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.List; +import java.util.Map; + +/** + * A repository for traces. Traces are simple documents (maps) with a timestamp, and can + * be used for analysing contextual information like HTTP headers. + * + * @author Dave Syer + * + */ +public interface TraceRepository { + + List traces(); + + void add(Map trace); + +} diff --git a/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepositoryTests.java b/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepositoryTests.java new file mode 100644 index 00000000000..79c3ca8728f --- /dev/null +++ b/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/InMemoryTraceRepositoryTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + * + */ +public class InMemoryTraceRepositoryTests { + + private InMemoryTraceRepository repository = new InMemoryTraceRepository(); + + @Test + public void capacityLimited() { + this.repository.setCapacity(2); + this.repository.add(Collections. singletonMap("foo", "bar")); + this.repository.add(Collections. singletonMap("bar", "foo")); + this.repository.add(Collections. singletonMap("bar", "bar")); + List traces = this.repository.traces(); + assertEquals(2, traces.size()); + assertEquals("bar", traces.get(1).getInfo().get("bar")); + } + +} diff --git a/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessorTests.java b/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessorTests.java new file mode 100644 index 00000000000..617515bd36c --- /dev/null +++ b/spring-bootstrap-service/src/test/java/org/springframework/bootstrap/service/trace/SecurityFilterPostProcessorTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.trace; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.bootstrap.service.trace.SecurityFilterPostProcessor.WebRequestLoggingFilter; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + * + */ +public class SecurityFilterPostProcessorTests { + + private SecurityFilterPostProcessor processor = new SecurityFilterPostProcessor( + new InMemoryTraceRepository()); + + @Test + public void filterDumpsRequest() { + WebRequestLoggingFilter filter = this.processor.new WebRequestLoggingFilter("foo"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addHeader("Accept", "application/json"); + Map trace = filter.getTrace(request); + assertEquals("GET", trace.get("method")); + assertEquals("/foo", trace.get("path")); + assertEquals("{Accept=application/json}", trace.get("headers").toString()); + } +} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertiesConfigurationFactory.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertiesConfigurationFactory.java index d1f869e3357..ba14c4242dc 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertiesConfigurationFactory.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertiesConfigurationFactory.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; +import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; import org.springframework.util.Assert; import org.springframework.validation.BindException; @@ -68,6 +69,8 @@ public class PropertiesConfigurationFactory implements FactoryBean, private String targetName; + private ConversionService conversionService; + /** * @param target the target object to bind too * @see #PropertiesConfigurationFactory(Class) @@ -142,6 +145,13 @@ public class PropertiesConfigurationFactory implements FactoryBean, this.propertySources = propertySources; } + /** + * @param conversionService the conversionService to set + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + /** * @param validator the validator to set */ @@ -176,6 +186,9 @@ public class PropertiesConfigurationFactory implements FactoryBean, if (this.validator != null) { dataBinder.setValidator(this.validator); } + if (this.conversionService != null) { + dataBinder.setConversionService(this.conversionService); + } dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields); dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields); customizeBinder(dataBinder); diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertySourcesBindingPostProcessor.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertySourcesBindingPostProcessor.java index 318ac9b9d1b..c66b902a4e3 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertySourcesBindingPostProcessor.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/PropertySourcesBindingPostProcessor.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.bootstrap.context.annotation.ConfigurationProperties; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; import org.springframework.validation.Validator; @@ -33,6 +34,8 @@ public class PropertySourcesBindingPostProcessor implements BeanPostProcessor { private Validator validator; + private ConversionService conversionService; + /** * @param propertySources */ @@ -47,6 +50,13 @@ public class PropertySourcesBindingPostProcessor implements BeanPostProcessor { this.validator = validator; } + /** + * @param conversionService the conversionService to set + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { @@ -63,6 +73,7 @@ public class PropertySourcesBindingPostProcessor implements BeanPostProcessor { bean); factory.setPropertySources(this.propertySources); factory.setValidator(this.validator); + factory.setConversionService(this.conversionService); factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields()); factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields()); String targetName = "".equals(annotation.value()) ? ("".equals(annotation diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/embedded/jetty/JettyEmbeddedServletContainer.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/embedded/jetty/JettyEmbeddedServletContainer.java index 7c533994a94..2ab7e2c2c75 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/embedded/jetty/JettyEmbeddedServletContainer.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/embedded/jetty/JettyEmbeddedServletContainer.java @@ -56,7 +56,6 @@ public class JettyEmbeddedServletContainer implements EmbeddedServletContainer { @Override public synchronized void stop() { try { - this.server.setGracefulShutdown(10000); this.server.stop(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java index 51598e11f04..887f031e7d8 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java @@ -32,8 +32,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.MutablePropertyValues; -import org.springframework.bootstrap.bind.RelaxedDataBinderTests.OAuthConfiguration.OAuthConfigurationValidator; import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.validation.BindingResult; @@ -55,6 +56,8 @@ public class RelaxedDataBinderTests { @Rule public ExpectedException expected = ExpectedException.none(); + private ConversionService conversionService; + @Test public void testBindString() throws Exception { VanillaTarget target = new VanillaTarget(); @@ -108,6 +111,23 @@ public class RelaxedDataBinderTests { assertEquals(123, target.getNested().getValue()); } + @Test + public void testBindNestedList() throws Exception { + TargetWithNestedList target = new TargetWithNestedList(); + bind(target, "nested: bar,foo"); + bind(target, "nested[0]: bar"); + bind(target, "nested[1]: foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + + @Test + public void testBindNestedListCommaDelimitedONly() throws Exception { + TargetWithNestedList target = new TargetWithNestedList(); + this.conversionService = new DefaultConversionService(); + bind(target, "nested: bar,foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + @Test public void testBindNestedMap() throws Exception { TargetWithNestedMap target = new TargetWithNestedMap(); @@ -174,96 +194,13 @@ public class RelaxedDataBinderTests { LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean(); validatorFactoryBean.afterPropertiesSet(); binder.setValidator(validatorFactoryBean); + binder.setConversionService(this.conversionService); binder.bind(new MutablePropertyValues(properties)); binder.validate(); return binder.getBindingResult(); } - @Documented - @Target({ ElementType.TYPE }) - @Retention(RUNTIME) - @Constraint(validatedBy = OAuthConfigurationValidator.class) - public @interface ValidOAuthConfiguration { - } - - @ValidOAuthConfiguration - public static class OAuthConfiguration { - - private Client client; - - private Map clients; - - public Client getClient() { - return this.client; - } - - public void setClient(Client client) { - this.client = client; - } - - public Map getClients() { - return this.clients; - } - - public void setClients(Map clients) { - this.clients = clients; - } - - public static class Client { - - private List autoapprove; - - public List getAutoapprove() { - return this.autoapprove; - } - - public void setAutoapprove(List autoapprove) { - this.autoapprove = autoapprove; - } - - } - - public static class OAuthClient { - - private String id; - - public String getId() { - return this.id; - } - - public void setId(String id) { - this.id = id; - } - - } - - public static class OAuthConfigurationValidator implements - ConstraintValidator { - - @Override - public void initialize(ValidOAuthConfiguration constraintAnnotation) { - } - - @Override - public boolean isValid(OAuthConfiguration value, - ConstraintValidatorContext context) { - boolean valid = true; - if (value.client != null && value.client.autoapprove != null) { - if (value.clients != null) { - context.buildConstraintViolationWithTemplate( - "Please use oauth.clients to specifiy autoapprove not client.autoapprove") - .addConstraintViolation(); - valid = false; - } - } - return valid; - } - - } - - } - @Documented @Target({ ElementType.FIELD }) @Retention(RUNTIME) @@ -332,6 +269,18 @@ public class RelaxedDataBinderTests { } } + public static class TargetWithNestedList { + private List nested; + + public List getNested() { + return this.nested; + } + + public void setNested(List nested) { + this.nested = nested; + } + } + public static class TargetWithNestedObject { private VanillaTarget nested;