论坛首页 Web前端技术论坛

让Ajax框架Buffalo支持JSON协议而非仅仅自定义的XML协议[JS前端及单元测试完成]

浏览 7926 次
精华帖 (2) :: 良好帖 (5) :: 新手帖 (0) :: 隐藏帖 (15)
作者 正文
   发表时间:2010-01-19  
老实说,也不是没在JSON的表达样式上思考过,我在写测试用例时就推翻了不下两个版本

我现在初步的考虑是:先完成一个解析JSON的版本的解析器,通过不同call请求对不同协议的需求,无缝的实现XML和JSON的输入-》解析-》服务寻址-》调用-》解析-》输出的同样过程。

也就是说,我要求自己写的JSON实现,在相同的测试用例下要一摸一样,同时都能通过

所以在这个角度上说,可能不能发挥JSON的“精炼”的特点,当然这可能更多的针对输入,输出上应该对JSON会更友好些。


但无论如何,对用户都是友好的,透明的
0 请登录后投票
   发表时间:2010-01-21  

我想到了’[‘字符。

经过努力,以下测试用例通过了:

JSON

private String callWithMap = "[{'buffalo-call':''},{'method':'sum'}," +
	                                "{'int':1}," +
			"{'map':[{'type':java.util.HashMap},{'string':'key1'},{'string':'value1'},{'string':'key2'},{'string':'value2'}]}," +
			"{'string':test}" +
			"]";

测试用例代码:

	// callWithMap - PASS
	public void testShoudReadNested() throws Exception {
		StringReader reader = new StringReader(callWithMap);
		StreamReader streamReader = new JsonStreamReader(reader);
		assertEquals("buffalo-call", streamReader.getNodeName());
		streamReader.moveDown();
		assertEquals("method", streamReader.getNodeName());
		assertEquals("sum", streamReader.getValue());
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("int", streamReader.getNodeName());
		assertEquals("1", streamReader.getValue());
		
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("map", streamReader.getNodeName());
		streamReader.moveDown();
		assertEquals("type", streamReader.getNodeName());
		assertEquals("java.util.HashMap", streamReader.getValue());
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("string", streamReader.getNodeName());
		assertEquals("key1", streamReader.getValue());		
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("string", streamReader.getNodeName());
		assertEquals("value1", streamReader.getValue());
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("string", streamReader.getNodeName());
		assertEquals("key2", streamReader.getValue());		
		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("string", streamReader.getNodeName());
		assertEquals("value2", streamReader.getValue());
		streamReader.moveUp();

		streamReader.moveUp();
		streamReader.moveDown();
		assertEquals("string", streamReader.getNodeName());
		assertEquals("test", streamReader.getValue());
		
		streamReader.moveUp();
	}

 

到了测试hasMoreChildren的时候,正如JsonStandardStreamReader编写过程中所遇到的困难一样,困难又来了。

如何处理好层级关系,理顺’[‘’]’字符的关系是重点。

重新审视了JSON表达式,决定再一次改版,如下:

simpleCall

[{'buffalo-call':[{'method':'divide'},{'double':1},{'double':2}]]

 

callWithMap

[{'buffalo-call':[{'method':'sum'},{'int':1},{'map':[{'type':java.util.HashMap},{'string':'key1'},{'string':'value1'},{'string':'key2'},{'string':'value2'}]},{'string':test}]]

 

hasMoreChildren

[{'buffalo-call':[{'method':'sum'},{'int':1},{'map':[{'type':java.util.HashMap},{'string':'key1'},{'string':'value1'},{'string':'list1'},{'list':[{'type':'java.util.ArrayList'},{'length':3},{'string':a},{'int':2},{'double':8.88}]},]}]]

 

简洁多了吧,主要还是考虑JSON的层级关系,通过层级开始字符’[‘和层级关闭字符’]’,应该可以更好的处理hasMoreChildren接口。

还好,这样改动后对simpleCallcallWithMap测试用例没有任何影响,PASS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0 请登录后投票
   发表时间:2010-01-21   最后修改:2010-01-21

经过两天的努力,终于攻克了各种难关,成功实现了所有测试用例的验证,核心方法如下:

moveDown

	public void moveDown() {		
		int tag = parseTag();
		switch(tag) {
		case TAG_METHOD:
		case TAG_INT:
		case TAG_LENGTH:
		case TAG_TYPE:
		case TAG_STRING:
		case TAG_BOOLEAN:
		case TAG_LONG:
		case TAG_DATE:
		case TAG_DOUBLE:
		case TAG_NULL:
		case TAG_REF:
		case TAG_LIST:
		case TAG_MAP:
		case TAG_CALL: 
			currentTag = new Tag(tag, parseString());
			stack.push(currentTag);
			depth++;
			break;
			
		default: throw new ProtocolException("unexpected tag: " + tagName(tag));
		}
	}

 

moveUp:

	public void moveUp() {
		int ch = skipWhitespace();		
		if (ch == '}'){
			depth--;
			stack.pop();
			currentTag = (Tag) stack.get(depth-1);
		}else if(ch == ']'){
			depth--;
			stack.pop();
			currentTag = (Tag) stack.get(depth-1);
			ch = skipWhitespace();
			if (ch != '}') throw expectedChar("'}'", ch);
		}else throw expectedChar("'}' or ']'", ch);	
	}

 

 

parseTag:

	protected int parseTag() {
		int ch = skipWhitespace();
		
		if(ch == '[') {
			ch = skipWhitespace();
		}else if (ch == ',') {
			ch = skipWhitespace();
		}else throw expectedChar("'[' or ','", ch);
				
		int endChar;
		if (ch != '{') throw expectedChar("'{'", ch);
		ch = read();
		if (ch != '\'' && ch != '"') throw expectedChar("'\'' or '\"'", ch);
		endChar = ch; 
		ch = read();
		
		if (!isTagChar(ch)) throw expectedChar("tag", ch); 
		sbuf.setLength(0);
		for (; isTagChar(ch); ch = read()) sbuf.append((char) ch); 
		if (ch != endChar) throw expectedChar("'"+endChar+"'", ch); 
		Integer value = (Integer) tagCache.get(sbuf.toString());
		if (value == null) throw new ProtocolException("Unknown tag '" + sbuf + "'");

		return value.intValue();
	}

 

 

parseString:

	protected String parseString() {
		sbuf.setLength(0);
		int ch = readChar();
		if (ch != ':') throw expectedChar("':'", ch);
		int endChar = -1;
		ch = readChar();
		if (ch == '\'' || ch == '"'){ 
			endChar = ch;
			ch = readChar();
		}else if (ch == '[') {
			peekChar = ch;
			return "[";
		}
		if(ch == endChar) return "";
		sbuf.append((char) ch);
		while ((ch = readChar()) >= 0) sbuf.append((char) ch); 
		if(endChar>0 && sbuf.charAt(sbuf.length()-1) == endChar){
			return sbuf.substring(0, sbuf.length()-1);
		}else{
			return sbuf.toString();
		}
	}

 

 

 测试用例

所提供的测试用例包括标准的buffalo-call请求,以及其他通用的JSON格式,均能有效解析。

所提供的测试用例清单如下:

测试用例

说明

testSimpleCall

简单的buffalo-call

testCallWithMap

map的相对复杂的buffalo-call

testHasMoreChildren

复杂的用以测试hasMoreChildren方法的buffalo-call

testComplexTest

最复杂的混杂map/list/primitivebuffalo-call

testPrimitive

buffalo-call的标准的primitive格式的JSON

testNull

buffalo-call的标准的null格式的JSON

testList

buffalo-call的标准的list格式的JSON

testChineseChar

buffalo-call的标准的带中文字符的JSON

 

 

 

 

 

 

 

 

 

 

 

 

 

抱怨一下:javaeye的编辑器一旦带有代码,往往需要反复编辑多次才能成功,否则会被莫名其妙的截掉了,我不得不把所有注释都干掉了——也该改进了!

0 请登录后投票
   发表时间:2010-01-21   最后修改:2010-01-21

net.buffalo.protocal.io.json.JsonStreamWriter

接下来就干这个了。

这个相对而言容易些,加上写JsonStreamWriter的经验,自然顺当些了。

原先提供的测试用例明显不足,也顺便完善了这块。

总体而言,花了个把小时开发和调试,整体就比较完善了,测试用例的思考和编写,反倒花费了更多的心思。

  核心方法

startNode

    public void startNode(String name) {     

       if(isStart){

           writer.write("[");

           stack.push("[");

       }else if(depth>0){

           writer.write(",");

       }

       writer.write("{'");

       writer.write(name);

       writer.write("':");

      

       stack.push("{");

       depth++;

       isStart = true;

    }

endNode

    public void endNode() {

       depth--;

       String beginFlag = (String) stack.pop();

      

       if(beginFlag.equals("[")) write("]");

       if(beginFlag.equals("{")) write("}");

      

       if(depth == 0) {

           while(stack.hasStuff()){

              beginFlag = (String) stack.pop();

              if(beginFlag.equals("[")) write("]");

              if(beginFlag.equals("{")) write("}");

           }         

           flush();

       }

       isStart = false;

    }

 

测试用例

提供更为丰富的测试用例:

测试用例

说明

testPrimitive

测试"boolean","date","double","double","long","string","null","node"等基本对象

testNested

测试存在基本嵌套的对象

testList

测试列表对象

testMap

测试map对象

testfault

测试异常对象

testComplex

测试最为复杂的多重嵌套对象。List->Map->List

 

 

0 请登录后投票
   发表时间:2010-01-21  
为了发布上面的这个回复,我老老实实的提交-》不行(都是缺胳膊少腿的)-》编辑了足足5遍!!

Robbin TX啊,这么牛气哄哄(回帖还需要考试)的网站,可不能被这个破编辑器给砸了呀
0 请登录后投票
   发表时间:2010-02-07  

服务端的重构

 

考虑到在纳入SVN前完成重构,挤出点时间完成了重构。

所谓的重构,其实很简单,就是新增了两个抽象类方法,采用Template模式进行了抽象与具体的分离。

1、  AbstractStreamReader.java

2、  AbstractStreamWriter.java

这样,无论是JSON还是XMLIO实现,都变成了很简单,也更聚焦了。

类图如下:



 

 

OK,很简单的套路,但显然,具体协议实现代码变得非常的简单,可读性大大增加了。以后再出现什么阿猫阿狗协议,只要好的,你都可以遵照实现,难度大大降低咯。

  • 大小: 30.9 KB
  • 大小: 21.8 KB
0 请登录后投票
   发表时间:2010-02-08  

 前端JS

其实服务端的重构是前段时间的事,也放到了SVN的分支(buffalo/branches/json)上了

近期杂事太多,耽搁了一下,但终于还是回到了前端。

说在开发前端前的话

老实说,我的前端架构、设计和开发能力要打折扣,但还是要硬着头皮上。

同样前端还是采用了TDD,基于script.aculo.us所提供的unittest.js做,还真的不错,不过实话说,咱还没干过js TDD,顺道学一把。

重点说说目标:

1、  平滑的实现兼容JSON协议的客户端;

2、  重构客户端脚本。

 

 

开发的重点

要说重点,主要还是一去一回:协议的封装和返回的解析。

 

call.js

call我的思路是:对外提供一致的接口,用类似于Factory模式的机制来做具体实现的生产和封装——jsOOP其实也是不错的,但我不是太会用,将就着先出一个beta版吧。

 

 

Buffalo.Call = Class.create();
Buffalo.Call.prototype = {
	initialize: function(methodname,protocol){
		this.method = methodname;
		this.params = [];
		this.protocol = (protocol!=null && (protocol=='XML' || protocol=='JSON'))
			? protocol 
			: Buffalo.DefaultProtocol;
	},

	addParameter: function(data){
		if (typeof(data) == 'undefined') return;
		this.params[this.params.length] = data;
	},
	
	toCall: function(){
		return (this.protocol == 'JSON')
			? this._json()
			: this._xml();
	},

	_xml: function(){
		var s = "<buffalo-call>\n";
		s += "<method>" + this.method+ "</method>\n";
		for (var i = 0; i < this.params.length; i++) {
		  var data = this.params[i];
		  var p = new Buffalo.XML(this, data);
		  s += p.parseValue(data) + "\n";
		}
		s += "</buffalo-call>";
		return s; 
	},

	_json: function(){
		//demo:[{'buffalo-call':[{'method':'divide'},{'double':1},{'double':2}]]
		var s = "[{'buffalo-call':[\n";
		s += "{'method':'" + this.method+ "'}\n";
		for (var i = 0; i < this.params.length; i++) {
		  var data = this.params[i];
		  var p = new Buffalo.JSON(this, data);
		  s += p.parseValue(data) + "\n";
		}
		s += "]";
		return s; 
	},

	getProtocol : function() {
		return this.protocol;
	}
}

 

把原先在这里面的一些方法一分为二:一部分变成了具体的XML实现,一部分新建了一个UtilsClass。这些就略了,有兴趣自己down下来看。

 

json.js

 

下面是json.js的实现:

/** JSON Implementation */
Buffalo.JSON = Class.create(); 
Buffalo.JSON.prototype = {
	initialize : function(call, data) {
		this.call = call;
		this.data = data;
	    this._objects = [];
	},

	parseValue : function(data) {
		var s;
		var type = Buffalo.Utils.typeOfData(data);
		switch (type) {
		case "date":
			s = this.doDate(data);
			break;
		case "list":
			s = this.doArray(data);
			break;
		case "map":
			s = this.doStruct(data);
			break;
		case "boolean":
			s = this.doBoolean(data);
			break;
		case "null":
			s = this.doNull();
			break;
		default:
			s = this.doValue(type, data);
			break;
		}
		return s;
	},

	doDate : function(data) {
		var s = "{'date':'";
		s += Buffalo.Utils.dateToISO8609(data);
		s += "'}";
		return "," + s;
	},

	doValue : function(type, data) {
		var s;
		if (typeof (data) == "string") {
			data = data.replace(/&/g, "&amp;");
			data = data.replace(/</g, "&lt;");
			data = data.replace(/>/g, "&gt;");
			data = data.replace(/'/g, "/'");
		}
		s = "{'" + type + "':'" + data + "'}";

		return "," + s;
	},

	doBoolean : function(data) {
		var value = (data == true) ? 1 : 0;
		var s = "{'boolean':" + value + "}";
		return "," + s;
	},

	doArray : function(data) {
		var ref = this._checkRef(data);
		if (ref != -1) return "{'ref':" + ref + "}";
		this._objects[this._objects.length] = data;

		var s = "{'list':[\n";
		var boClass = data[Buffalo.BOCLASS];
		var boType = boClass ? boClass : Buffalo.Utils.typeOfArray(data);
		s += "{'type':'" + boType + "'}\n";
		s += ",{'length':" + data.length + "}\n";
		for ( var i = 0; i < data.length; i++) {
			s += this.parseValue(data[i]) + "\n";
		}
		s += "]}\n";
		return "," + s;
	},

	doStruct : function(data) {
		var ref = this._checkRef(data);
		if (ref != -1)
			return "{'ref':" + ref + "}";
		this._objects[this._objects.length] = data;

		var boType = data[Buffalo.BOCLASS] || "java.util.HashMap";
		var s = "{'map':[\n";
		s += "{'type':'" + boType + "'}\n";

		for ( var i in data) {
			if (data[i] != boType) {
				if (typeof (data[i]) == "function")
					continue; /* the function shouldn't transfered. */
				s += this.parseValue(i) + "\n";
				s += this.parseValue(data[i]) + "\n";
			}
		}
		s += "]}\n";
		return "," + s;
	},

	doNull : function() {
		return "," + "{'null':null}";
	},
	
	


	_checkRef : function(obj) {
		var ref = -1;
		for ( var i = 0; i < this._objects.length; i++) {
			if (obj === this._objects[i]) {
				ref = i;
				break;
			}
		}
		return ref;
	}
};

 

 

Buffalo.Reply = Class.create();
Buffalo.Reply.prototype = {
	initialize: function(xhr,protocol) {		
		this._isFault = false;
		this._type = "null";
		this._source = xhr.responseText;
		this._impl = (protocol == 'JSON')
			? new Buffalo.Reply.JSON(xhr, this._source) 
			: new Buffalo.Reply.XML(xhr, this._source);
	},
	
	/** interface : getType, get the response object type */
	getType: function() { return this._impl.getType(); },

	/** interface : getResult, deserialize the response and return the javascript object */
	getResult : function() { return this._impl.getResult(); },

	/** interface : isFault, check response is fault or not */
	isFault : function() { return this._impl.isFault(); },	

	/** interface : isFault, check response is null or not */
	isNull: function() { return this._impl.isNull(); },	

	getSource : function() { return this._source; },
}

 

然后分别实现。后面的代码很多,就不贴了。

说实话,jsOOD/OOP也蛮有意思的,但一时半会儿还不顺手。

 

0 请登录后投票
   发表时间:2010-03-10  

Bug-fixed:net.buffalo.protocal.BuffaloProtocal.java

 

原先也提到取得BuffaloProtocal单例的时候会产生线程安全问题,其实能看的出来,为了慎重起见,还特意写了测试代码验证并得到确认。

 

最后进行了修复:

 

……
private static Map instanceMap = new HashMap();

……

public static BuffaloProtocal getInstance(String p){
	BuffaloProtocal instance = (BuffaloProtocal) instanceMap.get(p);
	if (instance == null) {
		instance = new BuffaloProtocal();
		instance.protocol = p;
		instanceMap.put(p, instance);
	}
	return instance;
}

 

 

其实也很简单,用了一个Map,对XML和JSON各放了一个实例。

 

0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics