Skip to content

Commit e18b09c

Browse files
committed
Add view resolver for JSON response by web controllers.
1 parent a570dac commit e18b09c

File tree

2 files changed

+213
-90
lines changed

2 files changed

+213
-90
lines changed

src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java

+8-90
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
*/
1919
package de.rwth.idsg.steve.config;
2020

21-
import com.fasterxml.jackson.databind.DeserializationFeature;
22-
import com.fasterxml.jackson.databind.ObjectMapper;
23-
import com.fasterxml.jackson.databind.SerializationFeature;
2421
import com.google.common.util.concurrent.ThreadFactoryBuilder;
2522
import com.mysql.cj.conf.PropertyKey;
2623
import com.zaxxer.hikari.HikariConfig;
@@ -31,6 +28,13 @@
3128
import de.rwth.idsg.steve.service.ReleaseCheckService;
3229
import de.rwth.idsg.steve.utils.DateTimeUtils;
3330
import de.rwth.idsg.steve.utils.InternetChecker;
31+
import java.util.concurrent.ExecutorService;
32+
import java.util.concurrent.ScheduledExecutorService;
33+
import java.util.concurrent.ScheduledThreadPoolExecutor;
34+
import java.util.concurrent.ThreadFactory;
35+
import java.util.concurrent.TimeUnit;
36+
import javax.annotation.PreDestroy;
37+
import javax.validation.Validator;
3438
import lombok.extern.slf4j.Slf4j;
3539
import org.jooq.DSLContext;
3640
import org.jooq.SQLDialect;
@@ -43,29 +47,8 @@
4347
import org.springframework.context.annotation.Configuration;
4448
import org.springframework.context.event.ContextRefreshedEvent;
4549
import org.springframework.context.event.EventListener;
46-
import org.springframework.core.Ordered;
47-
import org.springframework.format.support.FormattingConversionService;
48-
import org.springframework.http.converter.HttpMessageConverter;
49-
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
5050
import org.springframework.scheduling.annotation.EnableScheduling;
5151
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
52-
import org.springframework.web.accept.ContentNegotiationManager;
53-
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
54-
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
55-
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
56-
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
57-
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
58-
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
59-
import org.springframework.web.servlet.view.InternalResourceViewResolver;
60-
61-
import javax.annotation.PreDestroy;
62-
import javax.validation.Validator;
63-
import java.util.List;
64-
import java.util.concurrent.ExecutorService;
65-
import java.util.concurrent.ScheduledExecutorService;
66-
import java.util.concurrent.ScheduledThreadPoolExecutor;
67-
import java.util.concurrent.ThreadFactory;
68-
import java.util.concurrent.TimeUnit;
6952

7053
import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
7154

@@ -77,10 +60,9 @@
7760
*/
7861
@Slf4j
7962
@Configuration
80-
@EnableWebMvc
8163
@EnableScheduling
8264
@ComponentScan("de.rwth.idsg.steve")
83-
public class BeanConfiguration implements WebMvcConfigurer {
65+
public class BeanConfiguration {
8466

8567
private HikariDataSource dataSource;
8668
private ScheduledThreadPoolExecutor executor;
@@ -207,68 +189,4 @@ private void gracefulShutDown(ExecutorService executor) {
207189
executor.shutdownNow();
208190
}
209191
}
210-
211-
// -------------------------------------------------------------------------
212-
// Web config
213-
// -------------------------------------------------------------------------
214-
215-
/**
216-
* Resolver for JSP views/templates. Controller classes process the requests
217-
* and forward to JSP files for rendering.
218-
*/
219-
@Bean
220-
public InternalResourceViewResolver urlBasedViewResolver() {
221-
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
222-
resolver.setPrefix("/WEB-INF/views/");
223-
resolver.setSuffix(".jsp");
224-
return resolver;
225-
}
226-
227-
/**
228-
* Resource path for static content of the Web interface.
229-
*/
230-
@Override
231-
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
232-
registry.addResourceHandler("/static/**").addResourceLocations("static/");
233-
}
234-
235-
@Override
236-
public void addViewControllers(ViewControllerRegistry registry) {
237-
registry.addViewController("/manager/signin").setViewName("signin");
238-
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
239-
}
240-
241-
// -------------------------------------------------------------------------
242-
// API config
243-
// -------------------------------------------------------------------------
244-
245-
@Override
246-
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
247-
for (HttpMessageConverter<?> converter : converters) {
248-
if (converter instanceof MappingJackson2HttpMessageConverter) {
249-
MappingJackson2HttpMessageConverter conv = (MappingJackson2HttpMessageConverter) converter;
250-
ObjectMapper objectMapper = conv.getObjectMapper();
251-
// if the client sends unknown props, just ignore them instead of failing
252-
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
253-
// default is true
254-
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
255-
break;
256-
}
257-
}
258-
}
259-
260-
/**
261-
* Find the ObjectMapper used in MappingJackson2HttpMessageConverter and initialized by Spring automatically.
262-
* MappingJackson2HttpMessageConverter is not a Bean. It is created in {@link WebMvcConfigurationSupport#addDefaultHttpMessageConverters(List)}.
263-
* Therefore, we have to access it via proxies that reference it. RequestMappingHandlerAdapter is a Bean, created in
264-
* {@link WebMvcConfigurationSupport#requestMappingHandlerAdapter(ContentNegotiationManager, FormattingConversionService, org.springframework.validation.Validator)}.
265-
*/
266-
@Bean
267-
public ObjectMapper objectMapper(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
268-
return requestMappingHandlerAdapter.getMessageConverters().stream()
269-
.filter(converter -> converter instanceof MappingJackson2HttpMessageConverter)
270-
.findAny()
271-
.map(conv -> ((MappingJackson2HttpMessageConverter) conv).getObjectMapper())
272-
.orElseThrow(() -> new RuntimeException("There is no MappingJackson2HttpMessageConverter in Spring context"));
273-
}
274192
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
3+
* Copyright (C) 2013-2023 SteVe Community Team
4+
* All Rights Reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
package de.rwth.idsg.steve.config;
20+
21+
import com.fasterxml.jackson.databind.DeserializationFeature;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import com.fasterxml.jackson.databind.SerializationFeature;
24+
import java.util.ArrayList;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Locale;
28+
import java.util.Map;
29+
import java.util.Set;
30+
import java.util.stream.Collectors;
31+
import javax.servlet.http.HttpServletRequest;
32+
import javax.servlet.http.HttpServletResponse;
33+
import lombok.extern.slf4j.Slf4j;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.core.Ordered;
37+
import org.springframework.format.support.FormattingConversionService;
38+
import org.springframework.http.converter.HttpMessageConverter;
39+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
40+
import org.springframework.validation.BeanPropertyBindingResult;
41+
import org.springframework.validation.BindingResult;
42+
import org.springframework.web.accept.ContentNegotiationManager;
43+
import org.springframework.web.servlet.View;
44+
import org.springframework.web.servlet.ViewResolver;
45+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
46+
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
47+
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
48+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
49+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
50+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
51+
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
52+
import org.springframework.web.servlet.view.InternalResourceViewResolver;
53+
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
54+
55+
import static de.rwth.idsg.steve.web.GlobalControllerAdvice.EXCEPTION_MODEL_KEY;
56+
import static de.rwth.idsg.steve.web.GlobalControllerAdvice.EXCEPTION_VIEW_NAME;
57+
58+
/**
59+
* Configuration and beans of Spring Framework.
60+
*
61+
* @author Sevket Goekay <[email protected]>
62+
* @author Emeric Chardiny <[email protected]>
63+
* @since 15.08.2014
64+
*/
65+
@Slf4j
66+
@Configuration
67+
@EnableWebMvc
68+
public class WebConfiguration implements WebMvcConfigurer {
69+
// -------------------------------------------------------------------------
70+
// Web config
71+
// -------------------------------------------------------------------------
72+
73+
private View resolveViewName(String viewName, Locale locale) {
74+
if (viewName.equals(EXCEPTION_VIEW_NAME)) {
75+
return jsonErrorView;
76+
}
77+
return jsonView;
78+
}
79+
80+
/**
81+
* JSON view for nominal case in response to 'Accept: applicatiob/json' HTTP Header
82+
* GET AND POST requests are supported for many Controllers
83+
*/
84+
private MappingJackson2JsonView jsonView = new MappingJackson2JsonView() {
85+
/**
86+
* In case of success, controllers redirect request to overview page, with a HTTP 302 redirect.
87+
* In case of error, we change this behavior:
88+
* - put the binding result containing errors back in model
89+
* - set response status to HTTP 400
90+
*/
91+
@Override
92+
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
93+
// lookup any BindingResult entry with errors
94+
Set<Object> set =
95+
model.entrySet()
96+
.stream()
97+
.filter(entry -> entry.getKey().startsWith(BindingResult.MODEL_KEY_PREFIX))
98+
.map(Map.Entry::getValue)
99+
.collect(Collectors.toSet());
100+
101+
List<Object> errors = new ArrayList<>();
102+
for (Object o : set)
103+
errors.addAll(((BeanPropertyBindingResult) o).getAllErrors());
104+
105+
// if no errors, return back to normal behavior
106+
if (errors.isEmpty())
107+
return super.createMergedOutputModel(model, request, response);
108+
109+
// otherwise put errors into model and switch http response status
110+
Map<String, Object> result = new HashMap<>();
111+
result.put("errors", errors);
112+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
113+
return result;
114+
}
115+
};
116+
// private MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
117+
118+
/**
119+
* JSON view in case of Exception
120+
*/
121+
private MappingJackson2JsonView jsonErrorView = new MappingJackson2JsonView() {
122+
@Override
123+
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
124+
Map<String, Object> result = new HashMap<>();
125+
Exception e = (Exception) model.get(EXCEPTION_MODEL_KEY);
126+
result.put("exception", e.getClass().getCanonicalName());
127+
result.put("message", e.getMessage());
128+
129+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
130+
super.renderMergedOutputModel(result, request, response);
131+
}
132+
};
133+
134+
/**
135+
* Resolver for either JSP views/templates or JSON response.
136+
*/
137+
@Bean
138+
public ViewResolver contentNegotiatingViewResolver(
139+
ContentNegotiationManager manager) {
140+
141+
List<ViewResolver> resolvers = new ArrayList<>();
142+
143+
// Resolver for JSP views/templates when http request header is "Accept: application/x-www-form-urlencoded"
144+
// Controller classes process the requests and forward to JSP files for rendering.
145+
resolvers.add(new InternalResourceViewResolver("/WEB-INF/views/", ".jsp"));
146+
147+
// Resolver for JSON body response when http request header is "Accept: application/json"
148+
resolvers.add(this::resolveViewName);
149+
150+
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
151+
resolver.setViewResolvers(resolvers);
152+
resolver.setContentNegotiationManager(manager);
153+
154+
return resolver;
155+
}
156+
157+
/**
158+
* Resource path for static content of the Web interface.
159+
*/
160+
@Override
161+
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
162+
registry.addResourceHandler("/static/**").addResourceLocations("static/");
163+
}
164+
165+
@Override
166+
public void addViewControllers(ViewControllerRegistry registry) {
167+
registry.addViewController("/manager/signin").setViewName("signin");
168+
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
169+
}
170+
171+
// -------------------------------------------------------------------------
172+
// API config
173+
// -------------------------------------------------------------------------
174+
175+
@Override
176+
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
177+
for (HttpMessageConverter<?> converter : converters) {
178+
if (converter instanceof MappingJackson2HttpMessageConverter) {
179+
MappingJackson2HttpMessageConverter conv = (MappingJackson2HttpMessageConverter) converter;
180+
ObjectMapper objectMapper = conv.getObjectMapper();
181+
// if the client sends unknown props, just ignore them instead of failing
182+
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
183+
// default is true
184+
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
185+
break;
186+
}
187+
}
188+
}
189+
190+
/**
191+
* Find the ObjectMapper used in MappingJackson2HttpMessageConverter and initialized by Spring automatically.
192+
* MappingJackson2HttpMessageConverter is not a Bean. It is created in {@link WebMvcConfigurationSupport#addDefaultHttpMessageConverters(List)}.
193+
* Therefore, we have to access it via proxies that reference it. RequestMappingHandlerAdapter is a Bean, created in
194+
* {@link WebMvcConfigurationSupport#requestMappingHandlerAdapter(ContentNegotiationManager, FormattingConversionService, org.springframework.validation.Validator)}.
195+
*/
196+
@Bean
197+
public ObjectMapper objectMapper(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
198+
return requestMappingHandlerAdapter.getMessageConverters().stream()
199+
.filter(converter -> converter instanceof MappingJackson2HttpMessageConverter)
200+
.findAny()
201+
.map(conv -> ((MappingJackson2HttpMessageConverter) conv).getObjectMapper())
202+
.orElseThrow(() -> new RuntimeException("There is no MappingJackson2HttpMessageConverter in Spring context"));
203+
}
204+
205+
}

0 commit comments

Comments
 (0)