Date: 2005-03-01T14:57:42
Editor: DakotaJack
Wiki: Apache Struts Wiki
Page: StrutsUpload
URL: http://wiki.apache.org/struts/StrutsUpload
no comment
New Page:
##language:en
== Struts Multipart Processing in Struts v1.2.6 for Upload Applications ==
The present code in Struts v1.2.6 and before is tied to the "default" or
"existing" upload application supplied in the Struts jar file. By "tied" I
mean that the code is coupled to that application such that it unnecessarily
impedes the development of competing or alternative implementations. This is
not a criticism of the default application. Rather, it is a desire to decouple
a particular implementation which is not a very sophisticated upload
application, from the framework. This is not a hard thing to do, and hopefully
this wiki page will start a discussion of how that might be achieved, if
desireable. As things stand, people regularly have to build around the
multipart processing in Struts rather than benefit from it. What is proposed
here hopefully would provide a basis for keeping what is good about the present
application while allowing other alternatives to be used too.
The changes suggested here can easily be implemented as is. I am going to,
however, first present the ideas and then see if anyone is interested in going
further.
I am going to suggest changes in multipart processing in Struts v1.2.6 and
forward. Equivalent changes are suggested for the chain implementations of
!RequestProcessor.
Let's first see that the present state of v1.2.6 is. Multipart processing in
Struts v1.2.6 is fairly simple even if a bit "discombobbled". The relevant
classes are:
1. Globals
2. !ActionForm
3. !RequestProcessor
4. !IncludeAction
5. !ActionConfig
6. !ConfigHelper
7. !ConfigHelperInterface
8. !ControllerConfig
9. !RequestUtils
Let's follow the bouncing-ball like in the old time silent movie sing-alongs.
The first thing to happen in the process method of !RequestProcessor in Struts
v1.2.6 is the wrapping of the request, if relevant, in a
!MultipartRequestWrapper with:
=== RequestProcessor ===
{{{
request = processMultipart(request);
}}}
This is a pretty simple operation/method.
{{{
protected HttpServletRequest processMultipart(HttpServletRequest request) {
if (!"POST".equalsIgnoreCase(request.getMethod())) {
return (request);
}
String contentType = request.getContentType();
if ((contentType != null) &&
contentType.startsWith("multipart/form-data")) {
return (new MultipartRequestWrapper(request));
} else {
return (request);
}
}
}}}
The reason for wrapping the request at this point is not clear, or even a
mistake. The resultant !MultipartRequestWrapper is not initialized, i.e. the
state is not set. However, in some places, because Struts v1.2.6 is Servlet
2.2 compatible, the wrapper must be unwrapped, e.g. where the request must be
accessed for forwards and includes, cf. doForward(...) and doInclude(...) in
!RequestProcessor.
If the multipart request wrapper did not implement the !HttpServletRequest,
none of these difficulties with the differences between Servlet v2.2 and v2.3
would exist. I will suggest an alternative to avoid this. There is a wise
preference of composition to inheritance in coding generally, but in this case
we do not even need composition, much less inheritance. This will be revisited
below.
This is just extra coding with no value. In !ConfigHelper this leads somehow
to the !MultipartRequestWrapper and the !MultiparRequestHandler in Struts
v1.2.6 getting confused. The following code is in !ConfigHelper:
{{{
public MultipartRequestWrapper getMultipartRequestWrapper() {
if (this.request == null) {
return null;
}
return (MultipartRequestWrapper)
this.request.getAttribute(Globals.MULTIPART_KEY);
}
}}}
However, the class which is persisted under Globals.MULTIPART_KEY is the
!MultipartRequestHandler and not the !MultipartRequestWrapper. This code can
be, and I understand will be, jettisoned entirely.
Let's look briefly at the !MultipartRequestWrapper in the upload application,
which here has insinuated itself into the Struts framework code unnecessarily.
==== EXISTING MultipartRequestWrapper ====
{{{
public class MultipartRequestWrapper implements HttpServletRequest {
protected Map parameters;
protected HttpServletRequest request;
public MultipartRequestWrapper(HttpServletRequest request) {
this.request = request;
this.parameters = new HashMap();
}
public void setParameter(String name, String value) {
String[] mValue = (String[]) parameters.get(name);
if (mValue == null) {
mValue = new String[0];
}
String[] newValue = new String[mValue.length + 1];
System.arraycopy(mValue, 0, newValue, 0, mValue.length);
newValue[mValue.length] = value;
parameters.put(name, newValue);
}
public String getParameter(String name) {
String value = request.getParameter(name);
if (value == null) {
String[] mValue = (String[]) parameters.get(name);
if ((mValue != null) && (mValue.length > 0)) {
value = mValue[0];
}
}
return value;
}
public Enumeration getParameterNames() {
Enumeration baseParams = request.getParameterNames();
Vector list = new Vector();
while (baseParams.hasMoreElements()) {
list.add(baseParams.nextElement());
}
Collection multipartParams = parameters.keySet();
Iterator iterator = multipartParams.iterator();
while (iterator.hasNext()) {
list.add(iterator.next());
}
return Collections.enumeration(list);
}
public String[] getParameterValues(String name) {
String[] value = request.getParameterValues(name);
if (value == null) {
value = (String[]) parameters.get(name);
}
return value;
}
public HttpServletRequest getRequest() {
return request;
}
//WRAPPER IMPLEMENTATIONS OF SERVLET REQUEST METHODS
.....
//WRAPPER IMPLEMENTATIONS OF HTTPSERVLETREQUEST METHODS
.....
//SERVLET 2.3 EMPTY METHODS
.....
}
}}}
We will see that can do the same thing, and more, with the far less intrusive
and hugely more flexible. I add "Upload" to the class names in the following
for purposes of ease of discussion. (I realize that we might need a real
wrapper for a request object, so I call the following code a "facade", which it
is. However, if a real wrapper is needed, to pass to the Action, which I don't
think is a good idea, actually, then we can do so by including this class in
that wrapper to get the values of the methods in our facade.
==== SUGGESTED UploadMultipartFacade ====
{{{
public interface UploadMultipartFacade {
public Iterator getParameterNames();
public String getParameter(String name);
public String[] getParameterValues(String name);
public Map getFiles();
}
}}}
In addition to the flexibility, etc. of this interface, it also gives us what
we wanted from such wrapper: the files! But, let's continue down the request
processing chain in Struts v1.2.6.
The next relevant code is in the processPopulate(...) method of
!RequestProcessor. Early on in processPopulate(...) we find the value of
Globals.MULTIPART_KEY being set. There is no need to do this so early, which
just restricts options, and the reason might be that there is confusion about
which class is covered by the key. At any rate, shortly thereafter
!RequestUtils' method populate(...) is called, and this is the first relevant
code. This is where, unfortunately, I think, the !ActionForm gets
unnecessarily wedded to the !MultipartRequestWrapper and
!MultipartRequestHandler. The populate method has as parameters !ActionForm,
String (mapping.getPrefix()), String (mapping getSuffix()), and
!HttpServletRequest (which has been cast to !MultipartRequestWrapper but no
state has been set in !MultipartRequestWrapper, so that is irrelevant. The
code is just what I call a "cuffudgal", which means generally that there is no
particular rhyme or reason to where the code is placed. This is not meant as a
criticism of anyone, but only of the code. Presumably code can be criticized
without being uncivil?
=== RequestUtils ===
with the !RequestUtils populate(...) method, the Struts v1.2.6 upload
application !MultipartRequestHandler enters the picture. The populate method
is:
{{{
public static void populate(
Object bean,
String prefix,
String suffix,
HttpServletRequest request)
throws ServletException {
HashMap properties = new HashMap();
Enumeration names = null;
Map multipartParameters = null;
String contentType = request.getContentType();
String method = request.getMethod();
boolean isMultipart = false;
if ((contentType != null)
&& (contentType.startsWith("multipart/form-data"))
&& (method.equalsIgnoreCase("POST"))) {
ActionServletWrapper servlet;
if (bean instanceof ActionForm) {
servlet = ((ActionForm) bean).getServletWrapper();
} else {
throw new ServletException("bean that's supposed to be "
+ "populated from a multipart request
is not of type "
+
"\"org.apache.struts.action.ActionForm\", but type "
+ "\""
+ bean.getClass().getName()
+ "\"");
}
MultipartRequestHandler multipartHandler = getMultipartHandler(request);
(ActionForm) bean).setMultipartRequestHandler(multipartHandler);
if (multipartHandler != null) {
isMultipart = true;
servlet.setServletFor(multipartHandler);
multipartHandler.setMapping(
(ActionMapping) request.getAttribute(Globals.MAPPING_KEY));
multipartHandler.handleRequest(request);
Boolean maxLengthExceeded =
(Boolean) request.getAttribute(
MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED);
if ((maxLengthExceeded != null) && (maxLengthExceeded.booleanValue())) {
return;
}
multipartParameters = getAllParametersForMultipartRequest(
request, multipartHandler);
names = Collections.enumeration(multipartParameters.keySet());
}
}
if (!isMultipart) {
names = request.getParameterNames();
}
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String stripped = name;
if (prefix != null) {
if (!stripped.startsWith(prefix)) {
continue;
}
stripped = stripped.substring(prefix.length());
}
if (suffix != null) {
if (!stripped.endsWith(suffix)) {
continue;
}
stripped = stripped.substring(0, stripped.length() - suffix.length());
}
Object parameterValue = null;
if (isMultipart) {
parameterValue = multipartParameters.get(name);
} else {
parameterValue = request.getParameterValues(name);
}
if (!(stripped.startsWith("org.apache.struts."))) {
properties.put(stripped, parameterValue);
}
}
try {
BeanUtils.populate(bean, properties);
} catch(Exception e) {
throw new ServletException("BeanUtils.populate", e);
}
}
}}}
The multipart request handler has the following code:
==== EXISTING MultipartRequestHandler ====
{{{
public interface MultipartRequestHandler {
public static final String ATTRIBUTE_MAX_LENGTH_EXCEEDED =
"org.apache.struts.upload.MaxLengthExceeded";
public void setServlet(ActionServlet servlet);
public void setMapping(ActionMapping mapping);
public ActionServlet getServlet();
public ActionMapping getMapping();
public void handleRequest(HttpServletRequest request) throws ServletException;
public Hashtable getTextElements();
public Hashtable getFileElements();
}
}}}
With due respect and deference to whomever coded this handler, it does way too
much. What we need is a request handler that will handle the request and get
what the multipart request wrapper needs to do its work. All the rest just
ties the class down in the middle of the framework. These details should be in
the application, if wanted, but not in the framework. I would suggest, rather,
the following interface:
==== SUGGESTED UploadMultipartRequestHandler ====
{{{
public interface UploadMultipartRequestHandler {
public void handleRequest(Object [] params) throws IOException;
}
}}}
The only params that the handleRequest(...) method "must" take are two maps:
one for parameter names and one for files in the !UploadMultipartFacade. The
interface as defined allows one to create whatever other decorating params as
one desires. I do the following in my coding, which puts the maximum file
size, encoding, and repository/store path into the mix and initializes a list
of monitors/listeners as well as the maps of parameter names and files needed
by the !UploadMultipartFacade:
First the !UploadMultipartFacadeImpl and then the !MultipartRequestHandlerImpl:
==== SUGGESTED UploadMultipartFacadeImpl ====
{{{
public class UploadMultipartFacadeImpl
implements UploadMultipartFacade {
private Map parameterNames;
private Map files;
public UploadMultipartFacadeImpl(HttpServletRequest req,
List monitors,
int fileSizeLimit,
String encoding)
throws UploadException,
IOException {
if(req == null) {
new UploadException(UploadConstant.INVALID_REQUEST);
}
parameterNames = Collections.synchronizedMap(new HashMap(89));
files = Collections.synchronizedMap(new HashMap(89));
UploadMultipartRequestHandler umrh = new
UploadMultipartRequestHandlerImpl();
Object [] objects = new Object [] { req,monitors, new
Integer(fileSizeLimit),parameterNames,files, UploadConstant.PARSER_TEMP_DIR,
encoding };
umrh.handleRequest(objects);
}
public Iterator getParameterNames() {
return this.parameterNames.keySet().iterator();
}
public String getParameter(String name) {
List parameterValues = (List)parameterNames.get(name);
if(parameterValues == null || parameterValues.size() == 0) {
return null;
}
return (String)parameterValues.get(parameterValues.size() - 1);
}
public String[] getParameterValues(String name) {
List parameterValues = (List)parameterNames.get(name);
if(parameterValues == null || parameterValues.size() == 0) {
return null;
}
return (String [])parameterValues.toArray();
}
public Map getFiles() {
return (Map)files;
}
}
}}}
==== SUGGESTED UploadMultipartRequestHandlerImpl ====
{{{
public class UploadMultipartRequestHandlerImpl
implements UploadMultipartRequestHandler {
public UploadMultipartRequestHandlerImpl() {
}
public void handleRequest(Object [] params)
throws IOException {
handleRequest((HttpServletRequest)params[0],
(List)params[1],
((Integer)params[2]).intValue(),
(Map)params[3],
(Map)params[4],
(String)params[5],
(String)params[6]);
}
private void handleRequest(HttpServletRequest req,
List monitors,
int maxFileSize,
Map parameterNames,
Map files,
String repositoryPath,
String encoding)
throws IOException {
UploadFileItemFactory ufiFactory = new UploadFileItemFactory();
ufiFactory.setMonitors(monitors);
ufiFactory.setCustom(req.getContentLength());
CommonsDiskFileUpload cdfu = new CommonsDiskFileUpload();
cdfu.setFileItemFactory(ufiFactory);
cdfu.setSizeMax(maxFileSize);
cdfu.setSizeThreshold(UploadConstant.BUFFER_SIZE);
cdfu.setRepositoryPath(repositoryPath);
if(encoding != null) {
cdfu.setHeaderEncoding(encoding);
}
List list = null;
try {
list = cdfu.parseRequest(req);
} catch(CommonsFileUploadException cfue) {
throw new IOException(cfue.getMessage());
}
Object obj = null;
for(Iterator iterator = list.iterator(); iterator.hasNext();) {
CommonsFileItem cfi = (CommonsFileItem)iterator.next();
String fieldName = cfi.getFieldName();
if(cfi.isFormField()) {
String data = null;
if(encoding != null) {
data = cfi.getString(encoding);
} else {
data = cfi.getString();
}
List names = (List)parameterNames.get(fieldName);
if(names == null) {
names = Collections.synchronizedList(new LinkedList());
parameterNames.put(fieldName, names);
}
names.add(data);
} else {
String name = cfi.getName();
if(name != null) {
UploadFile fuuf = new UploadFileImpl(cfi);
name = name.replace('\\', '/');
int j = name.lastIndexOf("/");
if(j != -1) {
name = name.substring(j + 1, name.length());
}
fuuf.setFileName(name);
fuuf.setContentType(cfi.getContentType());
fuuf.setFileSize(cfi.getSize());
files.put(fieldName, fuuf);
}
}
}
}
}
}}}
Essentially, this way of coding the internals of the framework to service
potential upload applications maximizes the potential for alternative
implementations, etc.
I think this is a good place to stop, and to see if discussions are possible on
this. I have built and entire application, utilizing wrappers around commons
fileupload classes, with the multipart facade and multipart request handler
classes in my upload directory which can be discussed further, if there is any
interest.
My eyes are going buggy from reading this, so I will do a lot of the cleanup of
typos later. Any notes on those or anything else would be appreciated.
Jack
-----
---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]