Spring #RestController - after request being served - java

Background
here is the method defined in #RestController, it reads file from disk then stream back.
#RequestMapping(value = "/bill", method = RequestMethod.GET)
public ResponseEntity<Object> getbill(){
...
InputStream in = new FileInputStream(file);
InputStreamResource inputStreamResource = new InputStreamResource(in);
httpHeaders.setContentLength(file.Length());
return new ResponseEntity(inputStreamResource, httpHeaders, HttpStatus.OK);
}
Issue
I would like to delete the file after request is served, but unable to find a good place.
I would assume it should be after inputStream gets closed (https://github.com/spring-projects/spring-framework/blob/v4.3.9.RELEASE/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java#L117) . it can not be done in above method since file is opened by Inputstream.
Answer Summary
Thank you all for helping with this.
The accepted answer requires least change and working well.

Aside from the fact that it is bad practice in a RESTfull service to perform destructive operations on GET requests this can not be done by the default Java libraries. The more widely accepted implementation would be a GET that streams the file followed by a DELETE call to remove the file.
But you can do it by implementing your own InputStream, see an earlier thread in Stackoverflow on deleting files on closing a InputStream.

Assuming that you are creating the file in the same controller.
You can use:
try (BufferedWriter out = Files
.newBufferedWriter(newFilePath, Charset.defaultCharset(),
StandardOpenOption.DELETE_ON_CLOSE)) {
InputStream in = new FileInputStream(newFilePath.toFile());
InputStreamResource inputStreamResource = new InputStreamResource(in);
httpHeaders.setContentLength(file.Length());
return new ResponseEntity(inputStreamResource, httpHeaders, HttpStatus.OK);
} catch (Exception e) {
}
As the BufferedWriter will close on return, the file will get deleted.

Based on #phlogratos's answer, you can try like this.
#GetMapping("/download")
public ResponseEntity<InputStreamResource> download() throws Exception {
... codes ...
InputStreamResource isr = new InputStreamResource(new FileInputStream(file) {
#Override
public void close() throws IOException {
super.close();
boolean isDeleted = file.delete();
logger.info("export:'{}':" + (isDeleted ? "deleted" : "preserved"), filename);
}
});
return new ResponseEntity<>(isr, respHeaders, HttpStatus.OK);
}

Extend FileInputStream with your own implementation and then overwrite close. When the input stream is closed, your file gets deleted as well.
public class MyFileInputStream extends FileInputStream {
private final File myFile;
public MyFileInputStream(File file) throws FileNotFoundException {
super(file);
myFile = file;
}
#Override
public void close() throws IOException {
super.close();
myFile.delete();
}
}

Related

Reading resources from src/main/resources/.. in spring-MVC

I have a question that makes my head ache.
First, my project structure looks like below.
I made a controller, which returns image(*.png) file to the appropriate request.
The code of controller is written below.
#Controller
public class ImageController {
#GetMapping(value = "/ImageStore.do", produces = MediaType.IMAGE_PNG_VALUE)
public #ResponseBody byte[] getStoreImage(HttpServletRequest request) throws IOException {
String image_name = request.getParameter("image_name");
Resource resource = null;
try {
resource = new ClassPathResource("/images/stores/" + image_name);
if(resource == null) {
throw new NullPointerException();
}
} catch(NullPointerException e) {
resource = new ClassPathResource("/images/stores/noimage.png");
}
InputStream inputStream = resource.getInputStream();
return IOUtils.toByteArray(inputStream);
}
}
Q1. I added try-catch phrase to send noimage.png if the request parameter is wrong, or if the filename of request parameter image_name does not exist. But it doesn't seem to work, and it gives me log saying
class path resource [images/stores/noima.png] cannot be opened because it does not exist
(If you need to know the full stack trace, I will comment below.)
Q2. I have 2 image files, hello.png and noimage.png in the folder /resources/images/stores/. I can read noimage.png correctly, but if I make request localhost:8080/ImageStore.do?image_name=hello.png, then it makes an error, making the same log in Q1.
There's no reason to think that the constructor would result in a null value.
The exception you are getting is likely from the getInputStream method, which is documented to throw
FileNotFoundException - if the underlying resource doesn't exist
IOException - if the content stream could not be opened
A slight adjustment might help
#Controller
public class ImageController {
#GetMapping(value = "/ImageStore.do", produces = MediaType.IMAGE_PNG_VALUE)
public #ResponseBody byte[] getStoreImage(HttpServletRequest request) throws IOException {
InputStream is = null;
try {
String image_name = request.getParameter("image_name");
is = new ClassPathResource("/images/stores/" + image_name).getInputStream();
} catch(FileNotFoundException e) {
is = new ClassPathResource("/images/stores/noimage.png").getInputStream();
}
return IOUtils.toByteArray(is);
}
}
You should include the stack trace, and exception message, which might assist understanding your second query, but I would check that the file really does exist, with the exact name you're using.

Serving static files with Jersey - Error while closing the output stream in order to commit response

I'm trying to implement a project that will basically serve static files that are located in a folder of the project. I'm using Jersey 2.22.1. It should be a method that receives a filename and answers with the requested file.
Implementation:
#GET
#Path("/{filename : .*}")
public Response getFile(#PathParam("filename") String filename) {
String f = getFileFromSomewhere(filename);
StreamingOutput fileStream = new StreamingOutput()
{
#Override
public void write(java.io.OutputStream output) throws IOException, WebApplicationException
{
try
{
java.nio.file.Path path = Paths.get(f);
byte[] data = Files.readAllBytes(path);
output.write(data);
output.flush();
}
catch (Exception e)
{
throw new WebApplicationException("File Not Found !!");
}
}
};
return Response
.ok(fileStream)
.build();
}
The following error occurs:
GRAVE: Error while closing the output stream in order to commit response.
java.lang.IllegalStateException: getWriter() has already been called for this response
at org.apache.catalina.connector.Response.getOutputStream(Response.java:628)
The files are being requested by a browser and i have noticed that the error only occurs with .js files (it's ok with.html and .css files). There are reasons why this files should be served by this method and not by the conventional mode.
Any ideas?

How to zip- compress HTTP request with Spring RestTemplate?

How to gzip HTTP request, created by org.springframework.web.client.RestTemplate?
I am using Spring 4.2.6 with Spring Boot 1.3.5 (Java SE, not Android or Javascript in the web browser).
I am making some really big POST requests, and I want request body to be compressed.
I propose two solutions, one simpler without streaming and one that supports streaming.
If you don't require streaming, use a custom ClientHttpRequestInterceptor, a Spring feature.
RestTemplate rt = new RestTemplate();
rt.setInterceptors(Collections.singletonList(interceptor));
Where interceptor could be:
ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
request.getHeaders().add("Content-Encoding", "gzip");
byte[] gzipped = getGzip(body);
return execution.execute(request, gzipped);
}
}
getGzip I copied
private byte[] getGzip(byte[] body) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
try {
GZIPOutputStream zipStream = new GZIPOutputStream(byteStream);
try {
zipStream.write(body);
} finally {
zipStream.close();
}
} finally {
byteStream.close();
}
byte[] compressedData = byteStream.toByteArray();
return compressedData;
}
After configuring the interceptor all requests will be zipped.
The disadvantage of this approach is that it does not support streaming as the ClientHttpRequestInterceptor receives the content as a byte[]
If you require streaming create a custom ClientHttpRequestFactory, say GZipClientHttpRequestFactory, and use it like this:
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false);
ClientHttpRequestFactory gzipRequestFactory = new GZipClientHttpRequestFactory(requestFactory);
RestTemplate rt = new RestTemplate(gzipRequestFactory);
Where GZipClientHttpRequestFactory is:
public class GZipClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
public GZipClientHttpRequestFactory(ClientHttpRequestFactory requestFactory) {
super(requestFactory);
}
#Override
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory)
throws IOException {
ClientHttpRequest delegate = requestFactory.createRequest(uri, httpMethod);
return new ZippedClientHttpRequest(delegate);
}
}
And ZippedClientHttpRequest is:
public class ZippedClientHttpRequest extends WrapperClientHttpRequest
{
private GZIPOutputStream zip;
public ZippedClientHttpRequest(ClientHttpRequest delegate) {
super(delegate);
delegate.getHeaders().add("Content-Encoding", "gzip");
// here or in getBody could add content-length to avoid chunking
// but is it available ?
// delegate.getHeaders().add("Content-Length", "39");
}
#Override
public OutputStream getBody() throws IOException {
final OutputStream body = super.getBody();
zip = new GZIPOutputStream(body);
return zip;
}
#Override
public ClientHttpResponse execute() throws IOException {
if (zip!=null) zip.close();
return super.execute();
}
}
And finally WrapperClientHttpRequest is:
public class WrapperClientHttpRequest implements ClientHttpRequest {
private final ClientHttpRequest delegate;
protected WrapperClientHttpRequest(ClientHttpRequest delegate) {
super();
if (delegate==null)
throw new IllegalArgumentException("null delegate");
this.delegate = delegate;
}
protected final ClientHttpRequest getDelegate() {
return delegate;
}
#Override
public OutputStream getBody() throws IOException {
return delegate.getBody();
}
#Override
public HttpHeaders getHeaders() {
return delegate.getHeaders();
}
#Override
public URI getURI() {
return delegate.getURI();
}
#Override
public HttpMethod getMethod() {
return delegate.getMethod();
}
#Override
public ClientHttpResponse execute() throws IOException {
return delegate.execute();
}
}
This approach creates a request with chunked transfer encoding, this can be changed setting the content length header, if size is known.
The advantage of the ClientHttpRequestInterceptor and/or custom ClientHttpRequestFactory approach is that it works with any method of RestTemplate. An alternate approach, passing a RequestCallback is possible only with execute methods, this because the other methods of RestTemplate internally create their own RequestCallback(s) that produce the content.
BTW it seems that there is little support to decompress gzip request on the server. Also related: Sending gzipped data in WebRequest? that points to the Zip Bomb issue. I think you will have to write some code for it.
Further to the above answer from #TestoTestini, if we take advantage of Java 7+'s 'try-with-resources' syntax (since both ByteArrayOutputStream and GZIPOutputStream implement closeable() ) then we can shrink the getGzip function into the following:
private byte[] getGzip(byte[] body) throws IOException {
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
zipStream.write(body);
byte[] compressedData = byteStream.toByteArray();
return compressedData;
}
}
(I couldn't find a way of commenting on #TestoTestini's original answer and retaining the above code format, hence this Answer).
Since I cannot comment on #roj 's post I'm writing an answer here.
#roj snippet although is neat it actually does not do the same job as #Testo Testini 's snippet.
Testo is closing the streams before:
byteStream.toByteArray();
where in #rog answer, this occurs before the stream.close(), since streams are in the try/resource block.
If you need to use try-with-resources, zipStream should be closed before the byteStream.toByteArray();
The complete snippet should be:
private byte[] getGzip(byte[] body) throws IOException {
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
zipStream.write(body);
zipStream.close();
byte[] compressedData = byteStream.toByteArray();
return compressedData;
}
}
The was getting an error ("Compressed file ended before the end-of-stream marker was reached") and the above fixed the error in my case and I thought that I should share this.

How to test a controller with streamed content with MockMvc?

I'd like to test a Spring controller, without Spring context, with MockMvc.
This controller streams the content by writing it in the OutputStream of the response.
Here is the controller code:
#RequestMapping(method = GET, value = "/file")
public void getFile(HttpServletResponse response) throws IOException {
response.setHeader("Content-Disposition", "attachment; filename=file.json");
ObjectWriter objectWriter = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL).writer();
Item item = new Item();
List<Item> items = new ArrayList<>();
items.add(item);
try (Writer bufferedWriter = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))) {
objectWriter.writeValue(bufferedWriter, items);
bufferedWriter.flush();
}
}
Here is the test:
#Test
public void getFile_ok() throws Exception {
mockMvc.perform(get(END_POINT + "/file").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
This controller works well, but the unit test fails with an IOException:
java.io.IOException: Stream closed
at java.io.BufferedWriter.ensureOpen(BufferedWriter.java:116)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:126)
at java.io.BufferedWriter.flush(BufferedWriter.java:253)
Actually the objectWriter.writeValue(bufferedWriter, items); tries to close the stream.
Javadoc:
Note: method does not close the underlying stream explicitly here;
however, JsonFactory this mapper uses may choose to close the stream
depending on its settings (by default, it will try to close it when
JsonGenerator we construct is closed).
In runtime, it doesn't close it but in my tests, it does, and the next line bufferedWriter.flush(); just sends the IOEception.
So to fix it I added a try/catch this way:
try {
writer.flush();
} catch (IOException e) {
logger.info("Stream closed in writeValue()", e);
}

Jersey InputStream is modified in filter. Unable to figure out how to access modified inputStream in Jersey Resource

As discussed in How to use Jersey interceptors to get request body, I am modifying the EntityInputStream in a ContainerRequestFilter.
public filter(ContainerRequest request){
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream in = request.getEntityInputStream();
try{
Readerwriter.writeTo(in, out);
byte[] requestEntity = out.toByteArray();
// DO SOMETHING WITH BYTES HERE
request.setEntityInputStream(new ByteArrayInputStream(requestEntity));
}/// error handling code here
}
However, later on I can't figure out how to access the modified InputStream. I can get the ServletContext in the resource, but I can't figure out how to get ahold of the object I actually modified in the filter, the ContainerRequest.
Can I do something like this? Jersey can't start up out when I try this:
#Post
#Path("/test")
public Response test(#Context ContainerRequest cr){
// blah blah
return....
}
Jersey error:
Missing dependecy for method public javax.ws.rs.core.Response example.TestController.test(com.sun.jersey.spi.container.ContainerRequest), annotated with POST of resource, class example.TestController, is not recognized as a valid resource method.
I am stuck on an old version of jersey, 1.8, so I'm not sure if that's part of the problem.
All you need to do is accept an InputStream as the entity body in your resource method. If you want the ByteArrayInputStream just cast it.
#POST
public Response post(InputStream in) {
ByteArrayInputStream bin = (ByteArrayInputStream)in;
}
If you don't already know, how Jersey converts the request stream (for the request body) into Java types (for instance JSON to POJO) is through MessageBodyReaders. You can read more about them at JAX-RS Entity Providers.
Jersey already comes with some standard readers for easily convertible types, for instance String. Most content-types can be converted to String. Likewise, it has a reader to handle InputStream. This is probably the easiest conversion, as the request is already coming in as an InputStream, so really all the reader would need to do is return the original stream, and that's what would get passed to our method.
If we look at the implementation InputStreamProvider, we can see that that's what actually happens. The original stream is simply returned. And since the filter happens before the readers, the reader simply returns the stream that we set.
Here is a complete example using Jersey Test Framework
public class StreamFilterTest extends JerseyTest {
public static class InputStreamFilter implements ContainerRequestFilter {
#Override
public ContainerRequest filter(ContainerRequest request) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream in = request.getEntityInputStream();
ReaderWriter.writeTo(in, out);
byte[] requestBytes = out.toByteArray();
byte[] worldBytes = " World".getBytes(StandardCharsets.UTF_8);
byte[] newBytes = new byte[requestBytes.length + worldBytes.length];
System.arraycopy(requestBytes, 0, newBytes, 0, requestBytes.length);
System.arraycopy(worldBytes, 0, newBytes, requestBytes.length, worldBytes.length);
request.setEntityInputStream(new ByteArrayInputStream(newBytes));
} catch (IOException ex) {
Logger.getLogger(InputStreamFilter.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException(ex);
}
return request;
}
}
#Path("stream")
public static class StreamResource {
#POST
public String post(InputStream in) throws Exception {
ByteArrayInputStream bin = (ByteArrayInputStream) in;
StringWriter writer = new StringWriter();
ReaderWriter.writeTo(new InputStreamReader(bin), writer);
return writer.toString();
}
}
public static class AppConfig extends DefaultResourceConfig {
public AppConfig() {
super(StreamResource.class);
getContainerRequestFilters().add(new InputStreamFilter());
}
}
#Override
public WebAppDescriptor configure() {
return new WebAppDescriptor.Builder()
.initParam(WebComponent.RESOURCE_CONFIG_CLASS,
AppConfig.class.getName())
.build();
}
#Test
public void should_return_hello_world() {
String response = resource().path("stream").post(String.class, "Hello");
assertEquals("Hello World", response);
}
}
Here's the test dependency
<dependency>
<groupId>com.sun.jersey.jersey-test-framework</groupId>
<artifactId>jersey-test-framework-grizzly2</artifactId>
<version>1.17.1</version>
<scope>test</scope>
</dependency>

Categories

Resources