这几天我做的一个项目,要用到富文本编辑器,同时还要能在文本中插入图片。国内做的最成熟的富文本编辑器是百度的 UEditor,然而 UEditor 上传文件的功能是封装好的,开发者只要在配置文件内写一个 WebRoot 下的目录就可以自动上传,而 SAE 的服务 WebRoot 下的文件读写权限并没有开放,SAE 是通过另一个叫做 SAE Storage 的服务来实现保存文件的,所以 UEditor 自带的上传图片功能在 SAE 上完全无法使用。
我被这个矛盾坑的要死要活,又一时半会儿找不到别的合适的富文本编辑器,所以只好把 UEditor 内部封装的代码改了一些些来实现这个功能,以下是正文。
Ueditor JAVA版上传图片工作原理
UEditor 与后台的全部交互,都是通过 ueditor/jsp下的一个叫做
controller.jsp 来分派的。
每一次ueditor 与后台交互的请求,都会有一个参数 action来标记这次请求究竟是什么动作。其中上传图片的请求涉及到两个 action,分别是“config”和“uploadimage”,“config”是读取 ueditor 的配置文件,就是同一个目录下的 config.json 文件;“uploadimage”就是处理上传图片的 action。
controller.jsp 只有两句话,见图
class="java">
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
- 大小: 29.6 KB