001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/jsf/tags/sakai_2-4-1/widgets/src/java/org/sakaiproject/jsf/renderer/InputFileUploadRenderer.java $
003: * $Id: InputFileUploadRenderer.java 22247 2007-03-07 00:04:43Z ray@media.berkeley.edu $
004: **********************************************************************************
005: *
006: * Copyright (c) 2003, 2004 The Sakai Foundation.
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: ******************************************************************************/package org.sakaiproject.jsf.renderer;
021:
022: import java.io.File;
023: import java.io.IOException;
024: import java.lang.reflect.InvocationTargetException;
025: import java.lang.reflect.Method;
026:
027: import javax.faces.FacesException;
028: import javax.faces.application.FacesMessage;
029: import javax.faces.component.UIComponent;
030: import javax.faces.component.UIForm;
031: import javax.faces.component.UIInput;
032: import javax.faces.context.ExternalContext;
033: import javax.faces.context.FacesContext;
034: import javax.faces.context.ResponseWriter;
035: import javax.faces.render.Renderer;
036: import javax.servlet.http.HttpServletRequest;
037: import javax.servlet.http.HttpServletRequestWrapper;
038:
039: import org.apache.commons.fileupload.FileItem;
040: import org.sakaiproject.jsf.util.RendererUtil;
041:
042: public class InputFileUploadRenderer extends Renderer {
043: private static final String ID_INPUT_ELEMENT = ".uploadId";
044:
045: private static final String ID_HIDDEN_ELEMENT = ".hiddenId";
046:
047: private static final String ATTR_REQUEST_DECODED = ".decoded";
048:
049: private static final String[] PASSTHROUGH_ATTRIBUTES = { "accept",
050: "accesskey", "align", "disabled", "maxlength", "readonly",
051: "size", "style", "tabindex" };
052:
053: public static final String ATTR_UPLOADS_DONE = "sakai.uploads.done";
054:
055: public void encodeBegin(FacesContext context, UIComponent component)
056: throws IOException {
057: if (!component.isRendered())
058: return;
059:
060: ResponseWriter writer = context.getResponseWriter();
061: String clientId = component.getClientId(context);
062: //HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
063:
064: // check that the structure of the form is valid
065: boolean atDecodeTime = false;
066: String errorMessage = checkForErrors(context, component,
067: clientId, atDecodeTime);
068: if (errorMessage != null) {
069: addFacesMessage(context, clientId, errorMessage);
070: }
071:
072: // output field that allows user to upload a file
073: writer.startElement("input", null);
074: writer.writeAttribute("type", "file", null);
075: writer
076: .writeAttribute("name", clientId + ID_INPUT_ELEMENT,
077: null);
078: String styleClass = (String) RendererUtil.getAttribute(context,
079: component, "styleClass");
080: if (styleClass != null)
081: writer.writeAttribute("class", styleClass, null);
082: boolean writeNullPassthroughAttributes = false;
083: RendererUtil.writePassthroughAttributes(PASSTHROUGH_ATTRIBUTES,
084: writeNullPassthroughAttributes, context, component);
085: writer.endElement("input");
086:
087: // another comment
088: // output hidden field that helps test that the filter is working right
089: writer.startElement("input", null);
090: writer.writeAttribute("type", "hidden", null);
091: writer.writeAttribute("name", clientId + ID_HIDDEN_ELEMENT,
092: null);
093: writer.writeAttribute("value",
094: "filter_is_functioning_properly", null);
095: writer.endElement("input");
096: }
097:
098: public void decode(FacesContext context, UIComponent comp) {
099: UIInput component = (UIInput) comp;
100: if (!component.isRendered())
101: return;
102:
103: ExternalContext external = context.getExternalContext();
104: HttpServletRequest request = (HttpServletRequest) external
105: .getRequest();
106: String clientId = component.getClientId(context);
107: String directory = (String) RendererUtil.getAttribute(context,
108: component, "directory");
109:
110: // mark that this component has had decode() called during request
111: // processing
112: request.setAttribute(clientId + ATTR_REQUEST_DECODED, "true");
113:
114: // check for user errors and developer errors
115: boolean atDecodeTime = true;
116: String errorMessage = checkForErrors(context, component,
117: clientId, atDecodeTime);
118: if (errorMessage != null) {
119: addFacesMessage(context, clientId, errorMessage);
120: return;
121: }
122:
123: // get the file item
124: FileItem item = getFileItem(context, component);
125:
126: if (item.getName() == null || item.getName().length() == 0) {
127: if (component.isRequired()) {
128: addFacesMessage(context, clientId,
129: "Please specify a file.");
130: component.setValid(false);
131: }
132: return;
133: }
134:
135: //System.out.println("inputFileUpload: item: " + item + " name: " + item.getName() + " length: " + item.getSize());
136:
137: if (directory == null || directory.length() == 0) {
138: // just passing on the FileItem as the value of the component, without persisting it.
139: component.setSubmittedValue(item);
140: } else {
141: // persisting to a permenent file in a directory.
142: // pass on the server-side filename as the value of the component.
143: File dir = new File(directory);
144: String filename = item.getName();
145: filename = filename.replace('\\', '/'); // replaces Windows path seperator character "\" with "/"
146: filename = filename
147: .substring(filename.lastIndexOf("/") + 1);
148: File persistentFile = new File(dir, filename);
149: try {
150: item.write(persistentFile);
151: component.setSubmittedValue(persistentFile.getPath());
152: } catch (Exception ex) {
153: throw new FacesException(ex);
154: }
155: }
156:
157: }
158:
159: /**
160: * Check for errors (both developer errors and user errors) - return a
161: * user-friendly error message describing the error, or null if there are no
162: * errors.
163: */
164: private static String checkForErrors(FacesContext context,
165: UIComponent component, String clientId, boolean atDecodeTime) {
166: ExternalContext external = context.getExternalContext();
167: HttpServletRequest request = (HttpServletRequest) external
168: .getRequest();
169:
170: UIForm form = null;
171: try {
172: form = getForm(component);
173: } catch (IllegalArgumentException e) {
174: // there are more than one nested form - thats not OK!
175: return "DEVELOPER ERROR: The <inputFileUpload> tag must be enclosed in just ONE form. Nested forms confuse the browser.";
176: }
177: if (form == null
178: || !"multipart/form-data".equals(RendererUtil
179: .getAttribute(context, form, "enctype"))) {
180: return "DEVELOPER ERROR: The <inputFileUpload> tag must be enclosed in a <h:form enctype=\"multipart/form-data\"> tag.";
181: }
182:
183: // check tag attributes
184: String directory = (String) RendererUtil.getAttribute(context,
185: component, "directory");
186: if (directory != null && directory.length() != 0) {
187: // the tag is configured to persist the uploaded files to a directory.
188: // check that the specified directory exists, and is writeable
189: File dir = new File(directory);
190: if (!dir.isDirectory() || !dir.exists()) {
191: return "DEVELOPER ERROR: The directory specified on the <inputFileUpload> tag does not exist or is not writable.\n"
192: + "Check the permissions on directory:\n" + dir;
193: }
194: }
195:
196: FileItem item = getFileItem(context, component);
197: boolean isMultipartRequest = request.getContentType() != null
198: && request.getContentType().startsWith(
199: "multipart/form-data");
200: boolean wasMultipartRequestFullyParsed = request
201: .getParameter(clientId + ID_HIDDEN_ELEMENT) != null;
202: String requestFilterStatus = (String) request
203: .getAttribute("upload.status");
204: Object requestFilterUploadLimit = request
205: .getAttribute("upload.limit");
206: Exception requestFilterException = (Exception) request
207: .getAttribute("upload.exception");
208: boolean wasDecodeAlreadyCalledOnTheRequest = "true"
209: .equals(request.getAttribute(clientId
210: + ATTR_REQUEST_DECODED));
211:
212: if (wasDecodeAlreadyCalledOnTheRequest && !atDecodeTime) {
213: // decode() was already called on the request, and we're now at encode() time - so don't do further error checking
214: // as the FileItem may no longer be valid.
215: return null;
216: }
217:
218: // at this point, if its not a multipart request, it doesn't have a file and there isn't an error.
219: if (!isMultipartRequest)
220: return null;
221:
222: // check for user errors
223: if ("exception".equals(requestFilterStatus)) {
224: return "An error occured while processing the uploaded file. The error was:\n"
225: + requestFilterException;
226: } else if ("size_limit_exceeded".equals(requestFilterStatus)) {
227: // the user tried to upload too large a file
228: return "The upload size limit of "
229: + requestFilterUploadLimit
230: + "MB has been exceeded.";
231: } else if (item == null || item.getName() == null
232: || item.getName().length() == 0) {
233: // The file item will be null if the component was previously not rendered.
234: return null;
235: } else if (item.getSize() == 0) {
236: return "The filename '" + item.getName()
237: + "' is invalid. Please select a valid file.";
238: }
239:
240: if (!wasMultipartRequestFullyParsed) {
241: return "An error occured while processing the uploaded file. The error was:\n"
242: + "DEVELOPER ERROR: The <inputFileUpload> tag requires a <filter> in web.xml to parse the uploaded file.\n"
243: + "Check that the Sakai RequestFilter is properly configured in web.xml.";
244: }
245:
246: if (item.getName().indexOf("..") >= 0) {
247: return "The filename '" + item.getName()
248: + "' is invalid. Please select a valid file.";
249: }
250:
251: // everything checks out fine! The upload was parsed, and a FileItem
252: // exists with a filename and non-zero length
253: return null;
254: }
255:
256: /**
257: * Return the FileItem (if present) for the given component. Subclasses
258: * of this Renderer could get the FileItem in a different way.
259: * First, try getting it from the request attributes (Sakai style). Then
260: * try getting it from a method called getFileItem() on the HttpServletRequest
261: * (unwrapping the request if necessary).
262: */
263: private static FileItem getFileItem(FacesContext context,
264: UIComponent component) {
265: String clientId = component.getClientId(context);
266: HttpServletRequest request = (HttpServletRequest) context
267: .getExternalContext().getRequest();
268: FileItem item = null;
269: String fieldName = clientId + ID_INPUT_ELEMENT;
270:
271: // first try just getting it from the request attributes,
272: // where the Sakai RequestFilter puts it.
273: item = (FileItem) request.getAttribute(fieldName);
274: if (item != null)
275: return item;
276:
277: // For custom filter compatibility (MyFaces for example),
278: // walk up the HttpServletRequestWrapper chain looking for a getFileItem() method.
279: while (request != null) {
280: // call the getFileItem() method by reflection, so as to not introduce a dependency
281: // on MyFaces, and so the wrapper class that has getFileItem() doesn't have to
282: // implement an interface (as long as it has the right method signature it'll work).
283: try {
284: Class reqClass = request.getClass();
285: Method getFileItemMethod = reqClass.getMethod(
286: "getFileItem", new Class[] { String.class });
287: Object returned = getFileItemMethod.invoke(request,
288: new Object[] { fieldName });
289: if (returned instanceof FileItem)
290: return (FileItem) returned;
291: } catch (NoSuchMethodException nsme) {
292: } catch (InvocationTargetException ite) {
293: } catch (IllegalArgumentException iae) {
294: } catch (IllegalAccessException iaxe) {
295: }
296:
297: // trace up the request wrapper classes, looking for a getFileItem() method
298: if (request instanceof HttpServletRequestWrapper) {
299: request = (HttpServletRequest) ((HttpServletRequestWrapper) request)
300: .getRequest();
301: } else {
302: request = null;
303: }
304: }
305:
306: return null;
307: }
308:
309: private static void addFacesMessage(FacesContext context,
310: String clientId, String message) {
311: context.addMessage(clientId, new FacesMessage(
312: FacesMessage.SEVERITY_ERROR, message, message));
313: System.out.println(message);
314: }
315:
316: /**
317: * get containing UIForm from component hierarchy.
318: * @throws IllegalArgumentException If there is more than one enclosing form - only one form is allowed!
319: */
320: private static UIForm getForm(UIComponent component)
321: throws IllegalArgumentException {
322: UIForm ret = null;
323: while (component != null) {
324: if (component instanceof UIForm) {
325: if (ret != null) {
326: // Cannot have a doubly-nested form!
327: throw new IllegalArgumentException();
328: }
329: ret = (UIForm) component;
330: }
331: component = component.getParent();
332: }
333:
334: return ret;
335: }
336: }
|