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]