001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.wicket.markup.html.form.upload;
018:
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.Collections;
022: import java.util.Iterator;
023: import java.util.Map;
024: import java.util.Map.Entry;
025:
026: import org.apache.wicket.Request;
027: import org.apache.wicket.ResourceReference;
028: import org.apache.wicket.markup.ComponentTag;
029: import org.apache.wicket.markup.html.IHeaderContributor;
030: import org.apache.wicket.markup.html.IHeaderResponse;
031: import org.apache.wicket.markup.html.WebComponent;
032: import org.apache.wicket.markup.html.WebMarkupContainer;
033: import org.apache.wicket.markup.html.basic.Label;
034: import org.apache.wicket.markup.html.form.Form;
035: import org.apache.wicket.markup.html.form.FormComponentPanel;
036: import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
037: import org.apache.wicket.model.AbstractReadOnlyModel;
038: import org.apache.wicket.model.IModel;
039: import org.apache.wicket.model.Model;
040: import org.apache.wicket.protocol.http.IMultipartWebRequest;
041: import org.apache.wicket.util.convert.ConversionException;
042: import org.apache.wicket.util.string.Strings;
043: import org.apache.wicket.util.upload.FileItem;
044:
045: /**
046: * Form component that allows the user to select multiple files to upload via a
047: * single <input type="file"/> field.
048: *
049: * Notice that this component clears its model at the end of the request, so the
050: * uploaded files MUST be processed within the request they were uploaded.
051: *
052: * Uses javascript implementation from
053: * http://the-stickman.com/web-development/javascript/upload-multiple-files-with-a-single-file-element/
054: *
055: * For customizing caption text see {@link #RESOURCE_LIMITED} and
056: * {@link #RESOURCE_UNLIMITED}
057: *
058: * For an example of styling using CSS see the upload example in wicket-examples
059: *
060: * @author Igor Vaynberg (ivaynberg)
061: */
062: public class MultiFileUploadField extends FormComponentPanel implements
063: IHeaderContributor {
064: private static final long serialVersionUID = 1L;
065:
066: /**
067: * Represents an unlimited max count of uploads
068: */
069: public static final int UNLIMITED = 1;
070:
071: /**
072: * Resource key used to retrieve caption message for when a limit on the
073: * number of uploads is specfied. The limit is represented via ${max}
074: * variable.
075: *
076: * Example: org.apache.wicket.mfu.caption.limited=Files (maximum ${max}):
077: */
078: public static final String RESOURCE_LIMITED = "org.apache.wicket.mfu.caption.limited";
079:
080: /**
081: * Resource key used to retrieve caption message for when no limit on the
082: * number of uploads is specfied.
083: *
084: * Example: org.apache.wicket.mfu.caption.unlimited=Files:
085: */
086: public static final String RESOURCE_UNLIMITED = "org.apache.wicket.mfu.caption.unlimited";
087:
088: private static final String NAME_ATTR = "name";
089:
090: private static final String MAGIC_SEPARATOR = "_mf_";
091:
092: private static final ResourceReference JS = new JavascriptResourceReference(
093: MultiFileUploadField.class, "MultiFileUploadField.js");
094:
095: private final WebComponent upload;
096: private final WebMarkupContainer container;
097:
098: private final int max;
099:
100: private transient String[] inputArrayCache = null;
101:
102: /**
103: * Constructor
104: *
105: * @param id
106: * @param model
107: */
108: public MultiFileUploadField(String id) {
109: this (id, null, -1);
110: }
111:
112: /**
113: * Constructor
114: *
115: * @param id
116: * @param model
117: * @param max
118: * max number of files a user can upload
119: */
120: public MultiFileUploadField(String id, int max) {
121: this (id, null, max);
122: }
123:
124: /**
125: * Constructor
126: *
127: * @param id
128: * @param model
129: */
130: public MultiFileUploadField(String id, IModel model) {
131: this (id, model, UNLIMITED);
132: }
133:
134: /**
135: * Constructor
136: *
137: * @param id
138: * @param model
139: * @param max
140: * max number of files a user can upload
141: *
142: */
143: public MultiFileUploadField(String id, IModel model, int max) {
144: super (id, model);
145:
146: this .max = max;
147:
148: upload = new WebComponent("upload");
149: upload.setOutputMarkupId(true);
150: add(upload);
151:
152: container = new WebMarkupContainer("container");
153: container.setOutputMarkupId(true);
154: add(container);
155:
156: container.add(new Label("caption", new CaptionModel()));
157: }
158:
159: /**
160: * @see org.apache.wicket.markup.html.form.FormComponentPanel#onComponentTag(org.apache.wicket.markup.ComponentTag)
161: */
162: protected void onComponentTag(ComponentTag tag) {
163: super .onComponentTag(tag);
164: // remove the name attribute added by the FormComponent
165: if (tag.getAttributes().containsKey(NAME_ATTR)) {
166: tag.getAttributes().remove(NAME_ATTR);
167: }
168: }
169:
170: /**
171: * @see org.apache.wicket.Component#onBeforeRender()
172: */
173: protected void onBeforeRender() {
174: super .onBeforeRender();
175:
176: // auto toggle form's multipart property
177: Form form = (Form) findParent(Form.class);
178: if (form == null) {
179: // woops
180: throw new IllegalStateException("Component "
181: + getClass().getName() + " must have a "
182: + Form.class.getName()
183: + " component above in the hierarchy");
184: }
185: form.setMultiPart(true);
186: }
187:
188: /**
189: * @see org.apache.wicket.markup.html.IHeaderContributor#renderHead(org.apache.wicket.markup.html.IHeaderResponse)
190: */
191: public void renderHead(IHeaderResponse response) {
192: // initialize the javascript library
193: response.renderJavascriptReference(JS);
194: response.renderOnDomReadyJavascript("new MultiSelector('"
195: + getInputName() + "', document.getElementById('"
196: + container.getMarkupId() + "'), " + max
197: + ").addElement(document.getElementById('"
198: + upload.getMarkupId() + "'));");
199: }
200:
201: /**
202: * @see org.apache.wicket.markup.html.form.FormComponent#getInputAsArray()
203: */
204: public String[] getInputAsArray() {
205: // fake the input array as if it contained an array of all uploaded file
206: // names
207:
208: if (inputArrayCache == null) {
209: // this array will aggregate all input names
210: ArrayList names = null;
211:
212: final Request request = getRequest();
213: if (request instanceof IMultipartWebRequest) {
214: // retrieve the filename->FileItem map from request
215: final Map itemNameToItem = ((IMultipartWebRequest) request)
216: .getFiles();
217: Iterator it = itemNameToItem.entrySet().iterator();
218: while (it.hasNext()) {
219: // iterate over the map and build the list of filenames
220:
221: final Entry entry = (Entry) it.next();
222: final String name = (String) entry.getKey();
223: final FileItem item = (FileItem) entry.getValue();
224:
225: if (!Strings.isEmpty(name)
226: && name.startsWith(getInputName()
227: + MAGIC_SEPARATOR)
228: && !Strings.isEmpty(item.getName())) {
229:
230: // make sure the fileitem belongs to this component and
231: // is not empty
232:
233: names = (names != null) ? names
234: : new ArrayList();
235: names.add(name);
236: }
237: }
238: }
239:
240: if (names != null) {
241: inputArrayCache = (String[]) names
242: .toArray(new String[names.size()]);
243: }
244: }
245: return inputArrayCache;
246:
247: }
248:
249: /**
250: * @see org.apache.wicket.markup.html.form.FormComponent#convertValue(java.lang.String[])
251: */
252: protected Object convertValue(String[] value)
253: throws ConversionException {
254: // convert the array of filenames into a collection of FileItems
255:
256: Collection uploads = null;
257:
258: final String[] filenames = getInputAsArray();
259:
260: if (filenames != null) {
261: final IMultipartWebRequest request = (IMultipartWebRequest) getRequest();
262:
263: uploads = new ArrayList(filenames.length);
264:
265: for (int i = 0; i < filenames.length; i++) {
266: uploads.add(new FileUpload(request
267: .getFile(filenames[i])));
268: }
269: }
270:
271: return uploads;
272:
273: }
274:
275: /**
276: * @see org.apache.wicket.markup.html.form.FormComponent#updateModel()
277: */
278: public void updateModel() {
279: final Object object = getModelObject();
280:
281: // figure out if there is an existing model object collection for us to
282: // reuse
283: if (object == null) {
284: // no existing collection, push the one we created
285: setModelObject(getConvertedInput());
286: } else {
287: if (!(object instanceof Collection)) {
288: // fail early if there is something interesting in the model
289: throw new IllegalStateException("Model object of "
290: + getClass().getName()
291: + " component must be of type `"
292: + Collection.class.getName() + "<"
293: + FileUpload.class.getName()
294: + ">` but is of type `"
295: + object.getClass().getName() + "`");
296: } else {
297: // refresh the existing collection
298: Collection collection = (Collection) object;
299: collection.clear();
300: if (getConvertedInput() != null) {
301: collection.addAll((Collection) getConvertedInput());
302: }
303:
304: // push the collection in case the model is listening to
305: // setobject calls
306: setModelObject(collection);
307: }
308: }
309: }
310:
311: /**
312: * @see org.apache.wicket.Component#onDetach()
313: */
314: protected void onDetach() {
315: // cleanup any opened filestreams
316: Collection uploads = (Collection) getConvertedInput();
317: if (uploads != null) {
318: Iterator it = uploads.iterator();
319: while (it.hasNext()) {
320: final FileUpload upload = (FileUpload) it.next();
321: upload.closeStreams();
322: }
323: }
324:
325: // cleanup any caches
326: inputArrayCache = null;
327:
328: // clean up the model because we dont want FileUpload objects in session
329: Object modelObject = getModelObject();
330: if (modelObject != null && (modelObject instanceof Collection)) {
331: ((Collection) modelObject).clear();
332: }
333:
334: super .onDetach();
335: }
336:
337: /**
338: * Model that will construct the caption string
339: *
340: * @author ivaynberg
341: */
342: private class CaptionModel extends AbstractReadOnlyModel {
343:
344: private static final long serialVersionUID = 1L;
345:
346: /**
347: * @see org.apache.wicket.model.AbstractReadOnlyModel#getObject()
348: */
349: public Object getObject() {
350: if (max == UNLIMITED) {
351: return getString(RESOURCE_UNLIMITED);
352: } else {
353: return getString(RESOURCE_LIMITED, Model
354: .valueOf(Collections.singletonMap("max",
355: new Integer(max))));
356: }
357: }
358:
359: }
360: }
|