/*
* Copyright (C) 2000-2012 InfoChamp System Corporation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.gk.engine.client;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import jfreecode.gwt.event.client.bus.EventBus;
import jfreecode.gwt.event.client.bus.EventObject;
import jfreecode.gwt.event.client.bus.EventProcessImpl;
import jfreecode.gwt.event.client.bus.JsonConvert;
import org.gk.engine.client.build.Builder;
import org.gk.engine.client.build.EngineDataStore;
import org.gk.engine.client.build.INodeProvider;
import org.gk.engine.client.build.js.XJavaScript;
import org.gk.engine.client.build.layout.XLayoutData;
import org.gk.engine.client.event.EventCenter;
import org.gk.engine.client.event.EventHandler;
import org.gk.engine.client.exception.GKEngineException;
import org.gk.engine.client.exception.LibraryNotFoundException;
import org.gk.engine.client.gen.UIGen;
import org.gk.engine.client.i18n.EngineMessages;
import org.gk.engine.client.logging.EngineLogger;
import org.gk.engine.client.utils.ComLibrary;
import org.gk.engine.client.utils.NodeUtils;
import org.gk.ui.client.com.form.gkList;
import org.gk.ui.client.com.form.gkMap;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.core.DomQuery;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.Container;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.Window;
import com.extjs.gxt.ui.client.widget.form.AdapterField;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.Node;
import com.google.gwt.xml.client.NodeList;
/**
* <title>前端GWT的GK核心</title>
*
* <pre>
* GK核心是用來解析gul語法,完成gul描述畫面的生成,
* 並提供API讓AP可控制生成畫面中的任何Widget。
* 使用方式
* 1.在GWT模組繼承gk模組
* 2.每一個GWTPage搭配一個Engine
* </pre>
*
* @author I21890
* @since 2010/07/07
*/
public class Engine implements IEngine, INodeProvider {
static {
// 當這為true時,pageToolbar image出不來 (路徑不對)
// 這boolean是在GXT.java初始化根據瀏覽器放div的效果決定的,
// 但問題出在有時是true,有時是false
/**
* <pre>
* GXT 程式碼Initializes GXT時調用
* if ("none".equals(XDOM.getComputedStyle(div,"backgroundImage"))) {
* isHighContrastMode = true;
* XDOM.getBodyEl().addStyleName("x-contrast"); }
* </pre>
*/
GXT.isHighContrastMode = false;
GXT.setAutoIdPrefix("gk");
}
private static Engine engine;
protected String id = XDOM.getUniqueId();
protected String gul = "<page>\r\n</page>";
// 目前正在處理的XML Node
protected Node preprocessNode;
// 存放目前解析好的GK元件,GK元件型別為UIGen,可產生Component或程式碼
protected List<UIGen> uiGenNodeList = new gkList<UIGen>();
// 存放render過的LayoutContainer擁有的元件
protected static Map<String, Set> renderPanelCom = new gkMap();
protected static Map<String, Set> renderPanelComBak = new gkMap();
public static Engine get() {
if (engine == null) {
engine = new Engine();
engine.build();
}
return engine;
}
public static String getVersion() {
return Version.BUILD;
}
/**
* <pre>
* 載入引擎內定可用的元件 <com></com> 啟動元件建構器,
* 傳入NodeProvider讓元件 建構器可解析Node進行畫面元件的建構
* </pre>
*/
private Engine() {
// 目前對於GKEngine例外,或其他例外的捕捉先直接透過alert顯示
GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void onUncaughtException(Throwable e) {
EngineLogger.log(e);
}
});
}
private void build() {
Builder.attach(Engine.this);
loadingLibrary();
JSMethods.hookJSMethods();
}
/**
* 取得lib檔案所在
*/
public native void loadingLibrary()/*-{
(function() {
var gkEngine = $wnd.document.getElementById('_gk_');
if (gkEngine != null && gkEngine.getAttribute('lib') != null) {
var gkEngineLib = gkEngine.getAttribute('lib');
@org.gk.engine.client.utils.ComLibrary::loadingLibrary(Ljava/lang/String;)(gkEngineLib);
} else {
@org.gk.engine.client.utils.ComLibrary::loadingLibrary(Ljava/lang/String;)("/gul/lib/component.lib");
}
})();
}-*/;
private static String getCookie(String name) {
return Cookies.getCookie(name);
}
private static void setCookie(String name, String value) {
long timestamp = new Date().getTime() + (1000 * 86400) * 14; // 2 week
Cookies.setCookie(name, value, new Date(timestamp));
}
/**
* 執行javascript指令
*
* @param js
* javascript指令
*/
private static native void executeJS(String js)/*-{
$wnd.eval(js);
}-*/;
private static native void registryWindowFocusBlur(String userId,
String midTime)/*-{
$wnd.onfocus = function() {
@org.gk.engine.client.Engine::listener(Ljava/lang/String;Ljava/lang/String;)(userId, '' + midTime);
}
$wnd.onblur = function() {
@org.gk.engine.client.Engine::listener(Ljava/lang/String;Ljava/lang/String;)(userId, '0');
}
}-*/;
/**
* 取得gk.load指定GUL
*
* <pre>
* Page時,該Page的Path如果是Web通常會使用相對路徑方式設定,
* 如果考慮行動裝置的話,應設定完整路徑
* 例如: http://www.ezoui.com/portal/index.gul
* </pre>
*
* @return String
*/
public static native String getGKPath()/*-{
return $wnd.gk.path;
}-*/;
/**
* 啟動polling機制,接收後端傳來資料
*
* @param userId
* @param midTime
*/
private static void listener(String userId, String midTime) {
if (midTime == null || midTime.equals("0")
|| (userId != null && userId.toLowerCase().equals("guest"))) {
EventBus.get(userId).disconnect();
return;
}
String gkPath = getGKPath();
if (gkPath != null && gkPath.startsWith("http://")) {
String[] urlSplit = gkPath.split("/");
if (urlSplit.length == 3) {
throw new GKEngineException("gk.listener failure! " + gkPath);
}
String host = urlSplit[0] + "//" + urlSplit[2] + "/" + urlSplit[3]
+ "/";
EventBus.get(userId).connectServer(host, Integer.parseInt(midTime));
} else {
EventBus.get(userId).connectServer(Integer.parseInt(midTime));
}
EventBus.get(userId).subscribe("javascript",
new EventProcessImpl("javascript") {
@Override
public void execute(String eventId, EventObject eo) {
try {
executeJS(eo.getInfoString());
} catch (Exception e) {
EngineLogger.console("executeJS:" + e);
}
}
});
EventBus.get(userId).subscribe("callback",
new EventProcessImpl("callback") {
@Override
public void execute(String eventId, EventObject eo) {
Map gkInfo = eo.getInfoMap();
Iterator it = gkInfo.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Object> entry = (Entry) it.next();
EventHandler.setAttributeValue(entry.getKey(),
entry.getValue());
}
}
});
}
protected static void invokeEvent(String gulAttribute, JavaScriptObject jso) {
invokeEvent("", gulAttribute, jso);
}
protected static void invokeEvent(String srcId, String gulAttribute,
JavaScriptObject jso) {
XJavaScript js = new XJavaScript(srcId, "");
EventCenter.exec(js.getId(), gulAttribute, js, new BaseEvent(jso));
}
@Override
public Component getComponent(String id) {
return getComponent().get(id);
}
@Override
public Map<String, Component> getComponent() {
return EngineDataStore.getComponentMap();
}
@Override
public void render(String gul, LayoutContainer lc) {
render(gul, lc, true);
}
@Override
public void render(String gul, LayoutContainer lc, boolean clearAll) {
if (clearAll) {
this.gul = gul;
renderPanelCom.clear();
EngineDataStore.clearAll();
bus.removeAll();
EventCenter.clear();
}
renderPanel(gul, lc);
}
public String gul() {
return gul;
}
public void renderPanel(String gul, LayoutContainer lc) {
renderPanel(gul, lc, false);
}
/**
* <pre>
* 讓引擎可以解析GUL語法後將元件放到傳入的LayoutContainer
* 在解析GUL語法過程記錄所有產生的Component id,才能在下次
* 要重新繪製時,移除既有的元件ID
* </pre>
*
* @param gul
* @param container
* @param hasRoot
*/
public void renderPanel(String gul, LayoutContainer container,
boolean hasRoot) {
// 提供使用者在Engine解析前介入處理
gul = beforeParser(gul);
gul = i18n(gul);
// 為了包含多個元件,需要的話可以用root包起來讓xml解析完,再拿掉root Tag
if (hasRoot) {
gul = new StringBuffer("<root>").append(gul).append("</root>")
.toString();
}
uiGenNodeList.clear();
// 將面板擁有的所有 component 清空
removeAllComponent(container);
// 將目前使用的Bus名稱記起來,改用引擎本身的Bus
String outsideBusName = EventBus.get().getName();
EventBus.setDefaultName(bus.getName());
// 資料備份
backup();
try {
// 將GUL文件轉成XML文件,如果有問題就拋出Exception
Document doc = parseGUL(gul);
NodeList nodeList = hasRoot ? doc.getChildNodes().item(0)
.getChildNodes() : doc.getChildNodes();
// GUL文件轉換完成,開始解析節點
long time = System.currentTimeMillis();
parserNode(uiGenNodeList, nodeList);
putRenderPanelComSet(container);
long processNodeSpendTime = System.currentTimeMillis() - time;
// 節點解析完成,開始建立元件並產生畫面
time = System.currentTimeMillis();
renderUI(container);
container.layout();
initialAllUIGen(container);
long renderUISpendTime = System.currentTimeMillis() - time;
// 列印節點解析花費時間與元件建立花費時間
printLastRenderSpendTime(processNodeSpendTime, renderUISpendTime);
// 畫面產生完成,發佈事件通知
bus.publish(new EventObject(getId() + ".afterRender"));
} catch (Throwable e) {
EngineLogger.log(e);
rollback();
} finally {
// 清除備份
clearBackup();
// 改回原Bus名稱
EventBus.setDefaultName(outsideBusName);
// 每次render結束後,插上log以觀察是否有記憶體洩漏的狀況,並檢查資料的一致性
check();
}
}
private Document parseGUL(String gul) {
gul = ComLibrary.replaceAnonymousId(gul);
return NodeUtils.parseGUL(gul);
}
private void check() {
// 每次render結束後,插上log以觀察是否有記憶體洩漏的狀況
EngineLogger.console(EventBus.getEventBusList());
EngineLogger.console("Nodes:" + EngineDataStore.getUIGenNodeMapSize());
EngineLogger.console("Components:"
+ EngineDataStore.getComponentMapSize());
EngineLogger.console("renderPanelCom:" + renderPanelCom.size());
// 檢查資料是否一致
int uiGenNodeSize = EngineDataStore.getUIGenNodeMapSize();
int rpComSize = 0;
Set key = renderPanelCom.keySet();
Iterator it = key.iterator();
while (it.hasNext()) {
String ke = it.next().toString() + "";
Set map = renderPanelCom.get(ke);
rpComSize += map.size();
}
if (uiGenNodeSize != rpComSize) {
String temp = "";
Map nodeMap = EngineDataStore.getUIGenNodeMap();
it = key.iterator();
while (it.hasNext()) {
String ke = it.next().toString() + "";
Set map = renderPanelCom.get(ke);
Iterator mapIt = map.iterator();
while (mapIt.hasNext()) {
Object obj = mapIt.next();
if (!nodeMap.containsKey(obj)) {
temp += obj + ",";
}
}
}
if (!temp.equals("")) {
EngineLogger.console("different info===\n");
EngineLogger.console(temp + "\n");
EngineLogger.console("============");
}
}
}
/**
* 備份
*/
private void backup() {
EngineDataStore.backup();
renderPanelComBak.putAll(renderPanelCom);
}
/**
* 回復
*/
private void rollback() {
EngineDataStore.rollback();
renderPanelCom.clear();
renderPanelCom.putAll(renderPanelComBak);
}
/**
* 清除備份
*/
private void clearBackup() {
EngineDataStore.clearBackup();
renderPanelComBak.clear();
}
public void removeComponent(String id) {
EngineDataStore.removeComponent(id);// 刪除 GXT 元件
EngineDataStore.removeUIGenNode(id);// 刪除 XComponent 元件
EventCenter.remove(id); // 取消元件訂閱的事件
}
/**
* @param containerId
*/
public void removeRenderPanelComponent(String containerId) {
renderPanelCom.remove(containerId);
}
private void removeAllComponent(Container layoutContainer) {
// 依gxt容器內部資料做清除動作
Iterator<Component> componentIt = layoutContainer.getItems().iterator();
while (componentIt.hasNext()) {
Component com = componentIt.next();
if (com instanceof Container) {
removeAllComponent((Container) com);
} else if (com instanceof Grid) {
removeColumnModel((Grid) com);
} else if (com instanceof AdapterField) {
removeAdaptWidget((AdapterField) com);
}
removeComponent(com.getId());
removeRenderPanelComById(com.getId());
}
// 依renderPanelCom內的資料做清除動作
removeRenderPanelCom(layoutContainer.getId());
// 將面板清空
layoutContainer.removeAll();
}
private void removeRenderPanelCom(String containerId) {
if (renderPanelCom.get(containerId) == null) {
return;
}
Iterator<String> comIt = renderPanelCom.get(containerId).iterator();
while (comIt.hasNext()) {
removeComponent(comIt.next());
}
renderPanelCom.remove(containerId);
}
private void removeRenderPanelComById(String id) {
Set key = renderPanelCom.keySet();
Iterator it = key.iterator();
while (it.hasNext()) {
String ke = it.next().toString() + "";
Set set = renderPanelCom.get(ke);
if (set.contains(id)) {
set.remove(id);
return;
}
}
}
/**
* 清掉Grid同時,要將Grid裡面的ColumnConfig元件從EngineDataStore中移除
*
* @param grid
*/
private void removeColumnModel(Grid grid) {
ColumnModel cm = grid.getColumnModel();
Iterator<ColumnConfig> columnIt = cm.getColumns().iterator();
while (columnIt.hasNext()) {
String id = columnIt.next().getId();
removeComponent(id);
removeRenderPanelComById(id);
}
// 透過DomQuery,找到grid內,自訂id_rowIndex的元件,並清除
com.google.gwt.dom.client.NodeList<Element> nodes = DomQuery.select(
"*[id*=_]", grid.getElement());
for (int i = 0; i < nodes.getLength(); i++) {
Element ele = nodes.getItem(i);
removeComponent(ele.getId());
removeRenderPanelComById(ele.getId());
}
}
/**
* 清掉adaptField同時,要將adaptField裡面的widget從EngineDataStore中移除
*
* @param field
*/
private void removeAdaptWidget(AdapterField field) {
com.google.gwt.user.client.ui.Widget widget = field.getWidget();
if (widget instanceof Container) {
removeAllComponent((Container) widget);
}
}
/**
* 記錄此次繪製RenderPanel產生的元件id , 才能在不需要的時候移除掉
*
* @param lc
*/
private void putRenderPanelComSet(LayoutContainer lc) {
renderPanelCom.put(lc.getId(),
new LinkedHashSet(EngineDataStore.genSequenceSet()));
EngineDataStore.genSequenceSet().clear();
}
private static void printLastRenderSpendTime(long processNodeTime,
long renderTime) {
String p = "processNode:" + (processNodeTime / 1000.0);
String r = "render:" + (renderTime / 1000.0);
System.err.println("GK: " + p + " / " + r);
}
public String getId() {
return id;
}
@Override
public void parserNode(List<UIGen> list, NodeList nodes) {
for (int i = 0; i < nodes.getLength(); i++) {
preprocessNode(nodes.item(i), list);
}
}
@Override
public void parserNode(List<UIGen> list, Node node) {
preprocessNode(node, list);
}
/**
* 開始解析目前的Node
*
* @param node
* 準備要解析的Node
* @param list
* 解析Node拿到的uiGen放在此list
*/
public void preprocessNode(Node node, List<UIGen> list) {
// Override元件庫
this.preprocessNode = ComLibrary.overrideNode(node);
int processTagAmt = 0;
String nName = node.getNodeName();
// 過濾 <#text></#text>、<#comment></#comment>、<import></import>
if (nName.endsWith("#text") || nName.endsWith("#comment")
|| nName.endsWith("import")) {
return;
}
// 如果不是com節點,通知此節點需要透過buildList註冊的建構器進行處理
processTagAmt = builder.publish(new EventObject(nName.toLowerCase(),
list));
// processTagAmt=0表示該Tag沒人處理,將tag名稱視為GUL檔案名稱到元件庫去找該元件
if (processTagAmt == 0) {
processExternalTag(nName, node, list);
}
}
/**
* 檢查引擎是否已完成載入元件庫
*
* @return boolean
*/
public boolean isReady() {
return ComLibrary.isReady();
}
/**
* 處理元件庫的節點
*
* @param nodeName
* @param node
* @param list
*/
private void processExternalTag(String nodeName, Node node, List<UIGen> list) {
// 找到元件將處理新attach的節點
if (ComLibrary.contains(nodeName)) {
if (!ComLibrary.isServerPage(nodeName)) {
NodeList nList = ComLibrary.replaceNode(nodeName, node);
parserNode(list, nList);
} else {
Map nodeInfo = NodeUtils.getAttributes(node);
String url = ComLibrary.getContent(nodeName);
String gul = ajaxComponent(url, nodeInfo);
NodeList nList = ComLibrary.replaceNode(node, gul);
parserNode(list, nList);
}
} else {
throw new LibraryNotFoundException(
EngineMessages.msg.error_libraryNotFound(nodeName));
}
}
/**
* <pre>
* 此方法可透過ajax調用jsp檔案取得GUL畫面再redner到前端
* 尚未提供傳資料給後端jsp的功能,可參考
* http://api.jquery.com/jQuery.post/ 透過 data屬性設定 j={...} 資訊
* </pre>
*
* @param url
* @param nodeInfo
* @return String
*/
private String ajaxComponent(String url, Map nodeInfo) {
String id = DOM.createUniqueId();
nodeInfo.put("_gk_file", url);
StringBuffer gul = new StringBuffer("<page><panel id='").append(id);
gul.append("' layout='fit'>").append("<js init='js:this'>");
gul.append("$.ajax({type : 'POST',url : 'event/put/ajaxComponent/jspBean.forward.go");
gul.append("',data:");
gul.append("'j={\"t\":\"map\",\"i\":")
.append(JsonConvert.toJSONObject(nodeInfo) + "").append("}");
gul.append("',dataType : 'text',success : function(gul) {gk.set('");
gul.append(id).append("',gul);}});</js></panel></page>");
return gul.toString();
}
/**
* 開始將nodeList裡面的元件進行調用,產生Component元件,並放入目前Panel
*
* @param panel
*/
private void renderUI(LayoutContainer panel) {
Iterator<UIGen> it = uiGenNodeList.iterator();
// 增加uiGenNodeList不為空的判斷,多次調用renderpage()時之前的uiGenNodeList會被清空
while (it.hasNext() && !uiGenNodeList.isEmpty()) {
UIGen uiGen = it.next();
Component com = uiGen.build();
if (com != null) {
// 如果是Window,就不添加到容器中
if (!(com instanceof Window)) {
// 如果元件被layout包起來,放元件到容器時會設定layoutData
if (uiGen instanceof XLayoutData) {
XLayoutData xLayout = (XLayoutData) uiGen;
panel.add(com, xLayout.getLayoutData());
} else {
panel.add(com);
}
}
// 增加判斷,如果uiGenNodeList包含之前取到的元素,才調用remove
if (uiGenNodeList.contains(uiGen)) {
it.remove();
}
}
}
}
/**
* 初始化所有此次建立的元件
*
* @param lc
*/
private void initialAllUIGen(LayoutContainer lc) {
Iterator<String> it = renderPanelCom.get(lc.getId()).iterator();
while (it.hasNext()) {
UIGen ui = EngineDataStore.getUIGenNode(it.next());
ui.init();
}
}
public native static String beforeParser(String gul)/*-{
return $wnd.gk.beforeParser(gul);
}-*/;
public native static String i18n(String gul)/*-{
return $wnd.gk.i18n(gul);
}-*/;
@Override
public Node getPreprocessNode() {
return preprocessNode;
}
}