diff --git a/pom.xml b/pom.xml
index 1ffb241282..599e76aeaa 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 ab1f1bf3fd..f728796133 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 86d99e2ec1..9b06c00164 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 3034052053..e0e04fb0fa 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 5988cc36e7..fe77d1ac7a 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 9e6514a727..38c92108ce 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 0000000000..b59aaa8ad8
--- /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 9b996b590d..4d87cc413c 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 8fb50cb1c9..b237caba36 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 0000000000..cf6ab2fb15
--- /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 0000000000..e94d428170
--- /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 0000000000..1961843181
--- /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 0000000000..79059b3f23
--- /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 0000000000..1a857f0723
--- /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 0000000000..79c3ca8728
--- /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 0000000000..617515bd36
--- /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 d1f869e335..ba14c4242d 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 318ac9b9d1..c66b902a4e 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 7c533994a9..2ab7e2c2c7 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 51598e11f0..887f031e7d 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;