/* AuUploader.java
Purpose:
Description:
History:
Fri Jan 11 18:53:30 2008, Created by tomyeh
Copyright (C) 2008 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.au.http;
import static org.zkoss.lang.Generics.cast;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadBase.IOFileUploadException;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.image.AImage;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.mesg.Messages;
import org.zkoss.sound.AAudio;
import org.zkoss.util.media.AMedia;
import org.zkoss.util.media.ContentTypes;
import org.zkoss.util.media.Media;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.xml.XMLs;
import org.zkoss.zk.mesg.MZk;
import org.zkoss.zk.ui.ComponentNotFoundException;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.impl.Attributes;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.CharsetFinder;
import org.zkoss.zk.ui.util.Configuration;
/**
* The AU extension to upload files.
* It is based on Apache Commons File Upload.
*
* @author tomyeh
* @since 3.0.2
*/
public class AuUploader implements AuExtension {
private static final Logger log = LoggerFactory.getLogger(AuUploader.class);
private ServletContext _ctx;
public AuUploader() {
}
public void init(DHtmlUpdateServlet servlet) {
_ctx = servlet.getServletContext();
}
public void destroy() {
}
/** Processes a file uploaded from the client.
*/
public void service(HttpServletRequest request, HttpServletResponse response, String pathInfo)
throws ServletException, IOException {
final Session sess = Sessions.getCurrent(false);
if (sess == null) {
response.setIntHeader("ZK-Error", HttpServletResponse.SC_GONE);
return;
}
final Map<String, String> attrs = new HashMap<String, String>();
String alert = null, uuid = null, nextURI = null, sid = null;
Desktop desktop = null;
try {
if (!isMultipartContent(request)) {
if ("uploadInfo".equals(request.getParameter("cmd"))) {
// refix ZK-2056: should escape both XML and Javascript
uuid = escapeParam(request.getParameter("wid"));
sid = escapeParam(request.getParameter("sid"));
desktop = ((WebAppCtrl) sess.getWebApp()).getDesktopCache(sess)
.getDesktop(XMLs.encodeText(request.getParameter("dtid")));
Map<String, Integer> percent = cast((Map) desktop.getAttribute(Attributes.UPLOAD_PERCENT));
Map<String, Object> size = cast((Map) desktop.getAttribute(Attributes.UPLOAD_SIZE));
// ZK-2329
if (percent == null || size == null) {
response.getWriter().write("ignore");
return;
}
final String key = uuid + '_' + sid;
Object sinfo = size.get(key);
if (sinfo instanceof String) {
response.getWriter().write("error:" + sinfo);
size.remove(key);
percent.remove(key);
return;
}
final Integer p = percent.get(key);
final Long cb = (Long) sinfo;
response.getWriter()
.write((p != null ? p.intValue() : -1) + "," + (cb != null ? cb.longValue() : -1));
return;
} else
alert = "enctype must be multipart/form-data";
} else {
// refix ZK-2056: should escape both XML and Javascript
uuid = escapeParam(request.getParameter("uuid"));
sid = escapeParam(request.getParameter("sid"));
if (uuid == null || uuid.length() == 0) {
alert = "uuid is required!";
} else {
attrs.put("uuid", uuid);
attrs.put("sid", sid);
// refix ZK-2056: should escape both XML and Javascript
final String dtid = escapeParam(request.getParameter("dtid"));
if (dtid == null || dtid.length() == 0) {
alert = "dtid is required!";
} else {
desktop = ((WebAppCtrl) sess.getWebApp()).getDesktopCache(sess).getDesktop(dtid);
final Map<String, Object> params = parseRequest(request, desktop, uuid + '_' + sid);
nextURI = (String) params.get("nextURI");
// Bug 3054784
params.put("native", request.getParameter("native"));
processItems(desktop, params, attrs);
}
}
}
} catch (Throwable ex) {
if (uuid == null) {
uuid = request.getParameter("uuid");
if (uuid != null)
attrs.put("uuid", uuid);
}
if (nextURI == null)
nextURI = request.getParameter("nextURI");
if (ex instanceof ComponentNotFoundException) {
alert = Messages.get(MZk.UPDATE_OBSOLETE_PAGE, uuid);
} else if (ex instanceof IOFileUploadException) {
log.debug("File upload cancelled!");
} else {
alert = handleError(ex);
}
if (desktop != null) {
Map<String, Integer> percent = cast((Map) desktop.getAttribute(Attributes.UPLOAD_PERCENT));
Map<String, Object> size = cast((Map) desktop.getAttribute(Attributes.UPLOAD_SIZE));
final String key = uuid + '_' + sid;
if (percent != null) {
percent.remove(key);
size.remove(key);
}
}
}
if (attrs.get("contentId") == null && alert == null)
//B65-ZK-1724: display more meaningful errormessage
alert = "Upload Aborted : (contentId is required)";
if (alert != null) {
if (desktop == null) {
response.setIntHeader("ZK-Error", HttpServletResponse.SC_GONE);
return;
}
Map<String, Integer> percent = cast((Map) desktop.getAttribute(Attributes.UPLOAD_PERCENT));
Map<String, Object> size = cast((Map) desktop.getAttribute(Attributes.UPLOAD_SIZE));
final String key = uuid + '_' + sid;
if (percent != null) {
percent.remove(key);
size.put(key, alert);
}
}
if (log.isTraceEnabled())
log.trace(Objects.toString(attrs));
if (nextURI == null || nextURI.length() == 0)
nextURI = "~./zul/html/fileupload-done.html.dsp";
Servlets.forward(_ctx, request, response, nextURI, attrs, Servlets.PASS_THRU_ATTR);
}
/** Handles the exception that was thrown when uploading files,
* and returns the error message.
* When uploading file(s) causes an exception, this method will be
* called to generate the proper error message.
*
* <p>By default, it logs the error and then use {@link Exceptions#getMessage}
* to retrieve the error message.
*
* <p>If you prefer not to log or to generate the custom error message,
* you can extend this class and override this method.
* Then, specify it in web.xml as follows.
* (we change from processor0 to extension0 after ZK5.)
* @see DHtmlUpdateServlet
<code><pre><servlet>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
<init-param>
<param-name>extension0</param-name>
<param-value>/upload=com.my.MyUploader</param-value>
</init-param>
...</pre></code>
*
* @param ex the exception.
* Typical exceptions include org.apache.commons.fileupload .FileUploadBase.SizeLimitExceededException
* @since 3.0.4
*/
protected String handleError(Throwable ex) {
log.error("Failed to upload", ex);
if (ex instanceof FileUploadBase.SizeLimitExceededException) {
try {
FileUploadBase.SizeLimitExceededException fex = (FileUploadBase.SizeLimitExceededException) ex;
long size = fex.getActualSize();
long limit = fex.getPermittedSize();
final Class<?> msgClass = Classes.forNameByThread("org.zkoss.zul.mesg.MZul");
Field msgField = msgClass.getField("UPLOAD_ERROR_EXCEED_MAXSIZE");
int divisor1 = 1024;
int divisor2 = 1024 * 1024;
String[] units = new String[] { " Bytes", " KB", " MB" };
int i1 = (int) (Math.log(size) / Math.log(1024));
int i2 = (int) (Math.log(limit) / Math.log(1024));
String size_auto = Math.round(size / Math.pow(1024, i1)) + units[i1];
String limit_auto = Math.round(limit / Math.pow(1024, i2)) + units[i2];
Object[] args = new Object[] { size_auto, limit_auto, size, limit,
String.valueOf((Long) (size / divisor1)) + units[1],
String.valueOf((Long) (limit / divisor1)) + units[1],
String.valueOf((Long) (size / divisor2)) + units[2],
String.valueOf((Long) (limit / divisor2)) + units[2] };
return Messages.get(msgField.getInt(null), args);
} catch (Throwable e) {
log.error("Failed to parse upload error message..", e);
}
}
return Exceptions.getMessage(ex);
}
/** Process fileitems named file0, file1 and so on.
*/
private static final void processItems(Desktop desktop, Map<String, Object> params, Map<String, String> attrs)
throws IOException {
final List<Media> meds = new LinkedList<Media>();
final boolean alwaysNative = "true".equals(params.get("native"));
final Object fis = params.get("file");
if (fis instanceof FileItem) {
meds.add(processItem(desktop, (FileItem) fis, alwaysNative,
(org.zkoss.zk.ui.sys.DiskFileItemFactory) params.get("diskFileItemFactory")));
} else if (fis != null) {
for (Iterator it = ((List) fis).iterator(); it.hasNext();) {
meds.add(processItem(desktop, (FileItem) it.next(), alwaysNative,
(org.zkoss.zk.ui.sys.DiskFileItemFactory) params.get("diskFileItemFactory")));
}
}
final String contentId = Strings
.encode(new StringBuffer(12).append("z__ul_"), ((DesktopCtrl) desktop).getNextKey()).toString();
attrs.put("contentId", contentId);
desktop.setAttribute(contentId, meds);
}
/** Process the specified fileitem.
*/
private static final Media processItem(Desktop desktop, FileItem fi, boolean alwaysNative,
org.zkoss.zk.ui.sys.DiskFileItemFactory factory) throws IOException {
String name = getBaseName(fi);
if (name != null) {
//Not sure whether a name might contain ;jsessionid or similar
//But we handle this case: x.y;z
final int j = name.lastIndexOf(';');
if (j > 0) {
final int k = name.lastIndexOf('.');
if (k >= 0 && j > k && k > name.lastIndexOf('/'))
name = name.substring(0, j);
}
}
String ctype = fi.getContentType(),
ctypelc = ctype != null ? ctype.toLowerCase(java.util.Locale.ENGLISH) : null;
if (name != null && "application/octet-stream".equals(ctypelc)) { //Bug 1896291: IE limit
final int j = name.lastIndexOf('.');
if (j >= 0) {
String s = ContentTypes.getContentType(name.substring(j + 1));
if (s != null)
ctypelc = ctype = s;
}
}
// ZK 3132, a way to customize it
if (factory != null) {
return factory.createMedia(fi, ctype, name, alwaysNative);
}
if (!alwaysNative && ctypelc != null) {
if (ctypelc.startsWith("image/")) {
try {
return fi.isInMemory() ? new AImage(name, fi.get()) : new AImage(name, fi.getInputStream());
//note: AImage converts stream to binary array
} catch (Throwable ex) {
if (log.isDebugEnabled())
log.debug("Unknown file format: " + ctype);
}
} else if (ctypelc.startsWith("audio/")) {
try {
return fi.isInMemory() ? new AAudio(name, fi.get()) : new StreamAudio(name, fi, ctypelc);
} catch (Throwable ex) {
if (log.isDebugEnabled())
log.debug("Unknown file format: " + ctype);
}
} else if (ctypelc.startsWith("text/")) {
String charset = getCharset(ctype);
if (charset == null) {
final Configuration conf = desktop.getWebApp().getConfiguration();
final CharsetFinder chfd = conf.getUploadCharsetFinder();
if (chfd != null)
charset = chfd.getCharset(ctype,
fi.isInMemory() ? new ByteArrayInputStream(fi.get()) : fi.getInputStream());
if (charset == null)
charset = conf.getUploadCharset();
}
return fi.isInMemory() ? new AMedia(name, null, ctype, fi.getString(charset))
: new ReaderMedia(name, null, ctype, fi, charset);
}
}
return fi.isInMemory() ? new AMedia(name, null, ctype, fi.get()) : new StreamMedia(name, null, ctype, fi);
}
private static String getCharset(String ctype) {
final String ctypelc = ctype.toLowerCase(java.util.Locale.ENGLISH);
for (int j = 0; (j = ctypelc.indexOf("charset", j)) >= 0; j += 7) {
int k = Strings.skipWhitespacesBackward(ctype, j - 1);
if (k < 0 || ctype.charAt(k) == ';') {
k = Strings.skipWhitespaces(ctype, j + 7);
if (k <= ctype.length() && ctype.charAt(k) == '=') {
j = ctype.indexOf(';', ++k);
String charset = (j >= 0 ? ctype.substring(k, j) : ctype.substring(k)).trim();
if (charset.length() > 0)
return charset;
break; //use default
}
}
}
return null;
}
/** Parses the multipart request into a map of
* (String nm, FileItem/String/List(FileItem/String)).
*/
private static Map<String, Object> parseRequest(HttpServletRequest request, Desktop desktop, String key)
throws FileUploadException {
final Map<String, Object> params = new HashMap<String, Object>();
final Configuration conf = desktop.getWebApp().getConfiguration();
int thrs = conf.getFileSizeThreshold();
int sizeThreadHold = 1024 * 128; // maximum size that will be stored in memory
if (thrs > 0)
sizeThreadHold = 1024 * thrs;
File repository = null;
if (conf.getFileRepository() != null) {
repository = new File(conf.getFileRepository());
if (!repository.isDirectory()) {
log.warn("The file repository is not a directory! [" + repository + "]");
}
}
org.zkoss.zk.ui.sys.DiskFileItemFactory dfiFactory = null;
if (conf.getFileItemFactoryClass() != null) {
Class<?> cls = conf.getFileItemFactoryClass();
try {
dfiFactory = (org.zkoss.zk.ui.sys.DiskFileItemFactory) cls.newInstance();
params.put("diskFileItemFactory", dfiFactory);
} catch (Exception ex) {
throw UiException.Aide.wrap(ex, "Unable to construct " + cls);
}
}
final ItemFactory fty = new ItemFactory(desktop, request, key, sizeThreadHold, repository, dfiFactory);
final ServletFileUpload sfu = new ServletFileUpload(fty);
sfu.setProgressListener(fty.new ProgressCallback());
Integer maxsz = conf.getMaxUploadSize();
try {
maxsz = (Integer) desktop.getComponentByUuid(request.getParameter("uuid"))
.getAttribute(org.zkoss.zk.ui.impl.Attributes.UPLOAD_MAX_SIZE);
} catch (NumberFormatException e) {
}
sfu.setSizeMax(maxsz != null ? (maxsz >= 0 ? 1024L * maxsz : -1) : -1);
for (Iterator it = sfu.parseRequest(request).iterator(); it.hasNext();) {
final FileItem fi = (FileItem) it.next();
final String nm = fi.getFieldName();
final Object val;
if (fi.isFormField()) {
val = fi.getString();
} else {
val = fi;
}
final Object old = params.put(nm, val);
if (old != null) {
final List<Object> vals;
if (old instanceof List) {
params.put(nm, vals = cast((List) old));
} else {
params.put(nm, vals = new LinkedList<Object>());
vals.add(old);
}
vals.add(val);
}
}
return params;
}
/** Returns the base name for FileItem (i.e., removing path).
*/
private static String getBaseName(FileItem fi) {
String name = fi.getName();
if (name == null)
return null;
final String[] seps = { "/", "\\", "%5c", "%5C", "%2f", "%2F" };
for (int j = seps.length; --j >= 0;) {
final int k = name.lastIndexOf(seps[j]);
if (k >= 0)
name = name.substring(k + seps[j].length());
}
return name;
}
/**
* Internal Use Only.
*/
private static String escapeParam(String param) {
return Strings.escape(XMLs.encodeText(param), Strings.ESCAPE_JAVASCRIPT);
}
/** Returns whether the request contains multipart content.
*/
public static final boolean isMultipartContent(HttpServletRequest request) {
return "post".equals(request.getMethod().toLowerCase(java.util.Locale.ENGLISH))
&& FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}
private static class StreamMedia extends AMedia {
private final FileItem _fi;
public StreamMedia(String name, String format, String ctype, FileItem fi) {
super(name, format, ctype, DYNAMIC_STREAM);
_fi = fi;
}
public java.io.InputStream getStreamData() {
try {
return _fi.getInputStream();
} catch (IOException ex) {
throw new UiException("Unable to read " + _fi, ex);
}
}
public boolean isBinary() {
return true;
}
public boolean inMemory() {
return false;
}
}
private static class ReaderMedia extends AMedia {
private final FileItem _fi;
private final String _charset;
public ReaderMedia(String name, String format, String ctype, FileItem fi, String charset) {
super(name, format, ctype, DYNAMIC_READER);
_fi = fi;
_charset = charset;
}
public java.io.Reader getReaderData() {
try {
return new java.io.InputStreamReader(_fi.getInputStream(), _charset);
} catch (IOException ex) {
throw new UiException("Unable to read " + _fi, ex);
}
}
public boolean isBinary() {
return false;
}
public boolean inMemory() {
return false;
}
}
private static class StreamAudio extends AAudio {
private final FileItem _fi;
private String _format;
private String _ctype;
public StreamAudio(String name, FileItem fi, String ctype) throws IOException {
super(name, DYNAMIC_STREAM);
_fi = fi;
_ctype = ctype;
}
public java.io.InputStream getStreamData() {
try {
return _fi.getInputStream();
} catch (IOException ex) {
throw new UiException("Unable to read " + _fi, ex);
}
}
public String getFormat() {
if (_format == null) {
_format = ContentTypes.getFormat(getContentType());
}
return _format;
}
public String getContentType() {
return _ctype != null ? _ctype : _fi.getContentType();
}
}
/**
* The file item factory that monitors the progress of uploading.
*/
private static class ItemFactory extends DiskFileItemFactory implements Serializable {
private final Desktop _desktop;
private final String _key;
/** The total length (content length). */
private long _cbtotal;
/** # of bytes being received. */
private long _cbrcv;
private org.zkoss.zk.ui.sys.DiskFileItemFactory _factory;
@SuppressWarnings("unchecked")
/*package*/ ItemFactory(Desktop desktop, HttpServletRequest request, String key, int sizeThreshold,
File repository, org.zkoss.zk.ui.sys.DiskFileItemFactory factory) {
super(sizeThreshold, repository);
_factory = factory;
_desktop = desktop;
_key = key;
long cbtotal = 0;
String ctlen = request.getHeader("content-length");
if (ctlen != null)
try {
cbtotal = Long.parseLong(ctlen.trim());
//if (log.isDebugEnabled()) log.debug("content-length="+cbtotal);
} catch (Throwable ex) {
log.warn("", ex);
}
_cbtotal = cbtotal;
if (_desktop.getAttribute(Attributes.UPLOAD_PERCENT) == null) {
_desktop.setAttribute(Attributes.UPLOAD_PERCENT, new HashMap());
_desktop.setAttribute(Attributes.UPLOAD_SIZE, new HashMap());
}
((Map) _desktop.getAttribute(Attributes.UPLOAD_PERCENT)).put(key, new Integer(0));
((Map) _desktop.getAttribute(Attributes.UPLOAD_SIZE)).put(key, new Long(_cbtotal));
}
@SuppressWarnings("unchecked")
/*package*/ void onProgress(long cbRead) {
int percent = 0;
if (_cbtotal > 0) {
_cbrcv = cbRead;
percent = (int) (_cbrcv * 100 / _cbtotal);
}
((Map) _desktop.getAttribute(Attributes.UPLOAD_PERCENT)).put(_key, new Integer(percent));
}
//-- FileItemFactory --//
public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName) {
if (_factory != null)
return _factory.createItem(fieldName, contentType, isFormField, fileName, getSizeThreshold(),
getRepository());
return new ZkFileItem(fieldName, contentType, isFormField, fileName, getSizeThreshold(), getRepository());
}
//-- helper classes --//
/** FileItem created by {@link ItemFactory}.
*/
/*package*/ class ZkFileItem extends DiskFileItem {
/*package*/ ZkFileItem(String fieldName, String contentType, boolean isFormField, String fileName,
int sizeThreshold, File repository) {
super(fieldName, contentType, isFormField, fileName, sizeThreshold, repository);
}
/** Returns the charset by parsing the content type.
* If none is defined, UTF-8 is assumed.
*/
public String getCharSet() {
final String charset = super.getCharSet();
return charset != null ? charset : "UTF-8";
}
}
/*package*/ class ProgressCallback implements ProgressListener {
public void update(long pBytesRead, long pContentLength, int pItems) {
onProgress(pBytesRead);
if (pContentLength >= 0)
_cbtotal = pContentLength;
}
}
}
}