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);
}
Related
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();
}
}
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>
Hi have written controller class like below. I am trying to get file from mongo db and try to download it.
organizationFileAttachmentService.setUser(getUser());
GridFSDBFile file = organizationFileAttachmentService.getGridFSDBFileById(new ObjectId(id), "File");
if (file != null) {
byte[] content = organizationFileAttachmentService.findByIdAndBucket(new ObjectId(id), "File");
try {
int size = content.length;
InputStream is = null;
byte[] b = new byte[size];
try {
is = new ByteArrayInputStream(content);
is.read(b);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
} catch (Exception ex) {
}
}
response.setContentType(file.getContentType());
// String attachment =
// "attachment; filename=\""+file.getFilename()+"\"";
String attachment = "attachment; filename=" + file.getFilename();
// response.setContentLength(new
// Long(file.getLength()).intValue());
response.setCharacterEncoding(file.getMD5());
response.setHeader("content-Disposition", attachment);// "attachment;filename=test.xls"
// copy it to response's OutputStream
// FileCopyUtils.copy(is, response.getOutputStream());
IOUtils.copy(is, response.getOutputStream());
response.flushBuffer();
is.close();
} catch (IOException ex) {
_logger.info("Error writing file to output stream. Filename was '" + id + "'");
throw new RuntimeException("IOError writing file to output stream");
}
but i am not able to down load file. can any one help me.
In case you missed it, Spring provides various built in resource handlers.
http://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/resources.html#resources-implementations
If your method returns one of those (perhaps the ByteArrayResource in your case), then you just need a couple of annotations on the interface like so:
#RequestMapping(value = "/foo/bar/{fileId}",
method = RequestMethod.GET,
produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE })
#ResponseBody FileSystemResource downloadFile(Long fileId);
No fiddling with encodings and headers for you that way. I'd recommend trying that before rolling your own.
Edit: The above worked fine in Spring 3.1.4. It no longer works for 3.2.x or 4.x. Whereas previously, the produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE } would cause Spring to add the appropriate headers, it now treats that as a restriction. If accessing the URL with a standard web browser, an accept header of "application/octet-stream" will not be sent. Spring will therefore return a 406 error. To get it working again, such a method needs to be re-written without the "produces" attribute. Instead, add HttpServletResponse to the method arguments and add the header inside the method. i.e.:
#RequestMapping(value = "/foo/bar/{fileId}",
method = RequestMethod.GET)
#ResponseBody FileSystemResource downloadFile(
Long fileId, HttpServletResponse response) {
...
response.setHeader( "Content-Disposition", "attachment;filename=" + fileName );
...
}
Edit redux:
Now using Spring 4.0.7 via Spring Boot 1.1.8. It would appear that setting the produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE } instruction is now working again. Just having that instruction seems to be enough for all the browsers I have tried. Note however, that I have also found that it does not set the Content-Disposition, which is left as application/json. Although this doesn't seem to be an issue for browsers, I have come across bugs in PHP client applications, which seem to behave only based on the Content-Disposition. So it seems that the current solution is to do both of the above!
I have changed my request as GET and added request in anchor tag in html. Aslo changed my code as
#RequestMapping(value = "/getFileById/{id}", method = RequestMethod.GET)
public #ResponseBody
void download(#PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
organizationFileAttachmentService.setUser(getUser());
GridFSDBFile file = organizationFileAttachmentService.getGridFSDBFileById(new ObjectId(id), "File");
if (file != null) {
try {
response.setContentType(file.getContentType());
response.setContentLength((new Long(file.getLength()).intValue()));
response.setHeader("content-Disposition", "attachment; filename=" + file.getFilename());// "attachment;filename=test.xls"
// copy it to response's OutputStream
IOUtils.copyLarge(file.getInputStream(), response.getOutputStream());
} catch (IOException ex) {
_logger.info("Error writing file to output stream. Filename was '" + id + "'");
throw new RuntimeException("IOError writing file to output stream");
}
}
}
Now it is working fine for me.
Good evening, i want to know how to clear the data written to a PrintWriter, i.e. is it possible to remove the data from a PrintWriter after printing?
here in this servlet i print some text to the response and at the line denoted by # i want to remove all the previously printed data and print new stuff:
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
String uName = request.getParameter("uName");
String uPassword = request.getParameter("uPassword");
if (uName .equals("Islam")) {
out.println("Valid-Name");
if (uPassword !=null) {
if (uPassword .equals("Islam")) {
// # clear the writer from any printed data here
out.println("Valid-password");
} else {
out.println("");
out.println("InValid-password");
}
}
} else {
out.println("InValid-Name");
}
}
Note: i tried out.flush() but the old printed text remains
Create an in-memory PrintWriter using a StringWriter. You can get the underlying buffer from the StringWriter and clear it if you need to.
StringWriter sr = new StringWriter();
PrintWriter w = new PrintWriter(sr);
w.print("Some stuff");
// Flush writer to ensure that it's not buffering anything
w.flush();
// clear stringwriter
sr.getBuffer().setLength(0);
w.print("New stuff");
// write to Servlet out
w.flush();
response.getWriter().print(sr.toString());
HttpServlteResponse.resetBuffer() will clear the buffered content. But yes, if the response is already flushed to the client it will throw IllegalStateException. Because it is illegal to clear after partial response is sent to the client.
resetBuffer........
void resetBuffer()
Clears the content of the underlying buffer in the response without clearing headers or status code. If the response has been committed, this method throws an IllegalStateException.
References:
Cause of Servlet's 'Response Already Committed'
You can't do that with the original PrintWriter you get from the response, as that's backed by the actual OutputStream corresponding to the client connection. What you write there goes right to the browser via the wire (after some buffering), so you can't "take it back".
What you can do is write your message in some StringBuilder and once you know it's good to go, write it to the PrintWriter.
If you want this logic to be applied in multiple places (transparently), you can consider writing a filter that wraps the original response in an HttpServletResponseWrapper which returns a "fake" OutputStream or PrintWriter and performs this check prior to actually sending it over the wire.
public class CensorshipFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
CensorshipResponseWrapper wrapper = new CensorshipResponseWrapper(httpServletResponse);
chain.doFilter(request, wrapper);
String output = wrapper.sw.toString();
if ( output.contains("Some forbidden pattern") ) { // your check goes here
// throw exception or whatever
} else { // write the whole thing
httpServletResponse.getWriter().write(output);
}
}
#Override
public void destroy() {
}
static class CensorshipResponseWrapper extends HttpServletResponseWrapper {
private final StringWriter sw = new StringWriter();
public CensorshipResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public ServletOutputStream getOutputStream() throws IOException {
// you may also fake the output stream, if some of your servlets use this method
return super.getOutputStream();
}
#Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(sw);
}
}
}
What ended up working for me was to change the logic of how I was outputting my data.
This is the data structure I was outputting that stored the results of a search using the text from a html form as input.
private final TreeMap<String, ArrayList<SearchResult>> searchResults;
So I was iterating over the contents of this data structure and printing it out to html.
public void writeSearchResultsToHtml(PrintWriter writer)
{
try
{
JSONTreeWriter. writeSearchResultsToHtml(searchResults, writer);
} catch (ArithmeticException | IllegalArgumentException | IOException | NoSuchElementException e)
{
System.err.println("Unable to write the search results builder to JSON to the file html.");
}
// clear results for next search otherwise
// the next search will contain the previous
// results, store them in history.
searchResults.clear();
}
Clearing the data structure worked great given my servlet setup.
Here was my main serverlet loop logic:
public void startServer()
{
// seed the database for testing
crawler.startCrawl("http://cs.usfca.edu/~cs212/birds/birds.html");
index.toJSON("index.json");
// type of handler that supports sessions
ServletContextHandler servletContext = null;
// turn on sessions and set context
servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
servletContext.setContextPath("/");
servletContext.addServlet(ViewServlet.class, "/");
// default handler for favicon.ico requests
DefaultHandler defaultHandler = new DefaultHandler();
defaultHandler.setServeIcon(true);
ContextHandler defaultContext = new ContextHandler("/favicon.ico");
defaultContext.setHandler(defaultHandler);
// setup handler order
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{defaultContext, servletContext});
openWebBrowser();
// setup jetty server
Server server = new Server(portNumber);
server.setHandler(handlers);
try
{
server.start();
server.join();
} catch (Exception e)
{
e.printStackTrace();
}
}
When I try using standard servlet approach, in my browser the popup window shows up asking me whether to open .xls file or save it.
I tried the exactly same code via JAX-RS and the browser popup won't show up somehow. Has anyone encounter this?
JAX-RS way that won't display popup:
#Path("excellaTest")
public class ExcellaTestResource {
#Context
private UriInfo context;
#Context
private HttpServletResponse response;
#Context
private HttpServletRequest request;
public ExcellaTestResource() {
}
#Path("horizontalProcess")
#GET
//#Produces("application/vnd.ms-excel")
#Produces("application/vnd.ms-excel")
public void getProcessHorizontally() {
try {
URL templateFileUrl = this.getClass().getResource("myExcelTemplate.xls");
String templateFilePath = URLDecoder.decode(templateFileUrl.getPath(), "UTF-8");
String outputFileDir = "MasatoExcelHorizontalOutput";
ReportProcessor reportProcessor = new ReportProcessor();
ReportBook outputBook = new ReportBook(templateFilePath, outputFileDir, ExcelExporter.FORMAT_TYPE);
ReportSheet outputSheet = new ReportSheet("myExcelSheet");
outputBook.addReportSheet(outputSheet);
reportProcessor.addReportBookExporter(new OutputStreamExporter(response));
reportProcessor.process(outputBook);
System.out.println("done!!");
}
catch(Exception e) {
System.out.println(e);
}
return;
}
}//end class
class OutputStreamExporter extends ReportBookExporter {
private HttpServletResponse response;
public OutputStreamExporter(HttpServletResponse response) {
this.response = response;
}
//ReportProcessor output()
//This method is call when ReportProcessor process() is invoked.
//The Workbook from POI API can be used to write to stream
#Override
public void output(Workbook book, BookData bookdata, ConvertConfiguration configuration) throws ExportException {
//TODO write to stream
try {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=masatoExample.xls");
book.write(response.getOutputStream());
response.getOutputStream().close();
System.out.println("booya!!");
}
catch(Exception e) {
System.out.println(e);
}
}
}//end class
What JAX-RS framework are you using?
My guess is that your code doesn't work, because you are returning void. The framework you are using probably recognizes void as HTTP 204 No Content. This causes browser to skip the actual response body and to ignore conntent-disposition header.
As I already wrote you in a parallel thread: try to returning Response object. You can put either OutputStream or byte[] as entity as set the content-disposition header.
I have never used class level injection for a jax-rs service. I suggest one of 2 solutions.
1) Try injecting the request and response as method arguments.
2) Output your file to a byte array output stream and return a byte array from your method instead of void.