Sunday, February 24, 2008

Struts2: dynamic file download

(there is a better method for this at the end of this post)

In struts2, file is downloaded as stream using StreamResult class.
To download file of dynamic name, size or type we need to extend this StreamResult class.

StreamResult result type takes the following parameters:

* contentType - the stream mime-type as sent to the web browser (default = text/plain).
* contentLength - the stream length in bytes (the browser displays a progress bar).
* contentDispostion - the content disposition header value for specifing the file name (default = inline, values are typically filename="document.pdf".
* inputName - the name of the InputStream property from the chained action (default = inputStream).
* bufferSize - the size of the buffer to copy from input to output (default = 1024).

Example:

<result name="success" type="stream">
<param name="contentType">image/jpeg</param>
<param name="inputName">imageStream</param>
<param name="contentDisposition">filename="document.pdf"</param>

<param name="bufferSize">1024</param>
</result>


So in the extended StreamResult class we need to set these parameters dynamically.
You could download source code from here, its same as following:

package downloadexample;
import org.apache.struts2.dispatcher.StreamResult;

import com.opensymphony.xwork2.ActionInvocation;

/**
* This class for result-type="myStream"

*
* <result-types> <result-type name="myStream" default="false"
* class="downloadexample.DynamicStreamResult"/>
*
* </result-types>

*
* It extends StreamResult Used to download file as a stream.
*
* @author sheetal
*
*/


public class DynamicStreamResult extends StreamResult{
@Override

protected void doExecute(String finalLocation,
ActionInvocation invocation)
throws Exception {

//get name of downloaded file
String downloadedFileName = invocation.getStack().
findValue(conditionalParse
("name", invocation));

contentDisposition = "filename=\""

+downloadedFileName + "\"";

//get file size
contentLength = ""+ invocation.getStack().findValue(
conditionalParse("size", invocation));
// get type of file
contentType = ""+ invocation.getStack().
findValue(
conditionalParse("description", invocation));
/*
Executes the result given a final location
(jsp page, action, etc) and
the action invocation (the state in which
the action was executed).
*/

super.doExecute(finalLocation, invocation);

}

}



Let, our site is a search site where user inputs name of a file. our system searches the file in server's local directory and lets the user download it if found.

To do this, .jsp file should be include the following:


<s:form action="downloadFile" validate="true">

<s:textfield label="Search file"

name="name" required="true"/>

<s:textfield label="Define file type (image/jpeg, text/plain, application/pdf)"
name="description" required="true"/>

<s:submit value="Find file"/>

</s:form>


where downloadFile is the action for downloading file.

In your action class, add the following lines:

private String name;
//holds name of downloaded file

private InputStream inputStream;
//holds stream of downloaded file
private String description;
//holds the content type of the downloaded file

private long size;
//holds the content size of the downloaded file


//method for downloading file
public String downloadFile()
{

/*
let, method searchFile(String fileName)
does the searching for us
& returns InputStream of the file if found
and null otherwise.
*/

this.inputStream = searchFile(name);

if(inputStream !=null)
{

return Action.SUCCESS;

}
else
{
//handle error
return Action.ERROR;

}


}

//write setter getter methods
public InputStream getInputStream() throws Exception

{
return inputStream;

}

public String getName()

{
return name;
}
public void setName(String name)

{
this.name = name;
}
public String getDescription()

{
return this.description;
}
public void setDescription(String description)

{
this.description = description;
}
// write getter setter for attribute size


Now, edit your struts.xml file:


<!-- custom result type for file download -->
<result-types>
<result-type name="myStream"

default="false"
class="downloadexample.DynamicStreamResult"/>

</result-types>
<!-- action for downloading file-->
<action name="downloadFile"

method="downloadFile"
class="<action-class-name>
">

<result type="myStream"/>
<result name="error">jsps/your_error_page.jsp</result>


</action>



syntax highlighted by Code2HTML, v. 0.9.1

& we are done :D.

...................................................
May be a Better method:
the time i wrote this post i was too naive to find other solution for this. One person commented a quick solution on the blog.... thank you again. i haven't tried it, so i'm not sure if it works, check yourself...here is the comment.........

Actually, there's no need to extend the StreamResult class. You can dynamically pass the contentType (and other Stream parameters) by using parameter substitution in your Action mapping, like so:


<result name="success" type="stream">

<param name="contentType">${contentType}</param>
<param name="inputName">imageStream</param>

<param name="contentDisposition">filename="${fileName}"</param>

<param name="bufferSize">${bufferSize}</param>

</result>



You then add methods set/getContentType(), set/getFileName() and set/getBufferSize() to your Action class. In the Action method which handles your business logic, all you have to do is call this.setContentType(), this.setFileName() and this.setBufferSize() and supply whatever values you like.

---------------------------------------

36 comments:

Anonymous said...

In the .jsp "despription" should be "description".

sheetal said...

thanks a lot :)

Anonymous said...

First of all thank you for the fix, this is the only way I've found to download files using struts2.

In DynamicStreamResult class, inputName attribute must be the InputStream. In the example, just:
inputName = "inputStream";

In the action example, the size getter is missing.

sheetal said...

inputName attibute is by-default inputStream.

size getter is missing,...sorry i haven't noticed. thank you :)

Amin Tazifor said...

This was a very useful and great post, I believe the only one that I found online. It solved my entire problem. Thank you Sheethal.

Sunil Gupta said...

Hello Sheetalji,

Could you please send the proper source code for this, as i am not able to understand it fully. my email id is sunilg2000@hotmail.com

thanks in advance.

Sunil Gupta

Sunil said...

hi sheetal,

i am somehow able to run this example but still have few problems like if i download a zip file it downloads but if i download a image file or a text/xml file it does not download but opens in the browser itself. any idea, how to download all type of file using this technology.

sunilg2000@hotmail.com

sunil

BangaruPandu said...

hi sheetal,
Good day to u. Nice code snippet you posted here. I'm interested to see the whole code like from JSP, action mappings, servlet and config.xml file for this file download thing. I need it for a demo here. can u post it for me? Plz, it's bit urgent. i need the whole parts.
send to durgasravaz@gmail.com

DaShaun said...

I found your example very useful.

I am using it on my current project!

Keep up the good work!

Anonymous said...

Great Post.

Just One Issue that i am facing. When trying to download the pdf file, it shows the size as '0' zero and is not opening the file.

As guess as to why I am getting this issue. Please reply back at nanda1505@rediffmail.com. Also would request you to please send me the complete code sample.

Quick Question, how does struts closes the inputStream handle??



Regards
Sunny

chandrashekar said...

hi can u pls send me the working code of struts2 file download. am still baby in this struts2 world but now i have to implement file upload and download. my mail id is chandrucsb@gmail.com

satish said...

hi sheetal,
nice work!
can plz send me the sample source code on my id satishtale@gmail.com

Thanks lot......

Anonymous said...

I tried using your code, however at the time of download, it simply shows up a blank page and if I try to save target, it shows the download as 0 bytes. I tried debugging and the data is being read into the inputStream. Is there any reason why this problem should occur? Could you please reply at ameya.gargesh@gmail.com

Dave Newton said...

Result parameters can be set from action properties by using OGNL expressions in the result configuration.

It's unclear to me what problem you're trying to solve that isn't already solved by the framework.

Anonymous said...

I believe you have the contentDisposition incorrect. Valid values are "inline" or "attachment". The attachment also allow the specification of a filename attribute, but the syntax would be:

Content-Type: attachment; filename="attachment.ext"

(*Chris*)

sheetal said...

hi chris,
*default* value for contentDisposition is "inline", the format i mentioned is taken from struts.apache.org and it worked for me, so i don't think its wrong.
check this-
http://struts.apache.org/2.x/struts2-core/apidocs/org/apache/struts2/dispatcher/StreamResult.html

Anonymous said...

"It worked for me (when I tried it with a browser specifically designed to deal with broken values)." doesn't mean "It's right."

http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.5.1

sheetal said...

this

sheetal said...

if its wrong then some doc of stream result would mention that, but i failed to find any.

sheetal said...

Dave,
the project for which i did this, we had to download different types of files from the web site. But in action properties, i did not find any other ways to set the values of filenames and contentType values dynamically.

billgate_qn said...

in the case file of size is more than 100MB then if you use InputStream => occure outofMemory exception
Can you resolve this problem?

Dave Newton said...

@sheetal: Use dynamic results; action properties are available in the result configuration.

Anonymous said...

If you are getting size of the file as zero , you may need to set the size of the file in action class or comment
contentLength = ""+ invocation.getStack().findValue(conditionalParse("size", invocation)); , it should work.

Anonymous said...

Actually, there's no need to extend the StreamResult class. You can dynamically pass the contentType (and other Stream parameters) by using parameter substitution in your Action mapping, like so:

<result name="success" type="stream">
<param name="contentType">${contentType}</param>
<param name="inputName">imageStream</param>
<param name="contentDisposition">filename="${fileName}"</param>

<param name="bufferSize">${bufferSize}</param>
</result>

You then add methods set/getContentType(), set/getFileName() and set/getBufferSize() to your Action class. In the Action method which handles your business logic, all you have to do is call this.setContentType(), this.setFileName() and this.setBufferSize() and supply whatever values you like.

sheetal said...

thaaaaaank you :D... i'll add this to the post....thanks a lot :)

Screamy said...

/bows humbly

Anonymous said...

Hi-

Thanks so much for you post. It was very very helpful.

I was getting the errors if use the below line in struts.xml for a tag.

param name="bufferSize">${bufferSize}/param
result>

if I make the line with int number, then works fine.
< param >name="bufferSize">1024 < / param >
result

Errors:

2010-05-04 12:50:26,198 ERROR com.opensymphony.xwork2.ObjectFactory.error:27 - Unable to set parameter [bufferSize] in result of type [org.apache.struts2.dispatcher.StreamResult]
Caught OgnlException while setting property 'bufferSize' on type 'org.apache.struts2.dispatcher.StreamResult'. - Class: ognl.OgnlRuntime

I hope someone one might have found this issue already, but I do not have answer.

Thanks a lot again Sheetal.

sheetal said...

Thanks for pointing that out :)
Can I ask you a question?
Did you try the 2nd option I mentioned or the first one?

Anonymous said...

hi sheetal,

I m able to download a file using this code but if i cancel the download it is giving me the following exception:
java.lang.IllegalStateException
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:406)
at org.apache.struts2.dispatcher.Dispatcher.sendError(Dispatcher.java:770)
at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:505)
at org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77)
at org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:228)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:212)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:818)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:624)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:445)
at java.lang.Thread.run(Thread.java:619)


Thanks,
Sumita

sheetal said...

Hey Sumita,

Could you try the 2nd option I mentioned at the end of the post and see what happens?

Anonymous said...

hi,

Actually i m using following code in struts.xml:


param name="inputName" inputStream param

param name="contentDisposition" filename="abc.zip" param



inputStream is the object in my Action class. This zip file is being created dynamically. when i download it, it gets downloaded but when i click on cancel option it is giving me above exception at the back end..

Thanks,
Sumita

Hasna said...

Very Good Tutorial.....

suresh said...

hai
i am fresher to struts2.i have atask to develop the file download concept using struts2 .i understand your example , but want the complete example once ..
will u plz send me
my mail id....
suresh@gaitview.com
tanks in advance

suresh said...

hai
i am fresher to struts2.i have atask to develop the file download concept using struts2 .i understand your example , but want the complete example once ..
will u plz send me
my mail id....
suresh@gaitview.com
tanks in advance

suresh said...

hai
i am fresher to struts2.i have atask to develop the file download concept using struts2 .i understand your example , but want the complete example once ..
will u plz send me
my mail id....
suresh@gaitview.com
tanks in advance

Anonymous said...

Thanks a lot, it really helped me.

I have one question why does the download file sometimes show in browser and sometimes as an attachment.