[HOWTO] Dowloading files from a Portlet

Many people ask how to achieve file downloads from a portlet project.

I'm going to try putting this mystery to rest by first explaining the limitations and then showing some old and new ways of overcoming them in Liferay. To be clear on terminology we'll call anything, other than the usual presentation content, that we want to deliver to the user a resource. Examples of resources are: images, binary documents, css stylesheets, js files, and typically things that we want to dynamically generate access to users to download. They are also often generated on the fly, like a PDF report, or a captcha image.

To begin, the JSR-168/Portlet 1.0 spec did not have support for delivering anything other than a blob of text/html content. Nor did it support delivering such content outside of a response which included the portal's wrappings.

Here is a brief (and not all encompassing) recent history of resource downloads in Liferay.

Liferay <= 4.1.x: At this point the ONLY ways to provide download links were:

  1. provide a direct link to the resource
  2. delegate the functionality to a servlet

In Liferay's core this was very simple because we use Struts, so creating an action (and mapping) of type org.apache.struts.action.Action was relatively painless and we could quickly develop any download mechanism we needed. The only issue was that the URL could not be generated using the <portlet /> tag. A developer had to be aware of the path associated with the Action in order to access it.

For example, consider the following action-mapping from version 4.3.x's Document Library portlet:

<action path="/document_library/get_file" type="com.liferay.portlet.documentlibrary.action.GetFileAction" />

If you look at the code for com.liferay.portlet.documentlibrary.action.GetFileAction you will see that it implements a method:

public ActionForward strutsExecute(
		ActionMapping mapping, ActionForm form, HttpServletRequest req, HttpServletResponse res)
	throws Exception;

This method, though found in com.liferay.portal.struts.PortletAction, is simply a helper method which makes the PortletAction behave as a servlet by calling super.execute(mapping, form, req, res) directly.

In order to put this method to work we create an url as follows:

<a href="<%= themeDisplay.getPathMain() %>/document_library/get_file?folderId=<%= folderId %>&name=<%= HttpUtil.encodeURL(name) %>">
	<%= fileEntry.getTitle() %>
</a>

Notice that we haven't used any tag to generate the URL and we needed to include the portals context.

So, at this point if you were creating a portlet to be JSR-168 complient (outside of ext, or core), you could create an URL to a static resource like so (JSP example):

<a href="<%= renderRequest.getContextPath() %>/static_path/file_name.pdf">
	file_name.pdf
</a>

To create an URL to a dynamic resource, you would have to delegate to some servlet, like so:

<a href="<%= renderRequest.getContextPath() %>/servlet_path?p1=va&p2=v2">
	<%= someLinkText %>
</a>


Liferay <= 4.4.x and > 4.1.x: At this point we started to experiment with delivering portlet resources in a way similar to what was proposed in the upcoming JSR-286/Portlet 2.0 spec.

We had at our disposal a feature we had created which allowed us to deliver resources from portlets. It was used by specifying LiferayWindowState.EXCLUSIVE on a portlet URL. If using it from a actionURL it could deliver binary resources.

Example of returning a captcha image, from 4.3.x:

<portlet:actionURL windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>" var="captchaURL">
	<portlet:param name="struts_action" value="/message_boards/captcha" />
</portlet:actionURL>

On a renderURL it could be used for handling things like Ajax requests.

<form action="<liferay-portlet:renderURL 
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="struts_action" value="/password_generator/view" />
	</liferay-portlet:renderURL>" 
	method="post" 
	name="<portlet:namespace />fm" 
	onSubmit="AjaxUtil.submit(this, {update: this.parentNode}); return false;">

Unfortunately, this feature was only usable from within the EXT environment or from within the core for handling binary resources, because final handling of the response required accessing the original HttpServletResponse.

Here is the response handling side of the capcha example:

public void processAction(
		ActionMapping mapping, ActionForm form, PortletConfig config,
		ActionRequest req, ActionResponse res)
	throws Exception {

	try {
		PortletSession ses = req.getPortletSession();

		String captchaText = _producer.createText();

		ses.setAttribute(WebKeys.CAPTCHA_TEXT, captchaText);

		HttpServletResponse httpRes =
			((ActionResponseImpl)res).getHttpServletResponse();

		_producer.createImage(httpRes.getOutputStream(), captchaText);

		setForward(req, ActionConstants.COMMON_NULL);
	}
	catch (Exception e) {
		_log.error(e);
	}
}


Solution for external portlets: In order to provide a feature which was usable from portlets outside of EXT & core, we created a interface available in portal-kernel.jar and made RenderResponseImpl implement this interface. The interface provide the following methods:

public interface LiferayRenderResponse extends RenderResponse {

	public void addDateHeader(String name, long date);

	public void addHeader(String name, String value);

	public void addIntHeader(String name, int value);

	public void setDateHeader(String name, long date);

	public void setHeader(String name, String value);

	public void setIntHeader(String name, int value);

	public void setResourceName(String resourceName);

}

Additionally, we added support for changing the content type returned from RenderResponse, but ONLY if the WinowState was equal to LiferayWindowState.EXCLUISVE.

Here is an example which returns a png image:

public void doView(RenderRequest req, RenderResponse res)
	throws IOException, PortletException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo && req.getWindowState().equals(LiferayWindowState.EXCLUSIVE)) {
		LiferayRenderResponse liferayRes = (LiferayRenderResponse)res;

		liferayRes.setContentType("image/png");
		liferayRes.addHeader(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = liferayRes.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
	else {
		include(viewJSP, req, res);
	}
}

Notice that you can specify the contentType, set headers, and write binary/text data directly to the portlet's OutputStream. One note is that the content type MUST be set before attempting to call liferayRes.getPortletOutputStream(); otherwise an exception will be raised.

And the url looks like this:

<img src="<portlet:renderURL
			portletMode="view"
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="logo" value="true" />
	</portlet:renderURL>"
	alt="Image returned by portlet." />

This was pretty handy for everything from XML and JSON AJAX data, to dynamically generated binary resources.



Liferay >= 5.0.0: In the new release of Liferay, while still supporting all of the methods described above, we also support the brand new (and as of yet still unofficial) JSR-286/Portlet 2.0 feature ResourceRequest which is specifically designed for handling any resource delivery purely as a function of a portal & portlet which are JSR-286 compliant. It can equally well handle everything from binary files to JSON data returned by an AJAX request.

An example usage looks like this:

public void serveResource(ResourceRequest req, ResourceResponse res)
		throws PortletException, IOException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo) {
		res.setContentType("image/png");
		res.addProperty(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = res.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
}

And the URL looks like this:

<img src="<portlet:resourceURL>
		<portlet:param name="logo" value="true" />
	</portlet:resourceURL>"
	alt="Image returned by portlet." />

Well, there you have it.

博客
Hi Ray,

I'm wondering if the last bit of code works.
[...]
res.addProperty( HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");
[...]

Because PortletResponseImpl does not implement addProperty properly...

public void addProperty(String key, String value) {
if (key == null) {
throw new IllegalArgumentException();
}
}
(this code is from branch 5.0.x)
(see also LEP-5828)
Just committed a fix!

See http://support.liferay.com/browse/LEP-5828
I want to use this but couldn't figure out how to use it in my Struts Portlet. I am using Liferay 5.1.1. Should this be in action class or a jsp servlet or something else? Looking at the URL <portlet:resourceURL> couldn't understand how this would invoke the serveResource method. Can anyone give an elaborate explaination? Please.
Can you describe how can be used/overrided the method:

public void serveResource(ActionMapping mapping, ActionForm form,
PortletConfig portletConfig, ResourceRequest resourceRequest,
ResourceResponse resourceResponse) throws Exception { ... }

of MyStrutsPortletAction, write jsp tags and use configure Struts to download a file?

Thanks,
D.
i try to set the content-disposition to reflect the filename but it failed.

String contentDisposition = "attachment; filename=" + filename;
resourceResponse.setProperty(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);

Did anyone encounters this problem before? Any solution for it.

(I try both setProperty and addProperty but still fail to display the correct filename)
Hi,

I have a problem, i don't know how configure the files struts-config.xml and tiles-def.xml in Liferay 5.2.3.

I'm using my portlet in EXT enveroiment.

Thanks.
Jose Antonio.
How I solvethis problem for JSF-portlet???

http://stackoverflow.com/questions/2361765/problem-to-display-a-pdf-from-my-jsf-portlet-of-liferay

HEEELLLLPPPPPP!!!!
I'm using the example and it work for txt file and not for zip files. My code is in the following, and with zip files I cannot open that after download because the files seem corrupted...
String xmlString = FileUtil.read(fileDirectory+fileName);
HttpServletResponse response = PortalUtil.getHttpServletResponse(res);
in = new ByteArrayInputStream(xmlString.getBytes());
String contentType = MimeTypesUtil.getContentType(fileDirectory+fileName);
String extension = FileUtil.getExtension(fileName);
if (extension.equalsIgnoreCase("zip")) contentType="application/zip";
res.setContentType(contentType);
res.addProperty(HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");
res.addProperty(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+fileName);
res.addProperty(HttpHeaders.CONTENT_ENCODING, "Binary");
OutputStream out = res.getPortletOutputStream();
in = new ByteArrayInputStream(xmlString.getBytes());
if (in == null) {
out.close();
}
else {
byte[] buffer = new byte[4096];
int len;

while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}

out.flush();
in.close();
out.close();
}
}
You're reading a potentially binary file into a string so yeah. it's gonna be corrupted.

Stream the file right into the res, like so:

InputStream in = new FileInputStream(new File(fileDirectory+fileName));

The rest looks fine!
I'm not able to find dependencies for
import javax.portlet.ResourceResponse;
import javax.portlet.ResourceRequest;
in my portal 2.0, maven, liferay 5.2.3 application
and when I use instead of
<dependency>
<groupId>portlet-api</groupId>
<artifactId>portlet-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>

the

<dependency>
<groupId>javax.portlet</groupId>
<artifactId>portlet-api</artifactId>
<version>2.0</version>
</dependency>

Liferay crashes.
I am trying the below code to download PDF, XLS and CSV file from portlet. I am able to download CSV file currently but whenever I download PDF or XLS file I get corruptesd data. The reason for this is that I am trying to download the file in render phase which supports only text/html files. To achieve downloading for PDF and XLS we need to get it done in response phase but I dont know how to achieve this.For that I implemented the serveResource(ResourceRequest req, ResourceResponse res) method and read call the req.getResourceId() method of the ResourceRequest and invoked my business logic (create PDF) and subsequently write it to the output stream.
But still I am stuck with the same problem.Currently I am using JSF 2 on portlet. Can anyone help me with the solution?

public void DownloadPDF() throws DocumentException, FileNotFoundException, Exception
{


PortletResponse portletResponse = (PortletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
HttpServletResponse response= com.liferay.portal.util.PortalUtil.getHttpServletResponse(portletResponse);
//We must get first our context
FacesContext context = FacesContext.getCurrentInstance();

//Then we have to get the Response where to write our file

//Now we create some variables we will use for writting the file to the response
String filePath = null;
int read = 0;
byte[] bytes = new byte[1024];

String pathToTheFile="D:/NetbeansProject";
//Be sure to retrieve the absolute path to the file with the required method
filePath = pathToTheFile;

//Now set the content type for our response, be sure to use the best suitable content type depending on your file
//the content type presented here is ok for, lets say, text files and others (like CSVs, PDFs)
response.setContentType("application/pdf");

//This is another important attribute for the header of the response
//Here fileName, is a String with the name that you will suggest as a name to save as
//I use the same name as it is stored in the file system of the server.

String fileName="tablePDF.pdf";
response.setHeader("Content-Disposition", "attachment;filename=\"" +
fileName + "\"");

//Streams we will use to read, write the file bytes to our response
FileInputStream fis = null;
OutputStream os = null;

//First we load the file in our InputStream
try {
fis = new FileInputStream(new File(filePath,fileName));
os = response.getOutputStream();

} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

//While there are still bytes in the file, read them and write them to our OutputStream
try {
while((read = fis.read(bytes)) != -1){
os.write(bytes,0,read);
}
os.flush();
os.close();

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

//Clean resources

//This option isn't quite necessary, It worked for me with or without it
FacesContext.getCurrentInstance().responseComplete();

//End of the method
}
Hi All, How to download a file from portlet using a standalone java class? I was trying this by creating a simple portlet with serveResourse() method. The serveResource() would fetch the file. A standalone java class to hit this resourseURL to download a file. But I am not sure how to hit the resourseURL from a standalone java class.

Please let me know if my approach is correct or suggest if there is any better approach.

Thanks,
Ravi
Hi RAY AUGÉ,

I`m not able to download File, in "Action Class" but it works for "serveResource" Method.

Issue is to create "HttpServletResponse" object. I have posted my issue on following thread:

https://web.liferay.com/community/forums/-/message_boards/message/93422404