jsonengineをJSONじゃなくてAMFで使ってみた

jsonengineを使えばサーバーサイドのコーディング不要でGoogle App Engineをデータストアとして利用できるけど、
JSONなのでJavaScriptからだと使いやすいけど、ActionScriptだとちょい使いにくい。ような気がする。
ASでもJSON扱うライブラリはあるけど、Classへのマッピングとか面倒くさいし
(なんかリフレクションつかって自動マッピングしてくれる便利なライブラリあるような気がするけど)

んで、

S3BlazeDSを経由してjsonengineを使えば、ActionScriptからめんどくさい事せずにjsonengine使えるんじゃね?
って思って超適当に作ってみた。

これをつかえば、

get,update,delete,queryを、JSONに変換したり、HTTPリクエストしたりとかせずに、RemoteObject呼び出しできる。
ということは、通信はJSONじゃなくて、AMFになります。(jsonengineなのにAMFです…)
通信の効率ちょい良くなるかな。


あと、ASの型をjsonengineのdocTypeとして使用して、勝手に変換してくれるので便利?です。
権限他、例外処理等はException投げるので、FaultEventで処理ができます。

ソース

とりあえず、適当なので、使うと不具合があると思うので自分で直してください。


S3BlazeDSJEService.java(jsonengineのFrontControllerを元にして作ってます)
これをRemoteObjectとしてBlazeDSに登録します。

public class S3BlazeDSJEService {
    private static final Pattern condPattern = Pattern
        .compile("^([^\\.]*)\\.(eq|gt|ge|lt|le)\\.(.*)$");

    private static final Pattern quotePattern = Pattern
        .compile("^[\"'](.*)[\"']$");

    private Object convertPropValue(final String propValue) {

        // if it's quoted, treat it as a String
        final Matcher m = quotePattern.matcher(propValue);
        if (m.find()) {
            return m.group(1);
        }

        // if it's not quoted, try to parse it as a BigDecimal
        try {
            return new BigDecimal(propValue);
        } catch (NumberFormatException e) {
            // failed
        }

        // try to parse as a Boolean
        if ("true".equals(propValue)) {
            return true;
        } else if ("false".equals(propValue)) {
            return false;
        }

        // otherwise, treat it as a String
        return propValue;
    }

    private CRUDRequest createCRUDRequest(ASObject object,
            Long checkUpdatesAfter) throws UnsupportedEncodingException {

        // parse JSON doc
        final CRUDRequest jeReq = new CRUDRequest(object);

        // init the request
        initJERequest(jeReq, object.getType());

        // decode docId
        if (!StringUtil.isEmpty((String) object.get("_docId"))) {
            jeReq.setDocId((String) object.get("_docId"));
        }

        // set checkConflict flag
        try {
            jeReq.setCheckUpdatesAfter(checkUpdatesAfter);
        } catch (Exception e) {
            // NPE or NumberFormatException
        }
        return jeReq;
    }

    private void initJERequest(JERequest jeReq, String docType) {

        // decode docType
        if (StringUtil.isEmpty(docType)) {
            throw new IllegalArgumentException("No docType found");
        }
        jeReq.setDocType(docType);

        // set timestamp
        jeReq.setRequestedAt((new JEUtils()).getGlobalTimestamp());

        // set Google account info
        if (JEUserUtils.isLoggedIn()) {
            jeReq.setRequestedBy(JEUserUtils.userEmail());
            jeReq.setAdmin(JEUserUtils.isAdmin());
        }

        // set display name
        String displayName = JEUserUtils.getDisplayName();
        jeReq.setDisplayName(displayName);
    }

    private void parseCondFilter(final QueryRequest qReq, final String cond) {

        // try to parse the cond params
        final Matcher m = condPattern.matcher(cond);
        if (!m.find()) {
            throw new IllegalArgumentException("Illegal condFilter: " + cond);
        }
        final String propName = m.group(1);
        final String condToken = m.group(2);
        final String propValue = m.group(3);

        // try to convert propValue
        final Object propValueObj = convertPropValue(propValue);

        // if propName ends with "_", extract terms from propValue;
        if (propName.endsWith("_")) {
            final Set<String> values = (new JEUtils()).extractTerms(propValue);
            for (String value : values) {
                QueryFilter.addCondFilter(qReq, propName, condToken, value);
            }
        } else {
            QueryFilter.addCondFilter(qReq, propName, condToken, propValueObj);
        }
    }

    private void parseLimitFilter(final QueryRequest qReq,
            final String limitParam) {
        final int limit = Integer.parseInt(limitParam);
        QueryFilter.addLimitFilter(qReq, limit);
    }

    private void parseSortFilter(final QueryRequest qReq, final String sortParam) {
        final String[] sortTokens = sortParam.split("\\.");
        final String propName = sortTokens[0];
        final String sortOrder = sortTokens[1];
        QueryFilter.addSortFilter(qReq, propName, sortOrder);
    }

    public void doDelete(ASObject object, Long checkUpdatesAfter)
            throws IOException, JEAccessDeniedException, JENotFoundException,
            JEConflictException, JERedirectException {

        // do delete
        final CRUDRequest jeReq = createCRUDRequest(object, checkUpdatesAfter);

        try {
            (new CRUDService()).delete(jeReq);
        } catch (JEAccessDeniedException e) {
            if (JEUserUtils.isLoggedIn() == false) {
                jsonRedirectToLogin();
            }
            throw e;
        }
    }
    
   public ASObject doUpdate(ASObject object, Long checkUpdatesAfter,
            boolean isUpdateOnly) throws IOException, JEConflictException,
            JENotFoundException, JERedirectException, JEAccessDeniedException {

        // do put
        final CRUDRequest jeReq = createCRUDRequest(object, checkUpdatesAfter);
        final String resultJson;
        try {
            resultJson = (new CRUDService()).put(jeReq, isUpdateOnly);
        } catch (JEAccessDeniedException e) {
            if (JEUserUtils.isLoggedIn() == false) {
                jsonRedirectToLogin();
            }
            if (jeReq.getDisplayName() == null) {
                jsonRedirectToDisplayName();
            }
            throw e;
        }

        ASObject decode = JSON.decode(resultJson, ASObject.class);
        decode.setType((String) object.getType());

        // return the result
        return decode;
    }

    public ASObject doGet(ASObject object) throws IOException,
            JENotFoundException, JEConflictException, JERedirectException,
            JEAccessDeniedException {

        // do get
        final CRUDRequest jeReq = createCRUDRequest(object, null);
        final String resultJson;
        try {
            resultJson = (new CRUDService()).get(jeReq);
        } catch (JEAccessDeniedException e) {
            if (JEUserUtils.isLoggedIn() == false) {
                jsonRedirectToLogin();
            }
            throw e;
        }

        ASObject decode = JSON.decode(resultJson, ASObject.class);
        decode.setType(object.getType());

        // return the result
        return decode;
    }

    public ASObject[] doQuery(String docType, String[] conds, String sortParam,
            String limitParam) throws IOException, JERedirectException,
            JEAccessDeniedException {

        // add QueryFilters for "cond" parameters
        final QueryRequest qReq = new QueryRequest();
        initJERequest(qReq, docType);

        // add QueryFilters for "cond"
        if (conds != null) {
            for (String cond : conds) {
                parseCondFilter(qReq, cond);
            }
        }

        // add QueryFilters for "sort"
        if (sortParam != null) {
            parseSortFilter(qReq, sortParam);
        }

        // add QueryFilters for "limit"
        if (limitParam != null) {
            parseLimitFilter(qReq, limitParam);
        }

        // execute the query
        final String resultJson;
        try {
            resultJson = (new QueryService()).query(qReq);
        } catch (JEAccessDeniedException e) {
            if (JEUserUtils.isLoggedIn() == false) {
                jsonRedirectToLogin();
            }
            throw e;
        }

        ASObject[] decodes = JSON.decode(resultJson, ASObject[].class);
        for (ASObject decode : decodes) {
            decode.setType(docType);
        }

        return decodes;
    }

    protected void jsonRedirectToLogin() throws JERedirectException {
        final String redirectURL =
            JEUserUtils.getLoginURL("/user/index").toString();
        jsonRedirect(redirectURL);
    }

    protected void jsonRedirectToDisplayName() throws JERedirectException {
        final String redirectURL =
            JEUtils.getRequestServer() + "/user/displayName";
        jsonRedirect(redirectURL);
    }

    protected void jsonRedirect(String redirectURL) throws JERedirectException {
        throw new JERedirectException(redirectURL);
    }
}


Flex
超テキトーですが、こんな感じで使います。

	<fx:Script>
		<![CDATA[
			//Get実行
			private function jeGet():void{
				var s:Sample1  = new Sample1();
				s._docId = docId.text;
				
				var ro:RemoteObject = new RemoteObject("pingService");
				ro.addEventListener(ResultEvent.RESULT, function(e:ResultEvent):void {
					var rs:Sample1 = e.result as Sample1;
					Alert.show(rs.messageBody_);
				});
				ro.addEventListener(FaultEvent.FAULT, function(e:FaultEvent):void {
					Alert.show(e.toString());
				});
				ro.getOperation("doGet").send(s);
				
			}
			//Update実行
			private function jeUpdate():void{
				var s:Sample1;
				
				if(grid.selectedItem != null){
					s = grid.selectedItem as Sample1;
				} else{
					s = new Sample1();
				}
				s.messageBody_ = tiMsg.text;
				
				var ro:RemoteObject = new RemoteObject("pingService");
				ro.addEventListener(ResultEvent.RESULT, function(e:ResultEvent):void {
					var rs:Sample1 = e.result as Sample1;
					Alert.show(rs.messageBody_);
				});
				ro.addEventListener(FaultEvent.FAULT, function(e:FaultEvent):void {
					Alert.show(e.toString());
				});
				ro.getOperation("doUpdate").send(s, null, false);
				
			}
			//Delete実行
			private function jeDelete():void{
				var s:Sample1;
				
				if(grid.selectedItem != null){
					s = grid.selectedItem as Sample1;
				} else{
					s = new Sample1();
				}
				s._docId = docId.text;
				
				var ro:RemoteObject = new RemoteObject("pingService");
				ro.addEventListener(ResultEvent.RESULT, function(e:ResultEvent):void {
					Alert.show("ok.");
				});
				ro.addEventListener(FaultEvent.FAULT, function(e:FaultEvent):void {
					Alert.show(e.toString());
				});
				ro.getOperation("doDelete").send(s, null);
				
			}
			//Query実行
			private function jeQuery():void{
				var ro:RemoteObject = new RemoteObject("pingService");
				ro.addEventListener(ResultEvent.RESULT, function(e:ResultEvent):void {
					gridData = e.result as Array;
				});
				ro.addEventListener(FaultEvent.FAULT, function(e:FaultEvent):void {
					Alert.show(e.toString());
				});
				ro.getOperation("doQuery").send(">slim3.model.Sample1", null, null, null);
				
			}

			private function gridChangeEvent(e:ListEvent):void{
				if(grid.selectedItem != null){
					tiMsg.text = grid.selectedItem.messageBody_;
					docId.text = grid.selectedItem._docId;
				}
			}
		]]>
	</fx:Script>

	<fx:Declarations>
		<fx:Array id="gridData"></fx:Array>
	</fx:Declarations>

	<mx:VBox>
		<s:TextInput id="tiMsg" text="Hello!"/>

                <!-- jsonengineのdocId -->
		<s:TextInput id="docId" text=""/>
		
		<mx:HBox>
			<s:Button label="get" click="jeGet()"/>
			<s:Button label="update" click="jeUpdate()"/>
			<s:Button label="delete" click="jeDelete()"/>
			<s:Button label="query" click="jeQuery()"/>
		</mx:HBox>

                <!-- クエリの結果を表示するgrid -->
		<mx:DataGrid id="grid" dataProvider="{gridData}" change="gridChangeEvent(event)">
			<mx:columns>
				<mx:DataGridColumn headerText="docId" dataField="_docId"/>
				<mx:DataGridColumn headerText="messageBody_" dataField="messageBody_"/>
				<mx:DataGridColumn headerText="createdAt" dataField="_createdAt"/>
				<mx:DataGridColumn headerText="updatedAt" dataField="_updatedAt"/>
				<mx:DataGridColumn headerText="displayName" dataField="_displayName"/>
			</mx:columns>
		</mx:DataGrid>
		
	</mx:VBox>


メタタグでRemoteClassに指定しておけば、Class名がdocTypeとして使用されます。
Sample1.as

package slim3.model
{
	import mx.rpc.remoting.RemoteObject;

	[RemoteClass]
	public class Sample1
	{
		public function Sample1()
		{
		}
		
		public var _docId:String;
		public var _createdAt:Number;
		public var _updatedAt:Number;
		public var _displayName:String;
		public var messageBody_:String;
	}
}