package org.vaadin.applet;
import java.applet.Applet;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
/**
* This class can be used as base to implement Java Applets that integrate to
* Vaadin application.
*
* The class implements thread that polls for JavaScript (GWT) calls of
* {@link #execute(String)} and {@link #execute(String, Object[])} methods. This
* allows function privilege elevation if the applet has been signed
* accordingly. To support this behavior the inheriting applet should implement
* the {@link #doExecute(String, Object[])} method.
*
* Also the class introduces {@link #vaadinSync()} method for syncing the rest
*
* @author Sami Ekblad
*
*/
public abstract class AbstractVaadinApplet extends Applet {
private static final long serialVersionUID = -1091104541127400420L;
protected static final String PARAM_APP_SESSION = "appSession";
protected static final String PARAM_APP_URL = "appUrl";
protected static final String PARAM_APPLET_ID = "appletId";
protected static final String PARAM_PAINTABLE_ID = "paintableId";
protected static final String PARAM_APP_DEBUG = "appDebug";
protected static final String PARAM_ACTION_URL = "actionUrl";
protected static long MAX_JS_WAIT_TIME = 10000;
private boolean debug = false;
private JsPollerThread pollerThread;
private Object pollerLock = new Object[] {};
public boolean runPoller = true;
private String applicationURL;
private String sessionCookie;
private String paintableId;
private String appletId;
private String actionUrl;
@Override
public void init() {
setDebug("true".equals(getParameter(PARAM_APP_DEBUG)));
setAppletId(getParameter(PARAM_APPLET_ID));
setPaintableId(getParameter(PARAM_PAINTABLE_ID));
setApplicationURL(getParameter(PARAM_APP_URL));
setApplicationSessionCookie(getParameter(PARAM_APP_SESSION));
setAction(getParameter(PARAM_ACTION_URL));
// Start the poller thread for JS commands
pollerThread = new JsPollerThread();
pollerThread.start();
}
private void setAction(String submitAction) {
actionUrl = submitAction;
debug("actionUrl=" + submitAction);
}
/**
* Get the submit actionUrl.
*
* Submit actionUrl can be used to post multipart data
* back to the Vaadin server-side application.
*
* Note: This is not by the AppletIntegration automatically. It must be subclassed and a variable named "actionUrl" must be added to paintContent pointing to the {@link com.vaadin.server.StreamVariable}.
*
* @return
*/
protected String getActionUrl() {
return actionUrl;
}
/**
* Set the id of the applet in DOM.
*
* @param appletId
*/
private void setAppletId(String appletId) {
this.appletId = appletId;
debug("appletId=" + appletId);
}
/**
* Get the id of the applet in DOM.
*
* @return The id of this applet in the Vaadin application DOM document.
*/
protected String getAppleteId() {
return appletId;
}
/**
* Set the paintable id of the applet widget.
*
* @param paintableId
* The id of this applet widget in the Vaadin application.
*/
private void setPaintableId(String paintableId) {
this.paintableId = paintableId;
debug("paintableId=" + paintableId);
}
/**
* Get the paintable id of the applet widget.
*
* @return The id of this applet widget in the Vaadin application.
*/
protected String getPaintableId() {
return paintableId;
}
/**
* Set the application session cookie. Called from init.
*
* @param appSessionCookie
*/
private void setApplicationSessionCookie(String appSessionCookie) {
sessionCookie = appSessionCookie;
debug("sessionCookie=" + sessionCookie);
}
/**
* Get the application session cookie.
*
* @return The session cookie needed to communicate back to the Vaadin
* application instance.
*/
protected String getApplicationSessionCookie() {
return sessionCookie;
}
/**
* Set the application URL. Called from init.
*
* @param appUrl
*/
private void setApplicationURL(String appUrl) {
applicationURL = appUrl;
debug("applicationURL=" + applicationURL);
}
/**
* Get the application URL.
*
* @return The URL of the Vaadin application.
*/
protected String getApplicationURL() {
return applicationURL;
}
/**
* Debug a string if debugging has been enabled.
*
* @param string
*/
protected void debug(String string) {
if (!isDebug()) {
return;
}
System.err.println("debug: " + string);
}
/**
* Stop the poller and destroy the applet.
*
*/
@Override
public void destroy() {
runPoller = false;
super.destroy();
}
/**
* Invokes vaadin.forceSync that synchronizes the client-side GWT
* application with server. This is an asynchronous method call that returns
* immediately.
*
*/
public void vaadinSync() {
jsCallAsync("vaadin.forceSync()");
}
/**
* Invokes vaadin.appletUpdateVariable sends a variable to server.
*
* @param variableName
* @param newValue
* @param immediate
*/
public void vaadinUpdateVariable(String variableName, boolean newValue,
boolean immediate) {
String cmd = "vaadin.appletUpdateBooleanVariable('" + getPaintableId()
+ "','" + variableName + "'," + newValue + "," + immediate
+ ")";
jsCall(cmd);
}
/**
* Invokes vaadin.appletUpdateVariable sends a variable to server.
*
* @param variableName
* @param newValue
* @param immediate
*/
public void vaadinUpdateVariable(String variableName, int newValue,
boolean immediate) {
String cmd = "vaadin.appletUpdateIntVariable('" + getPaintableId()
+ "','" + variableName + "'," + newValue + "," + immediate
+ ")";
jsCall(cmd);
}
/**
* Invokes vaadin.appletUpdateVariable sends a variable to server.
*
* @param variableName
* @param newValue
* @param immediate
*/
public void vaadinUpdateVariable(String variableName, double newValue,
boolean immediate) {
String cmd = "vaadin.appletUpdateDoubleVariable('" + getPaintableId()
+ "','" + variableName + "'," + newValue + "," + immediate
+ ")";
jsCall(cmd);
}
/**
* Invokes vaadin.appletUpdateVariable sends a variable to server.
*
* @param variableName
* @param newValue
* @param immediate
*/
public void vaadinUpdateVariable(String variableName, String newValue,
boolean immediate) {
newValue = escapeJavaScript(newValue);
String cmd = "vaadin.appletUpdateStringVariable('" + getPaintableId()
+ "','" + variableName + "','" + newValue + "'," + immediate
+ ")";
jsCall(cmd);
}
/*
* TODO: Variable support missing for: String[], Object[], long, float,
* Map<String,Object>, Paintable
*/
/**
* Helper to call synchronously JavaScript and wrap the InterruptedException
* to a RuntimeException. If special handling for timeouts is needed the
* {@link RuntimeException} should be catched.
*/
private Object jsCall(String cmd) {
try {
return jsCallSync(cmd);
} catch (InterruptedException e) {
throw new RuntimeException(
"Synchronous JavaScript call timed out.", e);
}
}
/**
* Execute a JavaScript asynchronously. Note that this return immediately
* and JavaScript timing problems may occur if called sequentially multiple
* times.
*
* @param command
*/
public void jsCallAsync(String command) {
JSCallThread t = new JSCallThread(command);
t.start();
}
/**
* Execute a JavaScript synchronously.
*
* @param command
* @throws InterruptedException
*/
public Object jsCallSync(String command) throws InterruptedException {
JSCallThread t = new JSCallThread(command);
t.start();
t.join(MAX_JS_WAIT_TIME);
return t.getResult();
}
/**
* Thread for polling incoming JavaScript commands. Threading is used to
* change the call stack. If an applet function is invoked from JavaScript
* it will always use JavaScript permissions regardless of applet signing.
*
* This thread allows commands to be sent to the applet and executed with
* the applet's privileges.
*
* @author Sami Ekblad
*/
public class JsPollerThread extends Thread {
private static final long POLLER_DELAY = 100;
private String jsCommand;
private Object[] jsParams;
@Override
public void run() {
debug("Poller thread started.");
while (runPoller) {
// Check if a command was received
String cmd = null;
Object[] params = null;
synchronized (pollerLock) {
if (jsCommand != null) {
cmd = jsCommand;
params = jsParams;
jsCommand = null;
jsParams = null;
debug("Received JavaScript command '" + cmd + "'");
}
}
if (cmd != null) {
doExecute(cmd, params);
}
try {
Thread.sleep(POLLER_DELAY);
} catch (InterruptedException e) {
}
}
debug("Poller thread stopped.");
}
}
/**
* Thread for executing outgoing JavaScript commands. This thread
* implementation is used to asynchronously invoke JavaScript commands from
* applet.
*
* @author Sami Ekblad
*
*/
public class JSCallThread extends Thread {
private String command = null;
private Object result = null;
private boolean success = false;
/**
* Constructor
*
* @param command
* Complete JavaScript command to be executed including
*/
public JSCallThread(String command) {
super();
// SE: We need to remove all line changes to avoid exceptions
this.command = command.replaceAll("\n", " ");
}
@Override
public void run() {
debug("Call JavaScript '" + command + "'");
String jscmd = command;
try {
Method getWindowMethod = null;
Method evalMethod = null;
Object jsWin = null;
Class<?> c = Class.forName("netscape.javascript.JSObject");
Method ms[] = c.getMethods();
for (int i = 0; i < ms.length; i++) {
if (ms[i].getName().compareTo("getWindow") == 0) {
getWindowMethod = ms[i];
} else if (ms[i].getName().compareTo("eval") == 0) {
evalMethod = ms[i];
}
}
// Get window of the applet
jsWin = getWindowMethod.invoke(c,
new Object[] { AbstractVaadinApplet.this });
// Invoke the command
result = evalMethod.invoke(jsWin, new Object[] { jscmd });
if (!(result instanceof String) && result != null) {
result = result.toString();
}
success = true;
debug("JavaScript result: " + result);
}
catch (InvocationTargetException e) {
success = true;
result = e;
debug(e);
} catch (Exception e) {
success = true;
result = e;
debug(e);
}
}
/**
* Get result of the execution.
*
* @return
*/
public Object getResult() {
return result;
}
/**
* Get the result of execution as string.
*
* @return
*/
public String getResultAsString() {
if (result == null) {
return null;
}
return (String) (result instanceof String ? result : result
.toString());
}
/**
* Get the exception that occurred during JavaScript invocation.
*
* @return
*/
public Exception getException() {
return (Exception) (result instanceof Exception ? result : null);
}
/**
* Check if the JavaScript invocation was an success.
*
* @return
*/
public boolean isSuccess() {
return success;
}
}
public void setDebug(boolean debug) {
boolean change = this.debug != debug;
this.debug = debug;
if (change) {
debug("" + isDebug());
}
}
public void debug(Exception e) {
if (!isDebug()) {
return;
}
System.err.println("debug: Exception " + e);
e.printStackTrace();
}
public boolean isDebug() {
return debug;
}
/**
* Execute method that should be invoked from a JavaScript. This invokes a
* second thread (with applet's permission) to execute the command.
*
* @param command
*/
public void execute(String command) {
execute(command, null);
}
/**
* Execute method that should be invoked from a JavaScript. This invokes a
* second thread (with applet's permission) to execute the command.
*
* @param command
* @param params
*/
public void execute(String command, Object[] params) {
if (pollerThread == null) {
debug("Poller thread stopped. Cannot execute: '" + command + "'");
return;
}
synchronized (pollerLock) {
pollerThread.jsCommand = command;
pollerThread.jsParams = params;
}
}
/**
* Function to to actually execute a specific command.
*
* The inheriting applet must implement this to execute commands sent from
* JavaScript.
*
* Implementation may be empty if no JavaScript initiated commands are
* supported.
*
* @param command
*/
protected abstract void doExecute(String command, Object[] params);
/*
* --- Following methods are copied from
* org.apache.commons.lang.StringEscapeUtils under Apache 2.0 license--
*/
/**
* <p>
* Escapes the characters in a <code>String</code> using JavaScript String
* rules.
* </p>
* <p>
* Escapes any values it finds into their JavaScript String form. Deals
* correctly with quotes and control-chars (tab, backslash, cr, ff, etc.)
* </p>
*
* <p>
* So a tab becomes the characters <code>'\\'</code> and <code>'t'</code>.
* </p>
*
* <p>
* The only difference between Java strings and JavaScript strings is that
* in JavaScript, a single quote must be escaped.
* </p>
*
* <p>
* Example:
*
* <pre>
* input string: He didn't say, "Stop!"
* output string: He didn\'t say, \"Stop!\"
* </pre>
*
* </p>
*
* @param str
* String to escape values in, may be null
* @return String with escaped values, <code>null</code> if null string
* input
*/
public static String escapeJavaScript(String str) {
if (str == null) {
return null;
}
StringBuffer writer = new StringBuffer(str.length() * 2);
int sz = str.length();
for (int i = 0; i < sz; i++) {
char ch = str.charAt(i);
// handle unicode
if (ch > 0xfff) {
writer.append("\\u");
writer.append(hex(ch));
} else if (ch > 0xff) {
writer.append("\\u0");
writer.append(hex(ch));
} else if (ch > 0x7f) {
writer.append("\\u00");
writer.append(hex(ch));
} else if (ch < 32) {
switch (ch) {
case '\b':
writer.append('\\');
writer.append('b');
break;
case '\n':
writer.append('\\');
writer.append('n');
break;
case '\t':
writer.append('\\');
writer.append('t');
break;
case '\f':
writer.append('\\');
writer.append('f');
break;
case '\r':
writer.append('\\');
writer.append('r');
break;
default:
if (ch > 0xf) {
writer.append("\\u00");
writer.append(hex(ch));
} else {
writer.append("\\u000");
writer.append(hex(ch));
}
break;
}
} else {
switch (ch) {
case '\'':
// If we wanted to escape for Java strings then we would
// not need this next line.
writer.append('\\');
writer.append('\'');
break;
case '"':
writer.append('\\');
writer.append('"');
break;
case '\\':
writer.append('\\');
writer.append('\\');
break;
default:
writer.append(ch);
break;
}
}
}
return writer.toString();
}
/**
* <p>
* Returns an upper case hexadecimal <code>String</code> for the given
* character.
* </p>
*
* @param ch
* The character to convert.
* @return An upper case hexadecimal <code>String</code>
*/
private static String hex(char ch) {
return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
}
}