001: package abbot.script;
002:
003: import java.awt.Component;
004: import java.awt.Frame;
005: import java.awt.event.*;
006: import java.applet.*;
007: import java.util.*;
008: import java.io.*;
009: import java.lang.reflect.*;
010: import javax.swing.SwingUtilities;
011:
012: import abbot.Log;
013: import abbot.NoExitSecurityManager;
014: import abbot.finder.*;
015: import abbot.finder.matchers.*;
016: import abbot.i18n.Strings;
017: import abbot.tester.ComponentTester;
018:
019: /**
020: * Provides applet launch capability. Usage:<br>
021: * <blockquote><code>
022: * <applet code="..." [codebase="..."] [params="..."]
023: * [archive="..."]><br>
024: * </code></blockquote><p>
025: * The attributes are equivalent to those provided in the HTML
026: * <code>applet</code> tag. The <code>params</code> attribute is a
027: * comma-separated list of <code>name=value</code> pairs, which will be passed
028: * to the applet within the <code>applet</code> tag as <code>param</code>
029: * elements.
030: * <p>
031: * <em>WARNING: Closing the appletviewer window from the window manager
032: * close button will result applet-spawned event dispatch threads being left
033: * running. To avoid this situation, always use the appletviewer <b>Quit</b>
034: * menu item or use the {@link #terminate()} method of this class.</em>
035: */
036: public class Appletviewer extends Launch {
037:
038: private String code;
039: private Map params;
040: private String codebase;
041: private String archive;
042: private String width;
043: private String height;
044: private ClassLoader appletClassLoader;
045: private Frame appletViewerFrame;
046: private transient SecurityManager oldSM;
047: private transient boolean terminating;
048:
049: private static final String DEFAULT_WIDTH = "100";
050: private static final String DEFAULT_HEIGHT = "100";
051: private static final String CLASS_NAME = "sun.applet.Main";
052: private static final String METHOD_NAME = "main";
053: private static final int LAUNCH_TIMEOUT = 30000;
054:
055: private static final String USAGE = "<appletviewer code=\"...\" [params=\"name1=value1,...\"] "
056: + "[codebase=\"...\"] [archive=\"...\"]>";
057:
058: protected void quitApplet(final Frame frame) {
059: // FIXME: this isn't locale-safe
060: // Don't need to wait for idle
061: new ComponentTester().selectAWTMenuItem(frame, "Applet|Quit");
062: }
063:
064: private static Map patchAttributes(Map map) {
065: map.put(TAG_CLASS, CLASS_NAME);
066: map.put(TAG_METHOD, METHOD_NAME);
067: return map;
068: }
069:
070: /** Create an applet-launching step. */
071: public Appletviewer(Resolver resolver, Map attributes) {
072: super (resolver, patchAttributes(attributes));
073: code = (String) attributes.get(TAG_CODE);
074: params = parseParams((String) attributes.get(TAG_PARAMS));
075: codebase = (String) attributes.get(TAG_CODEBASE);
076: archive = (String) attributes.get(TAG_ARCHIVE);
077: width = (String) attributes.get(TAG_WIDTH);
078: height = (String) attributes.get(TAG_HEIGHT);
079: }
080:
081: /** Create an applet-launching step. */
082: public Appletviewer(Resolver resolver, String description,
083: String code, Map params, String codebase, String archive,
084: String classpath) {
085: super (resolver, description, CLASS_NAME, METHOD_NAME, null,
086: classpath, false);
087: this .code = code;
088: this .params = params;
089: this .codebase = codebase;
090: this .archive = archive;
091: }
092:
093: /** Run this step. Causes the appropriate HTML file to be written for use
094: by appletviewer and its path used as the sole argument.
095: */
096: public void runStep() throws Throwable {
097: File dir = new File(System.getProperty("user.dir"));
098: File htmlFile = File.createTempFile("abbot-applet", ".html",
099: dir);
100: htmlFile.deleteOnExit();
101: try {
102: FileOutputStream os = new FileOutputStream(htmlFile);
103: os.write(generateHTML().getBytes());
104: os.close();
105: setArguments(new String[] { "[" + htmlFile.getName() + "]" });
106: super .runStep();
107: // Wait for the applet to become visible
108: long start = System.currentTimeMillis();
109: ComponentFinder finder = new BasicFinder(getResolver()
110: .getHierarchy());
111: Matcher matcher = new ClassMatcher(Applet.class, true);
112: while (true) {
113: try {
114: Component c = finder.find(matcher);
115: appletViewerFrame = (Frame) SwingUtilities
116: .getWindowAncestor(c);
117: addCloseListener(appletViewerFrame);
118: appletClassLoader = c.getClass().getClassLoader();
119: break;
120: } catch (ComponentSearchException e) {
121: }
122: if (System.currentTimeMillis() - start > LAUNCH_TIMEOUT) {
123: throw new RuntimeException(Strings
124: .get("step.appletviewer.launch_timed_out"));
125: }
126: Thread.sleep(200);
127: }
128: } finally {
129: htmlFile.delete();
130: }
131: }
132:
133: private void addCloseListener(final Frame frame) {
134: // Workaround for lockup when applet is closed via WM close button and
135: // then relaunched. Can't figure out how to kill those extant threads.
136: // This avoids the lockup, but leaves threads around.
137: frame.addWindowListener(new WindowAdapter() {
138: public void windowClosing(WindowEvent e) {
139: Log.debug("window closing");
140: quitApplet(frame);
141: }
142: });
143: }
144:
145: /** Generate HTML suitable for launching this applet. */
146: protected String generateHTML() {
147: StringBuffer html = new StringBuffer();
148: html.append("<html><applet code=\"" + getCode() + "\"");
149: html.append(" width=\"" + getWidth() + "\"" + " height=\""
150: + getHeight() + "\"");
151: if (getCodebase() != null) {
152: html.append(" codebase=\"" + getCodebase() + "\"");
153: }
154: if (getArchive() != null) {
155: html.append(" archive=\"" + getArchive() + "\"");
156: }
157: html.append(">");
158: Iterator iter = params.keySet().iterator();
159: while (iter.hasNext()) {
160: String key = (String) iter.next();
161: String value = (String) params.get(key);
162: html.append("<param name=\"" + key + "\" value=\"" + value
163: + "\">");
164: }
165: html.append("</applet></html>");
166: return html.toString();
167: }
168:
169: public void setTargetClassName(String name) {
170: if (CLASS_NAME.equals(name))
171: super .setTargetClassName(name);
172: else
173: throw new IllegalArgumentException(Strings
174: .get("step.call.immutable_class"));
175: }
176:
177: public void setMethodName(String name) {
178: if (METHOD_NAME.equals(name))
179: super .setMethodName(name);
180: else
181: throw new IllegalArgumentException(Strings
182: .get("step.call.immutable_method"));
183: }
184:
185: public void setCode(String code) {
186: this .code = code;
187: }
188:
189: public String getCode() {
190: return code;
191: }
192:
193: public void setCodebase(String codebase) {
194: this .codebase = codebase;
195: }
196:
197: public String getCodebase() {
198: return codebase;
199: }
200:
201: public void setArchive(String archive) {
202: this .archive = archive;
203: }
204:
205: public String getArchive() {
206: return archive;
207: }
208:
209: public String getWidth() {
210: return width != null ? width : DEFAULT_WIDTH;
211: }
212:
213: public void setWidth(String width) {
214: this .width = width;
215: try {
216: Integer.parseInt(width);
217: } catch (NumberFormatException e) {
218: this .width = null;
219: }
220: }
221:
222: public String getHeight() {
223: return height != null ? height : DEFAULT_HEIGHT;
224: }
225:
226: public void setHeight(String height) {
227: this .height = height;
228: try {
229: Integer.parseInt(height);
230: } catch (NumberFormatException e) {
231: this .height = null;
232: }
233: }
234:
235: public Map getParams() {
236: return params;
237: }
238:
239: public void setParams(Map params) {
240: this .params = params;
241: }
242:
243: protected Map parseParams(String attribute) {
244: Map map = new HashMap();
245: if (attribute != null) {
246: String[] list = ArgumentParser.parseArgumentList(attribute);
247: for (int i = 0; i < list.length; i++) {
248: String p = list[i];
249: int eq = p.indexOf("=");
250: map.put(p.substring(0, eq), p.substring(eq + 1));
251: }
252: }
253: return map;
254: }
255:
256: public String[] getParamsAsArray() {
257: ArrayList list = new ArrayList();
258: // Ensure we always get a consistent order
259: Iterator iter = new TreeMap(params).keySet().iterator();
260: while (iter.hasNext()) {
261: String key = (String) iter.next();
262: String value = (String) params.get(key);
263: list.add(key + "=" + value);
264: }
265: return (String[]) list.toArray(new String[list.size()]);
266: }
267:
268: public String getParamsAttribute() {
269: return ArgumentParser.encodeArguments(getParamsAsArray());
270: }
271:
272: public Map getAttributes() {
273: Map map = super .getAttributes();
274: map.put(TAG_CODE, getCode());
275: if (params.size() > 0)
276: map.put(TAG_PARAMS, getParamsAttribute());
277: if (getCodebase() != null)
278: map.put(TAG_CODEBASE, getCodebase());
279: if (getArchive() != null)
280: map.put(TAG_ARCHIVE, getArchive());
281: if (!DEFAULT_WIDTH.equals(getWidth()))
282: map.put(TAG_WIDTH, getWidth());
283: if (!DEFAULT_HEIGHT.equals(getHeight()))
284: map.put(TAG_HEIGHT, getHeight());
285:
286: // don't need to store these
287: map.remove(TAG_CLASS);
288: map.remove(TAG_METHOD);
289: map.remove(TAG_THREADED);
290: map.remove(TAG_ARGS);
291:
292: return map;
293: }
294:
295: public String getDefaultDescription() {
296: String desc = Strings.get("step.appletviewer",
297: new Object[] { getCode() });
298: return desc;
299: }
300:
301: public String getUsage() {
302: return USAGE;
303: }
304:
305: public String getXMLTag() {
306: return TAG_APPLETVIEWER;
307: }
308:
309: /** Returns the applet class loader. */
310: public ClassLoader getContextClassLoader() {
311: // Maybe use codebase/archive to have an alternative classpath?
312: return appletClassLoader != null ? appletClassLoader : super
313: .getContextClassLoader();
314: }
315:
316: protected void install() {
317: super .install();
318:
319: // Appletviewer expects the security manager to be an instance of
320: // AppletSecurity. Use the custom class loader, *not* the one for the
321: // applet.
322: installAppletSecurityManager(super .getContextClassLoader());
323: }
324:
325: /** Install a security manager if there is none; this is a workaround
326: * to prevent sun's applet viewer's security manager from preventing
327: * any classes from being loaded.
328: */
329: private void installAppletSecurityManager(ClassLoader cl) {
330: oldSM = System.getSecurityManager();
331: Log.debug("install security manager");
332: // NOTE: the security manager *must* be loaded with the same class
333: // loader as the appletviewer.
334: try {
335: Class cls = Class.forName(
336: "abbot.script.AppletSecurityManager", true, cl);
337: Constructor ctor = cls.getConstructor(new Class[] {
338: SecurityManager.class, boolean.class, });
339: SecurityManager sm = (SecurityManager) ctor
340: .newInstance(new Object[] { oldSM,
341: new Boolean(removeSMOnExit()) });
342: System.setSecurityManager(sm);
343: } catch (Exception exc) {
344: Log.warn(exc);
345: }
346: }
347:
348: protected boolean removeSMOnExit() {
349: return false;
350: }
351:
352: protected Frame getFrame() {
353: return appletViewerFrame;
354: }
355:
356: /** To properly terminate, we need to invoke AppletViewer's appletQuit()
357: * method (protected, but accessible).
358: */
359: public void terminate() {
360: synchronized (this ) {
361: // Avoid recursion, since we'll return here when the applet
362: // invokes System.exit.
363: if (terminating)
364: return;
365:
366: terminating = true;
367: }
368: Frame frame = appletViewerFrame;
369: appletViewerFrame = null;
370: try {
371: // FIXME: figure out why closing the appletviewer window causes an
372: // EDT hangup.
373: // Also figure out who's creating all the extra EDTs and dispose of
374: // them properly, but it's probably not worth the effort.
375: if (frame != null) {
376: quitApplet(frame);
377: }
378: // Now clean up normally
379: super .terminate();
380: while (System.getSecurityManager() != oldSM) {
381: Thread.sleep(10);
382: }
383: Log.debug("SM restored");
384: appletClassLoader = null;
385: oldSM = null;
386: } catch (InterruptedException e) {
387: Log.warn(e);
388: } finally {
389: synchronized (this ) {
390: terminating = false;
391: }
392: }
393: }
394: }
|