Spring Content-Negotiation with Path Extension (set Accept-Header via extension?!) - json

I want to have the same URL Path handles by two different #Controller methods, which partially works. Here's what I've got:
WebMvcConfig:
public class WebMvcConfig extends WebMvcConfigurationSupport {
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter json = new MappingJackson2HttpMessageConverter();
json.setObjectMapper(objectMapper());
converters.add(json);
VCardMessageConverter vcard = new VCardMessageConverter();
converters.add(vcard);
}
#Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("vcf", MediaType.TEXT_VCARD)
;
}
}
Controller:
#RestController
#RequestMapping("test")
class UserProfileController {
#RequestMapping(value="profile", method=RequestMethod.GET, produces=MediaType.TEXT_VCARD_VALUE)
VCard getProfileVCard() {
Profile p = service.getProfile();
VCard v = p.getVCard();
return v;
}
#RequestMapping(value="profile", method=RequestMethod.GET)
Profile getProfile() {
Profile p = service.getProfile();
return p;
}
}
Current behavior:
The good ones:
GET /test/profile (with Accept=*/*) calls getProfile()
GET /test/profile (with Accept=application/json) calls getProfile()
GET /test/profile.json (with Accept=*/*) calls getProfile()
GET /test/profile.json (with Accept=text/vcard) returns 406 NOT ACCEPTABLE
GET /test/profile (with Accept=text/vcard) calls getProfileVCard()
GET /test/profile.vcf (with Accept=text/vcard) calls getProfileVCard()
The faulty one:
GET /test/profile.vcf (with Accept=*/*) calls getProfile() and returns 406 NOT ACCEPTABLE.
Why does the wrong method get called? I thought I set favorPathExtension(true) in my config in order to make Spring override the Accept-Header when some Path Extension is set?
EDIT:
I now also set favorPathExtension(true).ignoreAcceptHeader(true).favorParameter(true) in my config and it still doesn't work, even profile?format=vcf and profile.vcf?format=vcf don't work

Related

Camel - CSV Headers setting not working

I have CSV files without headers. Since I'm using 'useMaps' I want to specify the headers dynamically. If I set headers statically and then use in route it works fine as below Approach 1 -
#Component
public class BulkActionRoutes extends RouteBuilder {
#Override
public void configure() throws Exception {
CsvDataFormat csv = new CsvDataFormat(",");
csv.setUseMaps(true);
ArrayList<String> list = new ArrayList<String>();
list.add("DeviceName");
list.add("Brand");
list.add("status");
list.add("type");
list.add("features_c");
list.add("battery_c");
list.add("colors");
csv.setHeader(list);
from("direct:bulkImport")
.convertBodyTo(String.class)
.unmarshal(csv)
.split(body()).streaming()
.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
GenericObjectModel model = null;
HashMap<String, String> csvRecord = (HashMap<String, String>)exchange.getIn().getBody();
}
});
}
}
However, if the list is passed via Camel headers as below then it does not work Approach 2 -
#Component
public class BulkActionRoutes extends RouteBuilder {
#Override
public void configure() throws Exception {
CsvDataFormat csv = new CsvDataFormat(",");
csv.setUseMaps(true);
from("direct:bulkImport")
.convertBodyTo(String.class)
.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
ArrayList<String> fileHeaders = (ArrayList<String>)headers.get(Constants.FILE_HEADER_LIST);
if (fileHeaders != null && fileHeaders.size() > 0) {
csv.setHeader(fileHeaders);
}
}
})
.unmarshal(csv)
.split(body()).streaming()
.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
GenericObjectModel model = null;
HashMap<String, String> csvRecord = (HashMap<String, String>)exchange.getIn().getBody();
}
});
}
}
What could be missing in the Approach 2?
The big difference between approach 1 and 2 is the scope.
In approach 1 you fully configure the CSV data format. This is all done when the Camel Context is created, since the data format is shared within the Camel Context. When messages are processed, it is the same config for all messages.
In approach 2 you just configure the basics globally. The header configuration is within the route and therefore can change for every single message. Every message would overwrite the header configuration of the context-global data format instance.
Without being sure about this, I guess that it is not possible to change a context-global DataFormat inside the routes.
What would you expect (just for example) when messages are processed in parallel? They would overwrite the header config against each other.
As an alternative, you could use a POJO where you can do your dynamic marshal / unmarshal from Java code.

JAX-RS Exception Mapper not working in Grizzly container

Working on a Jersey web application with a team, as the project got bigger and bigger, we decided to switch from Tomcat to Grizzly to allow deploying parts of the project on different port numbers. What I've found out now, that the custom exception handling we have fails to work now, instead I always get the grizzly html page.
Example exception:
public class DataNotFoundException extends RuntimeException{
private static final long serialVersionUID = -1622261264080480479L;
public DataNotFoundException(String message) {
super(message);
System.out.println("exception constructor called"); //this prints
}
}
Mapper:
#Provider
public class DataNotFoundExceptionMapper implements ExceptionMapper<DataNotFoundException>{
public DataNotFoundExceptionMapper() {
System.out.println("mapper constructor called"); //doesnt print
}
#Override
public Response toResponse(DataNotFoundException ex) {
System.out.println("toResponse called"); //doesnt print
ErrorMessage errorMessage = new ErrorMessage(ex.getMessage(), 404, "No documentation yet.");
return Response.status(Status.NOT_FOUND)
.entity(errorMessage)
.build();
//ErrorMessage is a simple POJO with 2 string and 1 int field
}
}
I'm not sure where is the problem source, if needed I can provide more information/code. What's the problem, what can I try?
EDIT:
Main.class:
public class Main {
/**
* Main method.
* #param args
* #throws Exception
*/
public static void main(String[] args) throws Exception {
...
List<ServerInfo> serverList = new ArrayList<ServerInfo>();
serverList.add(new ServerInfo(
"api",8450,
new ResourceConfig().registerClasses(
the.package.was.here.ApiResource.class)
));
for(ServerInfo server : serverList) {
server.start();
}
System.out.println("Press enter to exit...");
System.in.read();
for(ServerInfo server : serverList) {
server.stop();
}
}
}
EDIT2:
based on this question I've tried using this ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, "true"property, which only helped a little. I still get the html grizzly page when the exception happens, but now I see my exception (+stack trace) in the body of the page.
You're only registering one resource class for the entire application
new ResourceConfig().registerClasses(
eu.arrowhead.core.api.ApiResource.class
)
The mapper needs to be registered also
new ResourceConfig().registerClasses(
eu.arrowhead.core.api.ApiResource.class,
YourMapper.class)
)
You can also use package scanning, which will pick up all classes and automatically register them, if they are annotated with #Path or #Provider
new ResourceConfig().packages("the.packages.to.scan")

Spring boot Jackson dynamic partial response

I am working on rest service using Spring-Boot 1.3. In this, I have to return partial response based on fields(to include) provided in request input parameter(e.g. ../employees?opFields=name,emailId,..).
I want to implement jackson.antpathfilter (An implementation to add filtering based on AntPath matching). I have to add configuration such that I don't need to change return type of Rest(Controller)'s service method. But based on the object instance of particular class, serialize using filter else use normal serialization. Filter should be applied to instance of particular class only.
Update
Basically I want to implement dynamic partial response with,
1) Retrieving opFields dynamically from request.
2) Setting filter based on object type(can be antpathbuilder or simple)
3) Not changing return type of (rest)controller method.
As of now I have added configuration as below, but its giving issue in ExceptionHandler.
#Configuration
public class CustomDispatcherServlet extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> messageConverters) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIn(Object.class, AntPathFilterMixin.class).build();
messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
extendMessageConverters(messageConverters);
}
}
I am extending MappingJacksonValue as below and using class object to send rest call response,
public class FilteredResponse extends MappingJacksonValue {
public FilteredResponse(final Object value, final String... opFields) {
super(value);
if (null == opFields || opFields.length <= 0) {
setFilters(new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter("**")));
} else {
setFilters(new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter(opFields)));
}
}
}
Doing so, giving me more issues, when object is not JacksonResponse class. Also, I have to create object at every controller method and change return type where partial response required.
Can we check object instance dynamically and set filter. Or any other solution?
Basically, when you extend MappingJacksonValue and set filters.
E.g.
public class PartialResponse extends MappingJacksonValue {
public JacksonResponse(final Object value, final String... filters) {
super(value);
if (null == filters || filters.length <= 0) {
setFilters(new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter("**")));
} else {
setFilters(new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter(filters)));
}
}
}
In this, instead of configuring object mapper with Object.class, if you add configuration for PartialResponse.class, Resolves problem.
#Configuration
public class CustomDispatcherServlet extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> messageConverters) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIn(PartialResponse.class, AntPathFilterMixin.class).build();
messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
extendMessageConverters(messageConverters);
}
}

Spring Boot with AngularJS html5Mode

I start my web application with spring boot. It use a simple main class to start an embedded tomcat server:
#Configuration
#EnableAutoConfiguration
#ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
I want to configure the server in the way that he can handle angularjs html5mode that will be activated with
$locationProvider.html5Mode(true);
Relevant postings from other users shows that you need to redirect to the root. the html5 mode remove the hashbag from the url. If you refresh the page the server doesnt find the page cause he do not handle the hash. see: AngularJS - Why when changing url address $routeProvider doesn't seem to work and I get a 404 error
Use this controller to forward the URI to index.html in order to preserve AngularJS routes. Source https://spring.io/blog/2015/05/13/modularizing-the-client-angular-js-and-spring-security-part-vii
#Controller
public class ForwardController {
#RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
In this solution ForwardController forwards only paths, which are not defined in any other Controller nor RestController. It means if you already have:
#RestController
public class OffersController {
#RequestMapping(value = "api/offers")
public Page<OfferDTO> getOffers(#RequestParam("page") int page) {
return offerService.findPaginated(page, 10);
}
}
both controllers are going to work properly - #RequestMapping(value = "api/offers") is checked before #RequestMapping(value = "/**/{[path:[^\\.]*}")
I had same problem. As far as I know, in html5 mode, angularjs don't resolve hash but entered url or url added through pushState.
The problem was that PathResourceResolver map directories but not files. Because it intended to serve requested files from directory but not to rewrite urls. For app it's mean, if you refresh your browser window or type url like http://example.com/mystate, it's query "/mystate" from the server. If spring don't know url, they return 404. One of the solutions is map every possible state to index.html like here (source, btw look at webjars - it's great!). But in my case I can safely map "/**" to index.html and therefore my solution is to override PathResourceResolver#getResource:
#Configuration
#EnableConfigurationProperties({ ResourceProperties.class })
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#Autowired
private ResourceProperties resourceProperties = new ResourceProperties();
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Integer cachePeriod = resourceProperties.getCachePeriod();
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(cachePeriod);
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/index.html")
.setCachePeriod(cachePeriod).resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
return location.exists() && location.isReadable() ? location
: null;
}
});
}
}
I found a solution I can live with it.
#Controller
public class ViewController {
#RequestMapping("/")
public String index() {
return "index";
}
#RequestMapping("/app/**")
public String app() {
return "index";
}
}
The angularjs app has to be under the subdomain app. If you do not want that you could create a subdomain like app.subdomain.com that mapps to your subdomain app. With this construct you have no conflicts with webjars, statis content and so on.
A small adjustment to a previous code which works to me.
// Running with Spring Boot v1.3.0.RELEASE, Spring v4.2.3.RELEASE
#Configuration
#EnableConfigurationProperties({ ResourceProperties.class })
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#Autowired
private ResourceProperties resourceProperties = new ResourceProperties();
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Integer cachePeriod = resourceProperties.getCachePeriod();
final String[] staticLocations = resourceProperties.getStaticLocations();
final String[] indexLocations = new String[staticLocations.length];
for (int i = 0; i < staticLocations.length; i++) {
indexLocations[i] = staticLocations[i] + "index.html";
}
registry.addResourceHandler(
"/**/*.css",
"/**/*.html",
"/**/*.js",
"/**/*.json",
"/**/*.bmp",
"/**/*.jpeg",
"/**/*.jpg",
"/**/*.png",
"/**/*.ttf",
"/**/*.eot",
"/**/*.svg",
"/**/*.woff",
"/**/*.woff2"
)
.addResourceLocations(staticLocations)
.setCachePeriod(cachePeriod);
registry.addResourceHandler("/**")
.addResourceLocations(indexLocations)
.setCachePeriod(cachePeriod)
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
return location.exists() && location.isReadable() ? location
: null;
}
});
}
}
You can forward all not found resources to your main page by providing custom ErrorViewResolver.
All you need to do is to add this to your #Configuration class:
#Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
return new ErrorViewResolver() {
#Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
return status == HttpStatus.NOT_FOUND
? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
: null;
}
};
}
I finally get my Angular 5 application working with spring boot with or without spring-boot-starter-tomcat as provided (embedded) or not!
/**
* Needed for html5mode (PathLocationStrategy in Angular). Every path except api/* and resources (css, html, js, woff, etc..)
* should be redirect to index.html and then should angular managed routes (which could be correct or non existing).
*/
#RestController
#RequestMapping
public class ForwardController {
#GetMapping(value = "/**/{[path:[^\\.]*}")
public ModelAndView forward() {
return new ModelAndView("/index.html");
}
}
I just encountered the similar issue where I wanted to configure Resources and at the same time I wanted to use AngularJS Html5 mode enabled.
In my case my static files were served from /public route so I used the following request mapping on my index action and it all works fine.
#RequestMapping(value = {"", "/", "/{[path:(?!public).*}/**"}, method = GET)
public String indexAction() {
return "index";
}
I had the same problem while using angular Html5Mode.
The solution that worked for me was to configure error page for 404 in web.xml assigning the path to my Index view in my case "/".
<error-page>
<error-code>404</error-code>
<location>/</location>
</error-page>
Similarly, you can try configuring error page in spring boot. for reference, you can check this link.
Spring boot and custom 404 error page
1- first you create new Controller then copy and paste simple below code
#Controller
public class viewController {
#RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
3- remove 2 below item from angular app
$locationProvider.hashPrefix('!');
$urlRouterProvider.otherwise("/");
2- in angular application you must add $locationProvider.html5Mode(true); to app route
3- Don't forget to place the base tag before any http request in your index.html file
<head>
<base href="/"> /* Or whatever your base path is */
//call every http request for style and other
...
</head>
it's work fine for me

Apache Camel Integration Test - NotifyBuilder

I am writing integration tests to test existing Routes. The recommended way of getting the response looks something like this (via Camel In Action section 6.4.1):
public class TestGetClaim extends CamelTestSupport {
#Produce(uri = "seda:getClaimListStart")
protected ProducerTemplate producer;
#Test
public void testNormalClient() {
NotifyBuilder notify = new NotifyBuilder(context).whenDone(1).create();
producer.sendBody(new ClientRequestBean("TESTCLIENT", "Y", "A"));
boolean matches = notify.matches(5, TimeUnit.SECONDS);
assertTrue(matches);
BrowsableEndpoint be = context.getEndpoint("seda:getClaimListResponse", BrowsableEndpoint.class);
List<Exchange> list = be.getExchanges();
assertEquals(1, list.size());
System.out.println("***RESPONSE is type "+list.get(0).getIn().getBody().getClass().getName());
}
}
The test runs but I get nothing back. The assertTrue(matches) fails after the 5 second timeout.
If I rewrite the test to look like this I get a response:
#Test
public void testNormalClient() {
producer.sendBody(new ClientRequestBean("TESTCLIENT", "Y", "A"));
Object resp = context.createConsumerTemplate().receiveBody("seda:getClaimListResponse");
System.out.println("***RESPONSE is type "+resp.getClass().getName());
}
The documentation is a little light around this so can anyone tell me what I am doing wrong with the first approach? Is there anything wrong with following the second approach instead?
Thanks.
UPDATE
I have broken this down and it looks like the problem is with the mix of seda as the start endpoint in combination with the use of a recipientList in the Route. I've also changed the construction of the NotifyBuilder (I had the wrong endpoint specified).
If I change the start endpoint to
direct instead of seda then the test will work; or
If I comment out the recipientList
then the test will work.
Here's a stripped down version of my Route that reproduces this issue:
public class TestRouteBuilder extends RouteBuilder {
#Override
public void configure() throws Exception {
// from("direct:start") //works
from("seda:start") //doesn't work
.recipientList(simple("exec:GetClaimList.bat?useStderrOnEmptyStdout=true&args=${body.client}"))
.to("seda:finish");
}
}
Note that if I change the source code of the NotifyTest from the "Camel In Action" source to have a route builder like this then it also fails.
Try to use "seda:getClaimListResponse" in the getEndpoint to be sure the endpoint uri is 100% correct
FWIW: It appears that notifyBuilder in conjunction with seda queues are not quite working: a test class to illustrate:
public class NotifyBuilderTest extends CamelTestSupport {
// Try these out!
// String inputURI = "seda:foo"; // Fails
// String inputURI = "direct:foo"; // Passes
#Test
public void testNotifyBuilder() {
NotifyBuilder b = new NotifyBuilder(context).from(inputURI)
.whenExactlyCompleted(1).create();
assertFalse( b.matches() );
template.sendBody(inputURI, "Test");
assertTrue( b.matches() );
b.reset();
assertFalse( b.matches() );
template.sendBody(inputURI, "Test2");
assertTrue( b.matches() );
}
#Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
from(inputURI).to("mock:foo");
}
};
}
}