/* AuUtility.java
Purpose:
Description:
History:
2012/3/22 Created by dennis
Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/
package org.zkoss.zats.mimic.impl.au;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.ast.AstNode;
import org.mozilla.javascript.ast.AstRoot;
import org.mozilla.javascript.ast.FunctionCall;
import org.mozilla.javascript.ast.FunctionNode;
import org.mozilla.javascript.ast.Name;
import org.mozilla.javascript.ast.NodeVisitor;
import org.zkoss.zats.ZatsException;
import org.zkoss.zats.common.json.JSONArray;
import org.zkoss.zats.common.json.JSONValue;
import org.zkoss.zats.mimic.AgentException;
import org.zkoss.zats.mimic.ComponentAgent;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zsoup.Zsoup;
import org.zkoss.zsoup.nodes.Document;
import org.zkoss.zsoup.nodes.Element;
import org.zkoss.zsoup.select.Elements;
/**
* A utility for AU.
*
* @author dennis
* @author jumperchen
*/
public class AuUtility {
/**
* lookup the event target of a component, it look up the component and its
* ancient. use this for search the actual target what will receive a event
* for a action on a component-agent
* <p/>
* Currently, i get it by server side directly
*/
public static ComponentAgent lookupEventTarget(ComponentAgent c, String evtname) {
if (c == null)
return null;
Component comp = (Component)c.getDelegatee();
if (Events.isListened(comp, evtname, true)) {
return c;
}
return lookupEventTarget(c.getParent(), evtname);
}
static void setEssential(Map<String,Object> data,String key, Object obj){
setEssential(data,key,obj,false);
}
static void setEssential(Map<String,Object> data,String key, Object obj, boolean nullable){
if(obj==null&&!nullable) throw new AgentException("data of "+key+" is null");
data.put(key, toSafeJsonObject(obj));
}
static void setOptional(Map<String,Object> data,String key, Object obj){
if(obj==null) return;
data.put(key, toSafeJsonObject(obj));
}
static void setReference(Map<String,Object> data,Component comp){
if(comp==null) return;
data.put("reference", comp.getUuid());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
static private Object toSafeJsonObject(Object obj){
if(obj instanceof Set){
//exception if data is Set
//>>Unexpected character (n) at position 10.
//>> at org.zkoss.json.parser.Yylex.yylex(Yylex.java:610)
//>> at org.zkoss.json.parser.JSONParser.nextToken(JSONParser.java:270)
return new ArrayList((Set)obj);
}
return obj;
}
/**
* convert JSON object of AU. response to AuResponse list.
* @param jsonObject AU. response. If null, throw null point exception.
* @return list of AuResponse if the format of object is valid, or null if otherwise.
*/
public static List<AuResponse> convertToResponses(Map<String, Object> jsonObject) {
// check argument
if (jsonObject == null)
throw new NullPointerException("input object can't be null");
Object responses = jsonObject.get("rs");
if (!(responses instanceof List))
return null;
// fetch all response
ArrayList<AuResponse> list = new ArrayList<AuResponse>();
for (Object response : (List<?>) responses) {
if (response instanceof List) {
List<?> resp = (List<?>) response;
if (resp.size() == 2) {
// create response
String cmd = resp.get(0).toString();
Object data = resp.get(1);
if (data instanceof List)
list.add(new AuResponse(cmd, ((List<?>) data).toArray()));
else
list.add(new AuResponse(cmd, data));
continue;
}
}
// format is invalid
return null;
}
return list;
}
public static Map<String, Object> parseAuResponseFromLayout(String raw) {
try {
String zkmxArgs = null;
// Bug fixed for ZATS-44
Document doc = Zsoup.parse(new ByteArrayInputStream(raw.getBytes("utf-8")), "UTF-8",
"", org.zkoss.zsoup.parser.Parser.xhtmlParser());
Elements scripts = doc.getElementsByTag("script");
for (Element script : scripts) {
// fetch arguments of zkmx()
String text = script.html().replaceAll("[\\n\\r]", "");
// ZATS-34: layout response might have other client-side scripts
// using JS parser to fetch argument of zkmx()
if (text.contains("zkmx(")) {
zkmxArgs = fetchJavascriptFuntionArguments("zkmx", text);
if(zkmxArgs != null) {
break; // find first
}
}
}
if (zkmxArgs == null) {
return null;
}
// ZATS-25: filter non-JSON part (i.e. real JS code)
String json = filterNonJSON(zkmxArgs);
// parse to JSON and wrap to map
JSONArray layoutCmds = (JSONArray) JSONValue.parseWithException(json);
if (layoutCmds.size() < 3) {
return null; // maybe no AU cmd
}
JSONArray rawAuCmds = (JSONArray) layoutCmds.get(2); // the 3rd argument are AuCmd -> mount.js -> zkmx()
JSONArray auCmds = new JSONArray();
// group up (two items > one AU cmd)
for (int i = 0; i < rawAuCmds.size(); i += 2) {
Object cmd = rawAuCmds.get(i);
Object data = rawAuCmds.get(i + 1);
// check data type
if (!(data instanceof JSONArray)) {
data = JSONValue.parseWithException(data.toString());
}
JSONArray a = new JSONArray();
a.add(cmd);
a.add(data);
auCmds.add(a);
}
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("rs", auCmds); // compatible with AU response
return map;
} catch (Exception e) {
throw new ZatsException(e.getMessage(), e);
}
}
/**
* @return an json array text contained specified function's arguments, or null otherwise.
*/
public static String fetchJavascriptFuntionArguments(final String funcName, String code) {
if (funcName == null || code == null) {
return null;
}
// parse js
Parser parser = new Parser();
AstRoot root = parser.parse(code, null, 0);
// find specified function
final AtomicReference<FunctionCall> target = new AtomicReference<FunctionCall>();
root.visit(new NodeVisitor() {
public boolean visit(AstNode node) {
if (node instanceof FunctionCall) {
FunctionCall call = (FunctionCall) node;
node = call.getTarget();
if (node instanceof Name) {
Name name = (Name) node;
if (funcName.equals(name.toSource())) {
target.set(call);
return false;
}
}
}
return true;
}
});
FunctionCall call = target.get();
if (call == null) {
return null;
}
// fetch arguments and merge to an array text
int lp = call.getLp();
int rp = call.getRp();
if (lp < 0 || rp < 0) {
return null;
}
int start = lp + call.getAbsolutePosition() + 1;
int end = rp + call.getAbsolutePosition();
return "[" + code.substring(start, end) + "]";
}
public static String filterNonJSON(String json) {
// prefix for valid js code
String prefix = "var tmp = ";
StringBuilder src = new StringBuilder(prefix).append(json);
// parse js
Parser parser = new Parser();
AstRoot root = parser.parse(src.toString(), null, 0);
// collect functions
final List<FunctionNode> functions = new ArrayList<FunctionNode>();
root.visit(new NodeVisitor() {
public boolean visit(AstNode node) {
if(node instanceof FunctionNode) {
functions.add((FunctionNode)node);
}
return true;
}
});
// sort and make sure ordered (last to first)
Collections.sort(functions);
// filter function declaration by replacing functions
for(int i = functions.size() - 1 ; i >= 0 ; --i) {
FunctionNode func = functions.get(i);
int p = func.getAbsolutePosition();
int len = func.getLength();
src.replace(p, p + len, "''"); // replace by empty js string literal
}
// remove prefix and return result
return src.substring(prefix.length());
}
}