百度 UEditor 1.43 JAVA 版本 在 SAE 服务器上传文件的问题_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 百度 UEditor 1.43 JAVA 版本 在 SAE 服务器上传文件的问题

百度 UEditor 1.43 JAVA 版本 在 SAE 服务器上传文件的问题

 2014/9/19 0:58:45  treagzhao  程序员俱乐部  我要评论(0)
  • 摘要:这几天我做的一个项目,要用到富文本编辑器,同时还要能在文本中插入图片。国内做的最成熟的富文本编辑器是百度的UEditor,然而UEditor上传文件的功能是封装好的,开发者只要在配置文件内写一个WebRoot下的目录就可以自动上传,而SAE的服务WebRoot下的文件读写权限并没有开放,SAE是通过另一个叫做SAEStorage的服务来实现保存文件的,所以UEditor自带的上传图片功能在SAE上完全无法使用。我被这个矛盾坑的要死要活,又一时半会儿找不到别的合适的富文本编辑器
  • 标签:上传 文件 Java 问题 百度 服务器 服务 上传文件 sae 版本
  这几天我做的一个项目,要用到富文本编辑器,同时还要能在文本中插入图片。国内做的最成熟的富文本编辑器是百度的 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
  • 查看图片附件
发表评论
用户名: 匿名