I have some webservice endpoints that should offer json data by default. Therefore configuring as follows:
#Configuration
public class ContentNegotiationConfiguration implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Problem: now I want to create an endpoint that offers a file download (thus is not json).
#RestController
public class FileServlet {
#GetMapping(value = "/docs/{filename}", consumes = MediaType.ALL_VALUE, produces = APPLICATION_OCTET_STREAM_VALUE)
public Object download(#Pathvariable filename) {
File file = fileservice.resolve(filename);
return new FileSystemResource(file);
}
}
Accessing this endpoint from the browser works fine. I can download the files.
But: when using native clients that are not setting any http headers like content-type, accept-header etc, the access fails with:
WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Resolved
[org.springframework.web.HttpMediaTypeNotAcceptableException:
Could not find acceptable representation]
All of them result in the exception:
curl localhost:8080/docs/testfile.txt
curl -O localhost:8080/docs/testfile.txt
wget localhost:8080/docs/testfile.txt
This is probably because I set the default content type to json above in ContentNegotiationConfiguration. I cannot change that due to all the other endpoints that should be json by default.
Question: how can I explicit ignore that default json setting on that single endpoint, and always just offer the download stream?
Try custom ContentNegotiationStrategy with AntPathMatcher something like:
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// configurer.defaultContentType(MediaType.APPLICATION_JSON,MediaType.APPLICATION_OCTET_STREAM);
configurer.defaultContentTypeStrategy(
new ContentNegotiationStrategy() {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
AntPathMatcher antPathMatcher = new AntPathMatcher();
#Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}
String path = this.urlPathHelper.getLookupPathForRequest(request);
if (antPathMatcher.match("/docs/*", path)) {
return Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM);
} else {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
}
});
}
With the hint from #M. Deinum, I got it working as follows:
#GetMapping(value = "/docs/{filename}")
public void download(#Pathvariable filename) {
FileSystemResource file = new FileSystemResource(fileservice.resolve(filename));
rsp.setHeader("Content-Disposition", "attachment; filename=" + file.getFilename());
ResourceHttpMessageConverter handler = new ResourceHttpMessageConverter();
handler.write(file, MediaType.APPLICATION_OCTET_STREAM, new ServletServerHttpResponse(rsp));
}
That way writing directly to the stream bypassing the content negotiation, while still relying on the Spring class ResourceHttpMessageConverter for not having to implement the response writer myself.
Related
Please have a look at my codes below. The Java codes seemed to work just fine, but localhost:8080 gives me the error code 404 when I try to access it. I want to make localhost 8080 work. Please let me know if you need further information.
Application
#SpringBootApplication(exclude = { ErrorMvcAutoConfiguration.class })
// exclude part is to elimnate whitelabel error
#EnableScheduling
public class Covid19TrackerApplication {
public static void main(String[] args) {
SpringApplication.run(Covid19TrackerApplication.class, args);
}
}
Controller
#Controller
public class HomeController {
CovidDataService covidDataService;
#RequestMapping("/")
public #ResponseBody String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
}
Main Code
#Service
public class CovidDataService {
private static String Covid_Data_URL = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv";
private List<LocationStats> allStats = new ArrayList<>();
public List<LocationStats> getAllStats() {
return allStats;
}
#PostConstruct//?
#Scheduled(cron = "* * 1 * * *") //????
// * sec * min *hour and so on
public void fetchCovidData() throws IOException, InterruptedException {
List<LocationStats> newStats = new ArrayList<>(); // why we are adding this? To prevent user get an error while we are working on new data.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(Covid_Data_URL))
.build(); // uri = uniform resource identifier
HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
StringReader csvBodyReader = new StringReader(httpResponse.body()); //StringReader needs to be imported
Iterable<CSVRecord> records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(csvBodyReader); // parse(in) had error, we needed a "reader" instance.
for (CSVRecord record : records) {
LocationStats locationStat = new LocationStats(); //create an instance
locationStat.setState(record.get("Province/State"));
locationStat.setCountry(record.get("Country/Region"));
locationStat.setLatestTotalCase(Integer.parseInt(record.get(record.size()-1)));
System.out.println(locationStat);
newStats.add(locationStat);
}
this.allStats = newStats;
}
}
The problem may come from this piece of code
#RequestMapping("/")
public #ResponseBody String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
it returns "home" which should be existing view, normally, the view will be a jsp file which is placed somewhere in WEB-INF, please see this tutorial: https://www.baeldung.com/spring-mvc-view-resolver-tutorial
In the case of wrong mapping, it may returns 404 error
when you run the server, you should be able to see which port it's taken in the console.
Also, is server.port=8080 in the src/main/resources/application.properties file?
In the controller, the RequestMapping annotation is missing the method type and header
#RequestMapping(
path="/",
method= RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
public String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
make sure to add consumes for POST or PUT methods
A bit unrelated to the question but the line in the controller is missing #Autowired annotation
CovidDataService covidDataService;
Preferrably, add the #Autowired in the constructor
#Autowired
public HomeController(CovidDataService covidDataService) {
this.covidDataService = covidDataService;
}
I want to serve a .yaml file via a REST endpoint with Spring, I know that it cannot be directly displayed in a browser (just talking about Chrome here), since it doesn't support display of yaml files.
I have included what I think is the necessary library for this purpose
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.9'.
If I open the endpoint /v2/api-doc in the browser, it will prompt me, to download a file named exactly as the endpoint /v2/api-doc. It contains the correct content.
Question: Is there a way to correctly transfer the .yaml file, so that the user will be prompted to safe myfile.yaml?
#RequestMapping(value = "/v2/api-doc", produces = "application/x-yaml")
public ResponseEntity<String> produceApiDoc() throws IOException {
byte[] fileBytes;
try (InputStream in = getClass().getResourceAsStream("/restAPI/myfile.yaml")) {
fileBytes = IOUtils.toByteArray(in);
}
if (fileBytes != null) {
String data = new String(fileBytes, StandardCharsets.UTF_8);
return new ResponseEntity<>(data, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
You should set a Content-Disposition header (and I recommend using ResourceLoader to load resources in Spring Framework).
Example:
#RestController
public class ApiDocResource {
private final ResourceLoader resourceLoader;
public ApiDocResource(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
#GetMapping(value = "/v2/api-doc", produces = "application/x-yaml")
public ResponseEntity produceApiDoc() throws IOException {
Resource resource = resourceLoader.getResource("classpath:/restAPI/myfile.yaml");
if (resource.exists()) {
return ResponseEntity
.ok()
.contentType(MediaType.parseMediaType("application/x-yaml"))
.header("Content-Disposition", "attachment; filename=myfile.yaml")
.body(new InputStreamResource(resource.getInputStream()));
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
I have this endpoint for Spring Rest API:
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
// get here plain XML
}
XML model.
#XmlRootElement(name = "payment_transaction")
#XmlAccessorType(XmlAccessType.FIELD)
public class PaymentTransaction {
public enum Response {
failed_response, successful_response
}
#XmlElement(name = "transaction_type")
public String transactionType;
.........
}
How I can get the XML request in plain XML text?
I also tried with Spring interceptor:
I tried this code:
#SpringBootApplication
#EntityScan("org.plugin.entity")
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
........
#Bean
public RestTemplate rsestTemplate() {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate(
new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
Component for logging:
#Component
public class RestTemplateHeaderModifierInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("[ ");
for (byte b : body) {
sb.append(String.format("0x%02X ", b));
}
sb.append("]");
System.out.println("!!!!!!!!!!!!!!!");
System.out.println(sb.toString());
ClientHttpResponse response = execution.execute(request, body);
InputStream inputStream = response.getBody();
String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
System.out.println("!!!!!!!!!!!!!!!");
System.out.println(result);
return response;
}
}
But nothing is printed into the console. Any idea where I'm wrong? Probably this component is not registered?
Shouldn't it be easy like below to get it from HttpServletRequest, unless I'm missing something. I don't think there is need to use interceptor etc.
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(HttpServletRequest request) throws Exception {
String str, wholeXML = "";
try {
BufferedReader br = request.getReader();
while ((str = br.readLine()) != null) {
wholeXML += str;
}
System.out.println(wholeXML);
//Here goes comment question, to convert it into PaymentTransaction
JAXBContext jaxbContext = JAXBContext.newInstance(PaymentTransaction.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringReader reader = new StringReader(wholeXML);
PaymentTransaction paymentTransaction = (PaymentTransaction) unmarshaller.unmarshal(reader);
}
We had the same issue and use this solution in production. Which is not framework dependent (always an upside in my book) and simple.
Just consume it without specifying it as an XML. Then read the request lines and join them by \n if you want to have new lines in your xml. If not, join them by "" or whatever you please. This presumes you are using the javax.servlet.http.HttpServletRequest
Example:
#PostMapping(value = "/v1")
public PaymentResponse handleMessage(HttpServletRequest request) throws Exception {
final InputStream xml = request.getInputStream();
final String xmlString = new BufferedReader(new InputStreamReader(xml))
.lines()
.collect(Collectors.joining("\n"));
// do whatever you please with it
}
And you have an plain xml string.
For your controller to receive the request body as a plain xml string, you need only change the #RequestBody parameter type to String:
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#RequestBody String xmlOrJson, HttpServletRequest request) throws Exception {
...
With the above mapping, if the client has submitted xml, you'll see the raw XML. Otherwise, if the client has submitted json, you'll see the raw JSON. Make sure you check the request's "Content-Type" header to know which type you're dealing with.
See https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestbody
We've been using the spring-mvc-logger in production for quite a while. It's written as a servlet filter, so can be added as an independent wrapper to the MVC endpoint.
Our set up is almost exactly like described on the readme.md there, though we restrict the <url-pattern> under the <filter-mapping> to just the useful endpoints.
Even if it's not exactly what you're after, the codebase there makes quite a nice small example. In particular note the request/response wrapping that is needed in the filter. (This is to avoid the IllegalStateException: getReader(), getInputStream() already called that would otherwise happen if getReader() were called twice).
You have created List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(); but did not add the RestTemplateHeaderModifierInterceptor object to it.
You can autowire in the same in Application like below:
#Autowired
ClientHttpRequestInterceptor clientHttpRequestInterceptor;
and
interceptors.add(clientHttpRequestInterceptor);
The code looks like below:
class Application {
...
#Autowired
ClientHttpRequestInterceptor clientHttpRequestInterceptor;
#Bean
public RestTemplate rsestTemplate() {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate(
new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
interceptors.add(clientHttpRequestInterceptor);
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
...
}
Hope it helps
I have a Spring Boot web application where I catch my custom exceptions in ControllerAdvice class. The problem is that Spring Boot doesn't throw exception by default if no handler is found (it sends json back to a client).
What I want is to catch NoHandlerFoundException in my ControllerAdvice class. To make this possible I explicitly configured
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
This trick does the job and I can catch NoHandlerFoundException now but it disables Spring to auto-configure path to static resources. So all my static resources are not available for a client now. I tried to resolve this using one more configuration which doesn't help
spring.resources.static-locations=classpath:/resources/static/
Could anybody please advise how to map static resources in Spring Boot when auto-configuration was disabled with spring.resources.add-mappings=false?
Thanks!
If your static resources are limited to specific URL paths, you can configure only those paths to be handled by the Spring static resources handler. In this example, the /doc URL path is served by static resources in the /resources/static/doc/ folder in the classpath:
spring.mvc.static-path-pattern=/doc/**
spring.resources.static-locations=classpath:/resources/static/doc/
You'll need to remove this configuration:
spring.resources.add-mappings=false
I experienced the same issue and after some research, I found out that it is obviously not possible to have both options enabled (i.e. throwing NoHandlerFoundException by setting spring.mvc.throw-exception-if-no-handler-found=true AND serving static resources automatically).
Enabling the option to throw NoHandlerFoundException requires one to set spring.resources.add-mappings to false, otherwise it would not work. Furthermore, in my test setup it was not possible to disable spring.resources.add-mappings and specify the URLs for static resources manually (e.g. via application properties spring.mvc.static-path-pattern and spring.resources.static-locations or programmatically by overriding public void addResourceHandlers(ResourceHandlerRegistry registry)), because then the spring.resources.add-mappings=false setting seems to be overruled.
Finally, I implemented the following workaround for serving static resources manually via my own controller implementation:
#Controller
public class StaticWebContentController {
private Map<String, byte[]> cache = new HashMap<String,byte[]>();
#RequestMapping(value = "/css/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getCssFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\css\\"+name,"text/css");
return responseEntity;
}
#RequestMapping(value = "/img/bootstrap-icons-1.1.0/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getimgFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\img\\bootstrap-icons-1.1.0\\"+name,"image/svg+xml");
return responseEntity;
}
#RequestMapping(value = "/js/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getJsFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\js\\"+name,"text/javascript");
return responseEntity;
}
private ResponseEntity<byte[]> loadResource(String path, String contentType){
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", contentType);
if(hasCachedContent(path)){
return new ResponseEntity<byte[]>(getCachedContent(path),responseHeaders,HttpStatus.OK);
}else{
Resource resource = new ClassPathResource(path);
if(resource.exists()){
try{
InputStream inputStream = resource.getInputStream();
byte[] content = inputStream.readAllBytes();
putCache(path, content);
return new ResponseEntity<byte[]>(content,responseHeaders,HttpStatus.OK);
}catch(IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,e.getMessage());
}
}else{
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"The requested resource '"+path+"' does not exist'");
}
}
}
private byte[] getCachedContent(String path){
return cache.get(path);
}
private boolean hasCachedContent(String path){
return cache.containsKey(path);
}
private void putCache(String path, byte[] content){
cache.put(path, content);
}
}
In my application, I have three types of static resources located in three different sub folders. Each type is handled by a separate endpoint in order to set the Content-Type header properly. Moreover, the controller caches each resource in order to avoid to reload the requested resource from hard disk again.
Probably, this is not the best solution, however, a feasible workaround in case of my application. Any recommendations for improvement are highly appreciated!
Instead of adding below lines to config properties
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
write your custom Error Attributes as below:
#Configuration
public class CustomErrorAttributes extends DefaultErrorAttributes {
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
Map<String, Object> newErrorAttributes = new LinkedHashMap<String, Object>();
Object errorMessage = requestAttributes.getAttribute(RequestDispatcher.ERROR_MESSAGE, RequestAttributes.SCOPE_REQUEST);
if (errorMessage != null) {
newErrorAttributes.put("response-type", "error");
newErrorAttributes.put("error-code", errorAttributes.get("status"));
newErrorAttributes.put("message", errorAttributes.get("message"));
newErrorAttributes.put("error-message", errorAttributes.get("error"));
}
return newErrorAttributes;
}
};
}
}
I have a Client-Server application using SpringBoot and Angular2.
I would like to load a image from the server by filename. This works fine.
I store the attribute image:string at the client and I place it in the template again.
You might pay attention to return res.url;; I do not use the actual ressource, which might be wrong.
My objective is that image is cached. To my understanding the web-browser can automatically cache the images. Correct?
But the caching does not work yet and maybe somebody could give me a hint what needs to be adjusted?
Is a different header required?
Server (SpringBoot)
public class ImageRestController {
#RequestMapping(value = "/getImage/{filename:.+}", method = RequestMethod.GET)
public ResponseEntity<Resource> getImage(#PathVariable String filename) {
try {
String path = Paths.get(ROOT, filename).toString();
Resource loader = resourceLoader.getResource("file:" + path);
return new ResponseEntity<Resource>(loader, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<Resource>(HttpStatus.NOT_FOUND);
}
}
}
Client (Angular2)
#Component({
selector: 'my-image',
template: `
<img src="{{image}}"/>
`
})
export class MyComponent {
image:string;
constructor(private service:MyService) {}
showImage(filename:string) {
this.service.getImage(filename)
.subscribe((file) => {
this.image = file;
});
}
}
export class MyService() {
getImage(filename:String):Observable<any> {
return this.http.get(imagesUrl + "getImage/" + filename)
.map(this.extractUrl)
.catch(this.handleError);
}
extractUrl(res:Response):string {
return res.url;
}
}
You could do something like this on the server side (and perhaps add an ETag or Last-Modified header if you can get that information):
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.body(loader);
See the HTTP caching part of the reference documentation in Spring.
If you're just serving resources and not applying any additional logic, then you'd better do the following:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/getImage/**")
.addResourceLocations("classpath:/path/to/root/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic());
}
}
See the other relevant part of the reference documentation. You can also apply transformations and leverage cache busting (see this section as well).