我被这个矛盾坑的要死要活,又一时半会儿找不到别的合适的富文本编辑器,所以只好把 UEditor 内部封装的代码改了一些些来实现这个功能,以下是正文。
Ueditor JAVA版上传图片工作原理
UEditor 与后台的全部交互,都是通过 ueditor/jsp下的一个叫做 controller.jsp 来分派的。
每一次ueditor 与后台交互的请求,都会有一个参数 action来标记这次请求究竟是什么动作。其中上传图片的请求涉及到两个 action,分别是“config”和“uploadimage”,“config”是读取 ueditor 的配置文件,就是同一个目录下的 config.json 文件;“uploadimage”就是处理上传图片的 action。
controller.jsp 只有两句话,见图
String rootPath = application.getRealPath("/"); out.println(new ActionEnter(request,rootPath).exec());
其中的 ActionEnter 实例对象会处理具体的内部逻辑,上传图片成功后,这个对象的 exec 方法会返回一个如下格式的json 串.
{ "state":"SUCCESS", "title":"14110403960651.jpg", "url":"14110403960651.jpg", "original":"1.jpg", "size":11098, "type":".jpg" }
ttile 是用在文本中,给 img 标签设定 title 属性用的,original 是指上传图片文件的原始文件名,url 是读取图片的 url 路径。但是正如大家看到的,这个 url 参数返回的并不是完整的 url 路径,完整的路径需要配合配置文件config.json里面的参数imageUrlPrefix拼接出来。
{ //..... //把这个属性的值改成自己sae storage 的路径 "imageUrlPrefix": "http://yourappname-youdomainname.stor.sinaapp.com/" //...... }
所以上传之后的图片完整路径就是:http://yourappname-youdomainname.stor.sinaapp.com/14110403960651.jpg
修改 controller.jsp 文件
这里我们需要做的事情就是在controller.jsp 文件里,把上传图片的 action 拦截,写成自己的逻辑,这里我建立了一个类叫做 UploadToStorage 用来处理上传图片。
String rootPath = application.getRealPath("/"); ActionEnter enter = new ActionEnter(request,rootPath); String result = enter.exec(); String action = request.getParameter("action"); if("uploadimage".equals(action)){ out.println(UploadToStorage.upload(request, response)); } else out.println(result);
当我在 UploadToStorage 这个类的 upload 方法里面通过 InputStream 读取 request 里面的文件内容时,惊人的发现居然读不出任何内容!我就头大的想撞墙死,明明已经获取了参数,可是 request 的内容却读不出来。后来翻了很多文章,才知道原来 request 的 inputStream 是只能读取一次的。一旦调用过 getParameter 这样的跟参数有关的方法,inputStream 就已经指向末尾而且不能被 reset,所以一定要在读取参数之前先把 inputStream 里面的内容读取来缓存好。(这部分请参考文章 http://www.tuicool.com/articles/rEreEb)
所以我把 controller.jsp 文件改成了这样
InputStream in = request.getInputStream(); ByteArrayOutputStream baOut = new ByteArrayOutputStream(); int n; while((n=in.read())!=-1){ //把 request 里面的内容全部读入字节缓存 baOut.write(n); } baOut.close(); //生成 request 内容的字节数组 byte[] b = baOut.toByteArray(); request.setCharacterEncoding("utf-8"); response.setHeader("Content-Type", "text/html"); String rootPath = application.getRealPath("/"); ActionEnter enter = new ActionEnter(request,rootPath); String result = enter.exec(); String action = request.getParameter("action"); if("uploadimage".equals(action)){ //这个地方把内容的字节数组传递进去,让方法处理文件 out.println(UploadToStorage.upload(request,b)); } else out.println(result);
UploadToStorage 类的上传图片方法
废话不多说,上代码。
package com.tastinglib.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.sf.json.JSONObject; import org.apache.commons.fileupload.DiskFileUpload; import org.apache.commons.fileupload.FileItem; import com.sina.sae.storage.SaeStorage; import com.sun.corba.se.impl.ior.WireObjectKeyTemplate; public class UploadToStorage { private static final int NONE = 0x10001; private static final int DATA_HEAD = 0X10002; private static final int FIELD_DATA = 0x10003; private static final int FILE_DATA = 0x10004; private static final String DOMAIN = "yourdomain"; /** * @category 上传图片方法 * @param request * http 请求 * @param requestContent * 已经读取出来的 request 请求的内容字节数组 * @return 符合 UEditor 返回格式的 json 串 */ public static String upload(HttpServletRequest request, byte[] requestContent) { String result = ""; try { request.setCharacterEncoding("utf-8"); // 根据自己的 app 生成 storage 实例 SaeStorage storage = new SaeStorage("youraccesskey", "youraccesssecret", "youraappname"); // 保存文件 result = getFile(requestContent, request, storage).toString(); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return result; } /** * @category 保存文件并返回标准格式 json 串 * @param contentBytes * request 内容字节数组 * @param request * HttpServlet * @param storage * SAEStorage * @return json 串 * @throws IOException */ private static JSONObject getFile(byte[] contentBytes, HttpServletRequest request, SaeStorage storage) throws IOException { // 用来解析内容 int status = NONE; // 目前我自己的业务逻辑,一个请求只需要处理一个文件,所以我写成了只要上传一个文件就返回 boolean hasFile = false; // 最终返回的 json JSONObject obj = new JSONObject(); String headers = request.getHeader("Content-Type"); headers = new String(headers.getBytes(), "utf-8"); // 从这一下都是读取 request 内容的语句 int pos = headers.indexOf("boundary="); if (pos >= 0) { pos += "boundary=".length(); } String lastBoundary = headers.substring(pos) + "--"; String boundary = "--" + headers.substring(pos); String reqStr = new String(contentBytes); String line = null; StringReader strReader = new StringReader(reqStr); BufferedReader reader = new BufferedReader(strReader); String fileName = ""; while ((line = reader.readLine()) != null && !hasFile) { if (line.equalsIgnoreCase(lastBoundary)) break; switch (status) { case NONE: if (line.startsWith(boundary)) { // 如果读到分界符,则表示下一行一个表头 status = DATA_HEAD;// 状态设为表示表头信息 } break; case DATA_HEAD: pos = line.indexOf("filename="); if (pos > 0) { String temp = line; pos = line.indexOf("filename=") + "filename=".length() + 1; line = line.substring(pos, line.length() - 1); pos = line.lastIndexOf("//");// 转义字符 fileName = line.substring(pos + 1); pos = byteIndexOf(contentBytes, temp, 0);// 定位行 // 定位下一行,2表示一个回车和一个换行占2个字节 contentBytes = subBytes(contentBytes, pos + temp.getBytes().length + 2, contentBytes.length); // 再读一行信息,是这一部分数据的Content-type line = reader.readLine(); // 设置文件输入流,准备写文件 /** * 字节数组再往下一行,4表示两个回车换行占4个字节。本行(指Content-type行)的 * 回车换行2个字节,Content-type的下一行是回车换行表示的空行占2个字节 得到文件数据的起始位置 */ contentBytes = subBytes(contentBytes, line.getBytes().length + 4, contentBytes.length); // 定位文件数据的结尾 pos = byteIndexOf(contentBytes, boundary, 0); // 获取文件数据,pos-2是因为在文件数据和boundary之间有一回车换行表示的空行 contentBytes = subBytes(contentBytes, 0, pos - 2); // 将文件数据存盘 // fileOut.write(contentBytes); String storageFileName = System.currentTimeMillis() + fileName; storage.write(DOMAIN, storageFileName, contentBytes); obj.put("state", "SUCCESS"); obj.put("title", storageFileName); obj.put("url", storageFileName); obj.put("original", fileName); obj.put("size", contentBytes.length); int typePos = storageFileName.lastIndexOf("."); String fileType = storageFileName.substring(typePos); obj.put("type", fileType); // fileOut.close(); // 文件长度存入fileLength status = FILE_DATA; hasFile = true; } break; case FILE_DATA: while ((!line.startsWith(boundary)) && (!line.startsWith(lastBoundary))) line = reader.readLine(); if (line.startsWith(boundary)) status = DATA_HEAD; break; } } return obj; } /** * @param b * 要搜索的字节数组 * @param s * 要查找的字符串 * @param start * 搜索的起始位置 * @return 如果找到返回s的第一个字节在字节数组中的下标,否则返回-1 */ private static int byteIndexOf(byte[] b, String s, int start) { return byteIndexOf(b, s.getBytes(), start); } /** * @param b * 要搜索的字节数组 * @param s * 要查找的字节数组 * @param start * 搜索的起始位置 * @return 如果找到返回s的第一个字节在字节数组中的下标,否则返回-1 */ private static int byteIndexOf(byte[] b, byte[] s, int start) { int i; if (s.length == 0) return 0; int max = b.length - s.length; if (max < 0) return -1; else if (start > max) return -1; else if (start < 0) start = 0; search: for (i = start; i < max; i++) { if (b[i] == s[0]) { // 找到了s的第一个元素后比较剩余部分是否相等 int k = 1; while (k < s.length) { if (b[k + i] != s[k]) continue search; k++; } return i; } } return -1; } /** * 在一个字节数组中提取一个字节数组 */ private static byte[] subBytes(byte[] b, int from, int end) { byte[] result = new byte[end - from]; System.arraycopy(b, from, result, 0, end - from); return result; } /** * 在一个字节数组中提取一个字符串 */ private static String subBytesToString(byte[] b, int from, int end) { return new String(subBytes(b, from, end)); } public static byte[] intToBytes2(int num) { byte[] result = new byte[4]; result[0] = (byte) (num >>> 24);// 取最高8位放到0下标 result[1] = (byte) (num >>> 16);// 取次高8为放到1下标 result[2] = (byte) (num >>> 8); // 取次低8位放到2下标 result[3] = (byte) (num); // 取最低8位放到3下标 return result; } }
其中有大量的读取 request 内容的语句,这部分涉及到了 http 协议的相关内容,具体请参考这里 http://blog.csdn.net/yethyeth/article/details/1765925(在这里说一句抱歉,我无耻的把文章里的代码粘下来了,希望原作者不要介意)
当我把代码改到这个地步的时候,我在本地的 sae 环境上调试通过了。上传的图片文件已经能够顺利的保存在 sae storage 里面,然后我就欢欣鼓舞的把代码打包发到了 sae 服务器上,可是在线上环境上传图片还是不成功,firebug 控制台返回了一个错误语句。经过我的大量实验(绝对大量,量大的我想吐),分析得出应该是上传图片的同时还会发一个请求去获取 config.json 文件的内容,但是不知道为什么这个文件不能被读取出来(我到现在也不知道为什么,如果你知道,请告诉我)。所以我果断的把“config”这个 action 也拦截自己处理了。
加载 config.json 文件
首先是修改 controller.jsp 文件
String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()+ path + "/"; if("uploadimage".equals(action)){ out.println(UploadToStorage.upload(request,b)); } else if("config".equals(action)){ //把加载配置文件的这个分支也拦截自己处理 response.getWriter().println(UploadToStorage.readerUeditorConfig(basePath)); } else out.println(result1);
然后是 UploadToStorage 类里面的方法:
/** * @category 读取 config.json 文件 * @param basePath 根目录路径 * @return 文件内容字符串 */ public static String readerUeditorConfig(String basePath) { String result = ""; try { URL url = new URL(basePath + "ueditor/jsp/config.json"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setDoInput(true); conn.setDoInput(true); conn.setRequestProperty("Content-Type", "text/html"); conn.connect(); InputStream in = conn.getInputStream(); ByteArrayOutputStream baOut = new ByteArrayOutputStream(); int n; while ((n = in.read()) != -1) { baOut.write(n); } result = new String(baOut.toByteArray(), "utf-8"); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return result; }
顺利的把配置文件读取出来之后,果然豪不意外的可以上传图片了。
后记
感谢百度开源团队 UEditor,这个世界因为伟大的开源作者而变得越发美丽,希望这个世界能够对得起你们的付出。
感谢两位技术博客作者,再次列出他们的博客文章
关于 request 的 InputStream 读取次数问题: http://www.tuicool.com/articles/rEreEb
关于通过 request 的内容获取文件: http://blog.csdn.net/yethyeth/article/details/1765925
如果你照着以上内容写了可是依然没有能够实现上传图片,可以通过我的微博联系我 http://weibo.com/treagzhao