/* EmulatorClient.java
Purpose:
Description:
History:
Mar 20, 2012 Created by pao
Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/
package org.zkoss.zats.mimic.impl;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jetty.util.UrlEncoded;
import org.zkoss.zats.ZatsException;
import org.zkoss.zats.common.json.JSONValue;
import org.zkoss.zats.common.json.parser.ParseException;
import org.zkoss.zats.mimic.Client;
import org.zkoss.zats.mimic.ComponentAgent;
import org.zkoss.zats.mimic.DesktopAgent;
import org.zkoss.zats.mimic.EchoEventMode;
import org.zkoss.zats.mimic.exception.ZKExceptionHandler;
import org.zkoss.zats.mimic.impl.au.AuUtility;
import org.zkoss.zats.mimic.impl.emulator.Emulator;
import org.zkoss.zk.ui.Desktop;
/**
* The default server emulator client implementation.
* @author pao
*/
public class EmulatorClient implements Client, ClientCtrl {
private static Logger logger = Logger.getLogger(EmulatorClient.class.getName());
private Emulator emulator;
private Map<String, DesktopAgent> desktopAgents = new HashMap<String, DesktopAgent>();
private Map<String, String> cookies = new HashMap<String, String>();
private DestroyListener destroyListener;
private Map<String, List<UpdateEvent>> auQueues; // AU queues of desktops
private Map<String, List<UpdateEvent>> auQueues4piggyback; // AU queues for piggyback events
private EchoEventMode echoEventMode = EchoEventMode.IMMEDIATE;
public EmulatorClient(Emulator emulator) {
this.emulator = emulator;
this.auQueues = new ConcurrentHashMap<String, List<UpdateEvent>>();
this.auQueues4piggyback = new ConcurrentHashMap<String, List<UpdateEvent>>();
}
public DesktopAgent connectAsIncluded(String zulPath, Map<String, Object> args) {
if(zulPath == null)
throw new IllegalArgumentException("the path of ZUL can't be null");
if (args == null)
args = new HashMap<String, Object>();
// generate key and map for transferring data into server side
String key = "zats_" + Long.toString(Thread.currentThread().getId(), 36);
Map<String, Object> data = new HashMap<String, Object>();
data.put("url", zulPath);
data.put("args", args);
emulator.getServletContext().setAttribute(key, data);
// connect to adapter with key
String adapter = "/~./zats/includingAdapter.zul?id=" + key;
DesktopAgent desktop = connect(adapter);
// clean resources
emulator.getServletContext().removeAttribute(key);
data.clear();
return desktop;
}
public DesktopAgent connectWithContent(String content, String ext,
ComponentAgent parent, Map<String, Object> args) {
if (content == null)
throw new IllegalArgumentException(
"the content of ZUL can't be null");
if (args == null)
args = new HashMap<String, Object>();
// generate key and map for transferring data into server side
String key = "zats_"
+ Long.toString(Thread.currentThread().getId(), 36);
Map<String, Object> data = new HashMap<String, Object>();
data.put("content", content);
data.put("ext", ext);
data.put("args", args);
if (parent != null)
data.put("parent", parent.getOwner());
emulator.getServletContext().setAttribute(key, data);
// connect to adapter with key
String adapter = "/~./zats/createComponentsDirectlyAdapter.zul?id=" + key;
DesktopAgent desktop = connect(adapter);
// clean resources
emulator.getServletContext().removeAttribute(key);
data.clear();
return desktop;
}
public DesktopAgent connect(String zulPath) {
if(zulPath == null)
throw new IllegalArgumentException("the path of ZUL can't be null");
InputStream is = null;
try {
// load zul page
HttpURLConnection huc = getConnection(zulPath, "GET");
huc.connect();
// read response
fetchCookies(huc);
// check if there exists any exception during connect
List l;
if ((l = ZKExceptionHandler.getInstance().getExceptions()).size() > 0) {
//only throw the first exception, and clear all once thrown
throw (Throwable)l.get(0);
}
is = huc.getInputStream();
String raw = getReplyString(is, huc.getContentEncoding());
// get specified objects such as Desktop
Desktop desktop = (Desktop) emulator.getRequestAttributes().get("javax.zkoss.zk.ui.desktop");
// TODO, what if a non-zk(zul) page, throw exception?
DesktopAgent desktopAgent = new DefaultDesktopAgent(this, desktop);
desktopAgents.put(desktopAgent.getId(), desktopAgent);
// pass layout response to response handlers
List<LayoutResponseHandler> handlers = ResponseHandlerManager.getInstance().getLayoutResponseHandlers();
for (LayoutResponseHandler h : handlers) {
try {
h.process(desktopAgent, raw);
} catch (Throwable e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest("HTTP response header: " + huc.getHeaderFields());
logger.finest("HTTP response content: " + raw);
}
// ZATS-11: must flush AU requests after layout
flush(desktopAgent.getId());
return desktopAgent;
} catch (Exception e) {
throw new ZatsException(e.getMessage(), e);
} catch (Throwable t) {
throw new ZatsException(t.getMessage(), t);
} finally {
close(is);
//clear exceptions once thrown out
ZKExceptionHandler.getInstance().destroy();
}
}
public void destroy() {
if (destroyListener != null) {
destroyListener.willDestroy(this);
}
for (DesktopAgent d : desktopAgents.values()) {
destroy(d);
}
desktopAgents.clear();
// should be after cleaning desktop
cookies.clear();
auQueues.clear();
auQueues4piggyback.clear();
}
public void destroy(DesktopAgent desktopAgent) {
postUpdate(desktopAgent.getId(), null, "rmDesktop", null, true);
flush(desktopAgent.getId());
}
public void postUpdate(String desktopId, String targetUUID, String command, Map<String, Object> data, boolean ignorable) {
postUpdate(desktopId, targetUUID, command, data, ignorable, false);
}
public void postPiggyback(String desktopId, String targetUUID, String command, Map<String, Object> data, boolean ignorable) {
postUpdate(desktopId, targetUUID, command, data, ignorable, true);
}
private void postUpdate(String desktopId, String targetUUID, String command, Map<String, Object> data, boolean ignorable, boolean piggyback) {
if(desktopId==null){
throw new IllegalArgumentException("desktop id is null");
}else if(command == null){
throw new IllegalArgumentException("command is null");
}
// get or create AU queue
Map<String, List<UpdateEvent>> queues = piggyback ? auQueues4piggyback : auQueues;
List<UpdateEvent> queue = queues.get(desktopId);
if (queue == null) {
queue = new LinkedList<UpdateEvent>();
queues.put(desktopId, queue);
}
// queue such AU event
queue.add(new UpdateEvent(targetUUID, command, data, ignorable));
}
private String getCombinedEventString(String desktopId) {
// collect all events
List<UpdateEvent> queue = new LinkedList<UpdateEvent>();
if (auQueues4piggyback.containsKey(desktopId)) {
queue.addAll(auQueues4piggyback.remove(desktopId));
}
if (auQueues.containsKey(desktopId)) {
queue.addAll(auQueues.remove(desktopId));
}
if (queue.size() <= 0) {
return null;// do nothing
}
// combine AU events from queue into single request content
int index = 0;
StringBuilder sb = new StringBuilder();
while (!queue.isEmpty()) {
UpdateEvent event = queue.remove(0);
// command
sb.append("&cmd_").append(index).append("=").append(event.cmd);
// UUID
if (event.uuid != null) {
String uuid = UrlEncoded.encodeString(event.uuid);
sb.append("&uuid_").append(index).append("=").append(uuid);
}
// event data
if (event.data != null && event.data.size() > 0) {
String jsonData = UrlEncoded.encodeString(JSONValue.toJSONString(event.data));
sb.append("&data_").append(index).append("=").append(jsonData);
}
// ignorable
if (event.ignorable) {
sb.append("&opt_").append(index).append("=").append("i");
}
++index;
}
// debug log
if (logger.isLoggable(Level.FINEST)) {
logger.finest("Desktop " + desktopId + " perform AU: "
+ UrlEncoded.decodeString(sb.toString(), 0, sb.length(), "utf-8"));
}
return sb.toString();
}
@SuppressWarnings("unchecked")
public void flush(String desktopId) {
OutputStream os = null;
InputStream is = null;
// #ZATS-11: when post-flush, handlers process AU responses.
// They might require posting more AU requests immediately, so repeat posting.
while(auQueues.containsKey(desktopId) && auQueues.get(desktopId).size() > 0) {
try {
// combine AU events from queue into single request
StringBuilder sb = new StringBuilder();
sb.append("dtid=").append(UrlEncoded.encodeString(desktopId)); // desktop ID.
String combinedEvents = getCombinedEventString(desktopId); // this will clean such desktop's event queue
if (combinedEvents == null) { // do nothing
return;
}
final String content = sb.append(combinedEvents).toString();
// create http request and perform it
HttpURLConnection c = getConnection("/zkau", "POST");
c.setDoOutput(true);
c.setDoInput(true);
c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
if (logger.isLoggable(Level.FINEST)) {
logger.finest("HTTP request header: " + c.getRequestProperties());
logger.finest("HTTP request content: " + content);
}
c.connect();
os = c.getOutputStream();
os.write(content.getBytes("utf-8"));
close(os);
// read and parse response, and pass to response handlers
fetchCookies(c);
// check if there exists any exception during auRequest
List l;
if ((l = ZKExceptionHandler.getInstance().getExceptions()).size() > 0) {
//only throw the first exception, but can check in ZKExceptionHandler
throw (Throwable)l.get(0);
}
is = c.getInputStream();
String raw = getReplyString(is, c.getContentEncoding());
// ZATS-25: filter non-JSON part (i.e. real JS code)
raw = AuUtility.filterNonJSON(raw);
Map<String, Object> json = (Map<String, Object>) JSONValue.parseWithException(raw);
List<UpdateResponseHandler> handlers = ResponseHandlerManager.getInstance().getUpdateResponseHandlers();
for (UpdateResponseHandler h : handlers) {
try {
h.process(desktopAgents.get(desktopId), json);
} catch (Throwable e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest("HTTP response header: " + c.getHeaderFields());
logger.finest("HTTP response content: " + raw);
}
} catch (ParseException e) {
logger.log(Level.SEVERE, "unexpect exception when parsing JSON", e);
} catch (Exception e) {
throw new ZatsException(e.getMessage(), e);
} catch (Throwable t) {
throw new ZatsException(t.getMessage(), t);
} finally {
close(os);
close(is);
//clear exceptions once thrown out
ZKExceptionHandler.getInstance().destroy();
}
}
}
public HttpURLConnection getConnection(String path, String method) {
try {
URL url = new URL(emulator.getAddress() + path);
HttpURLConnection huc = (HttpURLConnection) url.openConnection();
huc.setRequestMethod(method);
huc.setUseCaches(false);
huc.addRequestProperty("Host", emulator.getHost() + ":" + emulator.getPort());
huc.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)");
huc.addRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
huc.addRequestProperty("Accept-Language", "zh-tw,en-us;q=0.7,en;q=0.3");
// handle cookies
for (Entry<String, String> cookie : cookies.entrySet()) {
String value = cookie.getValue() != null ? cookie.getValue() : "";
huc.addRequestProperty("Cookie", cookie.getKey() + "=" + value);
}
return huc;
} catch (Exception e) {
throw new ZatsException(e.getMessage(), e);
}
}
public InputStream openConnection(String path) throws IOException {
HttpURLConnection c = getConnection(path, "GET");
c.setDoOutput(false);
c.setDoInput(true);
return c.getInputStream();
}
private void close(Closeable c) {
try {
c.close();
} catch (Throwable e) {
}
}
private String getReplyString(InputStream is, String encoding) {
String reply = null;
Reader r = null;
try {
StringBuilder sb = new StringBuilder();
r = new BufferedReader(new InputStreamReader(is, encoding != null ? encoding : "ISO-8859-1"));
while (true) {
int c = r.read();
if (c < 0)
break;
sb.append((char) c);
}
reply = sb.toString();
} catch (Exception e) {
logger.log(Level.WARNING, "", e);
} finally {
close(r);
}
return reply;
}
public void setDestroyListener(DestroyListener l) {
destroyListener = l;
}
@SuppressWarnings("deprecation")
private void fetchCookies(HttpURLConnection connection)
{
// fetch and parse cookies from connection
List<String> list = connection.getHeaderFields().get("Set-Cookie");
if (list == null)
return;
for (String raw : list) {
try {
// parse cookie and append to the collection of cookies
String[] tokens = raw.trim().split(";"); // fetch cookie
tokens = tokens[0].split("="); // fetch key and value from cookie
cookies.put(tokens[0], tokens.length < 2 ? "" : tokens[1]); // value can be null
// get cookie attributes
byte[] data = raw.replaceAll(";", "\n").getBytes("ASCII");
Properties attr = new Properties();
attr.load(new ByteArrayInputStream(data));
// check expired time and remove cookie if necessary
String expired = attr.getProperty("Expires");
if (expired != null && expired.length() > 0) {
try {
long time = Date.parse(expired); // W3C Datetime Format
if (time < System.currentTimeMillis())
cookies.remove(tokens[0]);
} catch (Throwable e) {
logger.log(Level.WARNING, "unexpect exception when parsing HTTP Datetime string", e);
}
}
} catch (Exception e) {
new ZatsException("unexpected exception", e);
}
}
}
public void setCookie(String key, String value) {
if (key == null || key.startsWith("$"))
throw new IllegalArgumentException(key == null ? "cookie key name can't be null"
: "cookie key name can't be start with '$'");
if (value != null)
cookies.put(key, value);
else
cookies.remove(key);
}
public String getCookie(String key) {
if (key == null)
throw new IllegalArgumentException("cookie key name can't be null");
return cookies.get(key);
}
public Map<String, String> getCookies() {
return new HashMap<String, String>(cookies); // a copy of cookies
}
public void setEchoEventMode(EchoEventMode mode) {
if(mode != null) {
echoEventMode = mode;
}
}
public EchoEventMode getEchoEventMode() {
return echoEventMode;
}
private static class UpdateEvent {
String uuid;
String cmd;
Map<String, Object> data;
boolean ignorable = false;
UpdateEvent(String uuid, String cmd, Map<String, Object> data, boolean ignorable) {
this.uuid = uuid;
this.cmd = cmd;
this.data = data;
this.ignorable = ignorable;
}
}
}