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]

Reply via email to