Adding PathVariable changes view path on RequestMapping - spring-mvc

I have a view resolver:
#Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("WEB-INF/jsp/");
resolver.setSuffix(".jsp");
return resolver;
}
and a controller:
#Controller
public class WorkflowListController {
#RequestMapping(path = "/workflowlist", method = RequestMethod.GET)
public ModelAndView index() throws LoginFailureException, PacketException,
NetworkException {
String profile = "dev";
List<WorkflowInformation> workflows = workflows(profile);
Map<String, Object> map = new HashMap<String, Object>();
map.put("profile", profile);
map.put("workflows", workflows);
return new ModelAndView("workflowlist", map);
}
}
and when I call the page http://127.0.0.1:8090/workflowlist it serves the jsp from src/main/webapp/WEB-INF/jsp/workflowlist.jsp. That all seems to work well.
However when I try to add a #PathVariable:
#RequestMapping(path = "/workflowlist/{profile}", method = RequestMethod.GET)
public ModelAndView workflowlist(#PathVariable String profile)
throws LoginFailureException, PacketException, NetworkException {
List<WorkflowInformation> workflows = workflows(profile);
Map<String, Object> map = new HashMap<String, Object>();
map.put("profile", profile);
map.put("workflows", workflows);
return new ModelAndView("workflowlist", map);
}
When I call the page http://127.0.0.1:8090/workflowlist/dev gives the following message:
There was an unexpected error (type=Not Found, status=404).
/workflowlist/WEB-INF/jsp/workflowlist.jsp
Can someone explain why I'm returning the same view name in both cases but in the second example it is behaving differently?
How can I get it to work?

The problem was with my viewResolver:
resolver.setPrefix("WEB-INF/jsp/");
should have been:
resolver.setPrefix("/WEB-INF/jsp/");
With a / at the front the path is taken from the root (webapps folder) but when the / is missing it becomes a relative path.
I never got an answer as to why the view resolver only took the directory part of the path but that's what appeared to happen.
It's probably so you can define subtrees of views with different roots.

Related

ASP.NET Core ControllerContext vs ActionContext in UrlHelper

I am trying to implement pagination in my Asp.net core 2 API. To create pagination links, I am using UrlHelper. The constructor for UrlHelper requires the context in which the action runs.
The examples I've seen have been using below configuration in startup and then injecting IUrlHelper into the controller where it is needed.
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper>(x => {
var actionContext = x.GetRequiredService<IActionContextAccessor>().ActionContext;
var factory = x.GetRequiredService<IUrlHelperFactory>();
return factory.GetUrlHelper(actionContext);
});
But controllers also have ControllerContext which derives from ActionContext (https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllercontext?view=aspnetcore-2.1).
I am able to do the following:
public Object GetAll() //ignore object return, for test purposes
{
var urlHelper = new UrlHelper(ControllerContext);
var nextLink = urlHelper.Link("GetPosts", new { page = 1, pageSize = 3 });
//return _context.Posts;
return new
{
NextPageLink = nextLink,
Results = _context.Posts,
test = ControllerContext.RouteData.Values
};
}
The code above is able to create the links correctly. I don't have a firm grasp on the nuances of the framework so I am wondering if above is a correct way to initialize UrlHelper. Will this lead to problems? If you can point me in the direction of some documentation around this or explain the reason behind if the approach is good/bad, that would be very helpful.
What you have can work.
It does however tightly couple the controller to an implementation concern.
If you have need for the helper you can follow a similar format to what was configured at startup by injecting the IUrlHelperFactory into the controller and getting the helper using the controller's ControllerContext, which as you have already discovered, derives from ActionContext
public class MyController : Controller {
private readonly IUrlHelperFactory factory;
//...other dependencies
public MyController(IUrlHelperFactory factory) {
this.factory = factory;
//...other dependencies
}
public IActionResult GetAll() {
var urlHelper = factory.GetUrlHelper(ControllerContext);
var nextLink = urlHelper.Link("GetPosts", new { page = 1, pageSize = 3 });
return Ok(new {
NextPageLink = nextLink,
Results = _context.Posts,
test = ControllerContext.RouteData.Values
});
}
//...other actions
}

Is it possible to run Spring WebFlux and MVC (CXF, Shiro, etc.) services together in Undertow?

We are looking at implementing a few services using the new Spring 5 "Reactive" API.
We currently use, somewhat dependent on MVC, Apache CXF and Apache Shiro for our REST services and security. All of this runs in Undertow now.
We can get one or the other to work but not both together. It appears when we switch over to the reactive application it knocks out the servlets, filters, etc. Conversely, when we use the MVC-style application it does not see the reactive handlers.
Is it possible to run the Spring 5 Reactive services alongside REST/servlet/filter components or customize the SpringBoot startup to run REST and Reactive services on different ports?
Update:
I "seem" to be able to get the reactive handlers working doing this but I don't know if this is the right approach.
#Bean
RouterFunction<ServerResponse> routeGoodbye(TrackingHandler endpoint)
{
RouterFunction<ServerResponse> route = RouterFunctions
.route(GET("/api/rx/goodbye")
.and(accept(MediaType.TEXT_PLAIN)), endpoint::trackRedirect2);
return route;
}
#Bean
RouterFunction<ServerResponse> routeHello(TrackingHandler endpoint)
{
RouterFunction<ServerResponse> route = RouterFunctions
.route(GET("/api/rx/hello")
.and(accept(MediaType.TEXT_PLAIN)), endpoint::trackRedirect);
return route;
}
#Bean
ContextPathCompositeHandler servletReactiveRouteHandler(TrackingHandler handler)
{
final Map<String, HttpHandler> handlers = new HashMap<>();
handlers.put("/hello", toHttpHandler((this.routeHello(handler))));
handlers.put("/goodbye", toHttpHandler(this.routeGoodbye(handler)));
return new ContextPathCompositeHandler(handlers);
}
#Bean
public ServletRegistrationBean servletRegistrationBean(final ContextPathCompositeHandler handlers)
{
ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(
new ReactiveServlet(handlers),
"/api/rx/*");
registrationBean.setLoadOnStartup(1);
registrationBean.setAsyncSupported(true);
return registrationBean;
}
#Bean
TrackingHandler trackingEndpoint(final TrackingService trackingService)
{
return new TrackingHandler(trackingService,
null,
false);
}
public class ReactiveServlet extends ServletHttpHandlerAdapter
{
ReactiveServlet(final HttpHandler httpHandler)
{
super(httpHandler);
}
}
Ok, after playing around with this for too long I finally seemed to be able to cobble together a solution that works for me. Hopefully this is the right way to do what I need to do.
Now, executing normal CXF RESTful routes shows me Undertow using a blocking task and executing my Reactive routes shows me undertow using NIO directly. When I tried using the ServletHttpHandler it looked like it was just invoking the service as a Servlet 3 async call.
The handlers are running completely separate from each other and allows me to run my REST services beside my reactive services.
1) Create an annotation that will be used to map the RouterFunction to an Undertow Handler
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Target({ElementType.METHOD, ElementType.TYPE})
public #interface ReactiveHandler
{
String value();
}
2) Create an UndertowReactiveHandler "Provider" so that I can lazily get the injected RouterFunction and return the UndertowHttpHandler when I configure Undertow.
final class UndertowReactiveHandlerProvider implements Provider<UndertowHttpHandlerAdapter>
{
#Inject
private ApplicationContext context;
private String path;
private String beanName;
#Override
public UndertowHttpHandlerAdapter get()
{
final RouterFunction router = context.getBean(beanName, RouterFunction.class);
return new UndertowHttpHandlerAdapter(toHttpHandler(router));
}
public String getPath()
{
return path;
}
public void setPath(final String path)
{
this.path = path;
}
public void setBeanName(final String beanName)
{
this.beanName = beanName;
}
}
3) Create the NonBLockingHandlerFactory (implements BeanFactoryPostProcessor). This looks for any of my #Bean methods that have been annotated with "ReactiveHandler" and then dynamically creates a "UndertowReactiveHandlerProvider" bean for each annotated router function which is used later to provide the handlers to Undertow.
#Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException
{
final BeanDefinitionRegistry registry = (BeanDefinitionRegistry)configurableListableBeanFactory;
final String[] beanDefinitions = registry.getBeanDefinitionNames();
for (String name : beanDefinitions)
{
final BeanDefinition beanDefinition = registry.getBeanDefinition(name);
if (beanDefinition instanceof AnnotatedBeanDefinition
&& beanDefinition.getSource() instanceof MethodMetadata)
{
final MethodMetadata beanMethod = (MethodMetadata)beanDefinition.getSource();
final String annotationType = ReactiveHandler.class.getName();
if (beanMethod.isAnnotated(annotationType))
{
//Get the current bean details
final String beanName = beanMethod.getMethodName();
final Map<String, Object> attributes = beanMethod.getAnnotationAttributes(annotationType);
//Create the new bean definition
final GenericBeanDefinition rxHandler = new GenericBeanDefinition();
rxHandler.setBeanClass(UndertowReactiveHandlerProvider.class);
//Set the new bean properties
MutablePropertyValues mpv = new MutablePropertyValues();
mpv.add("beanName", beanName);
mpv.add("path", attributes.get("value"));
rxHandler.setPropertyValues(mpv);
//Register the new bean (Undertow handler) with a matching route suffix
registry.registerBeanDefinition(beanName + "RxHandler", rxHandler);
}
}
}
}
4) Create the Undertow ServletExtension. This looks for any UndertowReactiveHandlerProviders and adds it as an UndertowHttpHandler.
public class NonBlockingHandlerExtension implements ServletExtension
{
#Override
public void handleDeployment(DeploymentInfo deploymentInfo, final ServletContext servletContext)
{
deploymentInfo.addInitialHandlerChainWrapper(handler -> {
final WebApplicationContext ctx = getWebApplicationContext(servletContext);
//Get all of the reactive handler providers
final Map<String, UndertowReactiveHandlerProvider> providers =
ctx.getBeansOfType(UndertowReactiveHandlerProvider.class);
//Create the root handler
final PathHandler rootHandler = new PathHandler();
rootHandler.addPrefixPath("/", handler);
//Iterate the providers and add to the root handler
for (Map.Entry<String, UndertowReactiveHandlerProvider> p : providers.entrySet())
{
final UndertowReactiveHandlerProvider provider = p.getValue();
//Append the HttpHandler to the root
rootHandler.addPrefixPath(
provider.getPath(),
provider.get());
}
//Return the root handler
return rootHandler;
});
}
}
5) Under META-INF/services create a "io.undertow.servlet.ServletExtension" file.
com.mypackage.NonBlockingHandlerExtension
6) Create a SpringBoot AutoConfiguration that loads the post processor if Undertow is on the classpath.
#Configuration
#ConditionalOnClass(Undertow.class)
public class UndertowAutoConfiguration
{
#Bean
BeanFactoryPostProcessor nonBlockingHandlerFactoryPostProcessor()
{
return new NonBlockingHandlerFactoryPostProcessor();
}
}
7) Annotate any RouterFunctions that I want to map to an UndertowHandler.
#Bean
#ReactiveHandler("/api/rx/service")
RouterFunction<ServerResponse> routeTracking(TrackingHandler handler)
{
RouterFunction<ServerResponse> route = RouterFunctions
.nest(path("/api/rx/service"), route(
GET("/{cid}.gif"), handler::trackGif).andRoute(
GET("/{cid}"), handler::trackAll));
return route;
}
With this I can call my REST services (and Shiro works with them), use Swagger2 with my REST services, and call my Reactive services (and they do not use Shiro) in the same SpringBoot application.
In my logs, the REST call shows Undertow using the blocking (task-#) handler. The Reactive call shows Undertow using the non-blocking (I/O-# and nioEventLoopGroup) handler

jasperreporter view in springmvc format miss error

Hello guys I use jasperreporter as a report lib ,use spring mvc as a controller
,i put the two togeter with this
#Bean
JasperReportsViewResolver getJasperReportsViewResolver() {
JasperReportsViewResolver resolver = new JasperReportsViewResolver();
resolver.setPrefix("classpath:/jasperreports/");
resolver.setSuffix(".jasper");
resolver.setReportDataKey("datasource");
resolver.setViewNames("rpt_*");
resolver.setViewClass(JasperReportsMultiFormatView.class);
resolver.setOrder(0);
return resolver;
}
#Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setOrder(1);
return resolver;
}
#RequestMapping(value = "helloReport4", method = RequestMethod.GET)
public ModelAndView getRpt4(ModelMap modelMap, ModelAndView modelAndView) {
modelMap.put("datasource", getWidgets());
modelMap.put("format", "pdf");
modelAndView = new ModelAndView("rpt_HelloWorld", modelMap);
return modelAndView;
}
the above is config and controller
then i put compiled xxxx.jasper to somewhere of webapp directory
but while the tomcat run the few hours ,about ten or more, when request the jasper view by browser ,it makes error like [ java.io.FileNotFoundException :can't load resource of xxxx.html.jasper],and in my path ,put some xxxx.jasper
files ,but i have set the reporter format to pdf only ,can someone tell me about it!~

Java junit test change Locale in Spring SimpleFormController

For a SpringMVC, I have a SimpleFormController with a simple method which changes language for user by changing locale (i18n).
//localization
public void localize(HttpServletRequest request, HttpServletResponse response, String language) throws Exception {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver != null) {
LocaleEditor localeEditor = new LocaleEditor();
localeEditor.setAsText(language);
// set the new locale
localeResolver.setLocale(request, response,
(Locale) localeEditor.getValue());
}
}
And the code works fine while using the app. However I want to do the Junit test for this method and the following is what I have come up with so far:
public class LoginPostControllerTest extends TestCase {
public void testLocalize() throws Exception {
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
Locale frenchLocale = Locale.CANADA_FRENCH;
mockRequest.addPreferredLocale(frenchLocale);
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
mockRequest.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, localeResolver);
String language = "zh_CN";
LoginPostController loginPostControllerTest = new LoginPostController();
loginPostControllerTest.localize(mockRequest, mockResponse, language);
System.out.println(mockRequest.getLocale().toString());
}
}
but it prints out "fr_CA" not "zh_CN". Can somebody provide a better Junit test strategy for this?
you need obtain again the localeResolver on your test
LocaleResolver resolver = RequestContextUtils.getLocaleResolver(mockRequest);
System.out.println(mockRequest.getLocale().toString());
System.out.println(resolver.resolveLocale(mockRequest).toString());
assertTrue(!mockRequest.getLocale().equals(resolver.resolveLocale(mockRequest)));

How to manage validation in GET form and submit POST form?

the enviroment is Spring 3.0 with new function Vallidation.
I create an annotated controller (ResetUserPasswordController) which manages a showForm on HTTP.GET and the submit form on HTTP.POST. The function is a reset user password requested by email : the user access previously to another form, where i fill is email address and a recaptcha control, if recaptcha is correct, the user receive a mail with a link which contains a paramter. The two methods (on HTTP.GET, and HTTP.POST) have two different command bean have different paramters(i choice two differents beans to manage the validation process in two diffent validators classes). Probably you are questioning : why do you define two differents commands? I have defined the following role : Every bussiness and basic (like notnull validation etc) validation process must be managed by a validator class which supports a specific command bean
I want to create the istance of command bean managed by the POST, in the GET method, but during some tests I realized that this can be not correct beacuse if the validation process goes bad, I have all errors on the input command which is different from which i'm gogin to put in the returned ModelAndView.
Someone has some suggestion to manage correctly this scenario?
#RequestMapping(method = RequestMethod.POST)
public ModelAndView processSubmit(#Valid #ModelAttribute("command") ResetUserPasswordCommand command, BindingResult result, HttpServletRequest request, HttpServletResponse response) {
getValidator().validate(command, result);
if (result.hasErrors()) {
// TODO : implements error page.
return new ModelAndView();
} else {
Map<String, Object> model = new HashMap<String, Object>();
try {
PasswordChangeRequest passwordChangeRequest = getUserService().findPasswordChangeRequest(command.getUuid());
getUserService().updateUserPassword(command.getUuid(), command.getPassword());
autoLogin(request, response, passwordChangeRequest.getAccount(), command.getPassword());
} catch (ApplicationThrowable aex) {
return new ModelAndView("responseKO", model);
}
return new ModelAndView("Home", model);
}
}
#RequestMapping(method = RequestMethod.GET)
public ModelAndView setupForm(#Valid #ModelAttribute("command") ResetUserPasswordFormCommand command, BindingResult result) {
getFormValidator().validate(command, result);
if (result.hasErrors()) {
// TODO : implements error page.
return new ModelAndView();
} else {
Map<String, Object> model = new HashMap<String, Object>();
ResetUserPasswordCommand resetUserPasswordCommand = new ResetUserPasswordCommand();
resetUserPasswordCommand.setUuid(command.getUuid());
model.put("command", resetUserPasswordCommand);
model.put("reCaptchaHTML", getReCaptchaService().getReCaptchaObjectNoSSL().createRecaptchaHtml(null, null));
return new ModelAndView("user/ResetUserPassword", model);
}
}

Resources