|
|
||
Kirill Grouchnikov's BlogMay 2005 ArchivesThe real story about how Anakin Skywalker became Darth VaderPosted by kirillcool on May 28, 2005 at 01:54 PM | Permalink | Comments (16)Here it is - the JDK 5.0-compliant story behind "Revenge of the Sith". See it in all its glory, using the new java.util.concurrency package:
public class Princess implements ExecutorService {
public static final Princess PADME = new Princess();
// private member
private StarShip ship;
private Princess() {
this.setDefaultLocale(Locale.EN);
Timer timer = new Timer();
TimerTask task = new TimerTask() {
public void run() {
Princess.this.changeHairStyle();
Princess.this.changeClothes();
}
};
timer.scheduleAtFixedRate(task, new Date(), 1000);
}
}
public class JediKnight implements ExecutorService {
public static final JediKnight ANAKIN = new JediKnight(false);
public static final JediKnight OBI_WAN = new JediKnight(false);
public static final JediKnight YODA = new JediKnight(true);
private JediKnight(boolean isYoda) {
if (!isYoda) {
this.setDefaultLocale(Locale.EN);
}
else {
Locale yodaLocale = Locale.EN;
this.setDefaultLocale(Utils.shuffle(yodaLocale));
}
this.setLightSabre(LightSabreFactory.getInstance());
this.setGoodLooking(!isYoda);
}
}
public class Sith {
public static final Sith LORD = new Sith();
private Sith() {
this.setDefaultLocale(Locale.EN);
this.setClothes(DarkRobeFactory.getInstance());
this.setLightSabre(LightSabreFactory.getInstance());
}
}
public static main() {
// The following line has been commented out so as not
// to confuse fans who never did it
// Utils.initChildren(JediKnight.ANAKIN, Princess.PADME);
// Use JDK 5.0 concurrency package
Future<Set<Child>> padmeFuture = Princess.PADME.submit(
new Callable<Set<Child>>() {
public Set<Child> call() {
try {
int weeks = (int)(36+4.0*Math.random());
wait(weeks*7*24*60*60*1000);
this = null;
System.gc();
}
finally() {
Child boy = new Child("Luke");
Child girl = new Child("Leia");
Set<Child> result = new HashSet<Child>();
result.add(boy);
result.add(girl);
return result;
}
}
}
);
if (JediKnight.ANAKIN.poll(padmeFuture) instanceof ThreadDeath) {
JediKnight.ANAKIN.alarm();
// use some marketing ideas
Sith.LORD.promise(JediKnight.ANAKIN, new RuntimePermission("object.restore.afterGC"));
Sith.LORD.promise(JediKnight.ANAKIN, new RuntimePermission("force.power.dark.*"));
Sith.LORD.promise(JediKnight.ANAKIN, new RuntimePermission("force.power.*"));
Sith.LORD.promise(JediKnight.ANAKIN, new RuntimePermission("force.*"));
JediKnight.ANAKIN.setInternalName("Darth Vader");
JediKnight.ANAKIN.promise(Princess.PADME, new RuntimePermission("*"));
Princess.PADME.alarm();
Princess.PADME.boardShip();
// Use JDK 5.0 concurrency package
Future<StarShip> anakinFuture =
JediKnight.OBI_WAN.submit(
new Callable<StarShip>() {
public RuntimePermission call() {
try {
Class princessClass = Princess.getClass();
Field shipField = princessClass.getDeclaredField("ship");
// will never throw SecurityException on Jedi Knight
shipField.setAccessible(true);
JediKnight.this.add(new RuntimePermission("object.ship.board.*"));
return (StarShip)shipField.get(Princess.PADME);
}
catch (SecurityException se) {
// never supposed to happen to Jedi Knight
System.out.println("Internal error. Contact George Lucas.");
System.reboot();
}
}
}
);
StarShip padmeShip = anakinFuture.call();
padmeShip.put(JediKnight.OBI_WAN);
JediKnight.ANAKIN.see(Princess.PADME);
Princess.PADME.removeLove(JediKnight.ANAKIN);
JediKnight.ANAKIN.see(JediKnight.OBI_WAN);
JediKnight.ANAKIN.removeLove(Princess.PADME);
JediKnight.OBI_WAN.removeLimbs(JediKnight.ANAKIN);
// note - absolutely no need for GC, Anakin will be collected automatically.
ReferenceQueue<JediKnight> sithWatchList = new ReferenceQueue<JediKnight>();
PhantomReference<JediKnight> anakinRef = sithWatchList.remove();
JediKnight anakinPhantom = anakinRef.get();
Sith DARTH_VADER = Sith.LORD.assemble(anakinPhantom, LimbFactory.getSpareInstances());
MouthPiece newMouthPiece = MouthPieceFactory.getInstance();
// no need to check that it works - guaranteed to work with no noise
DARTH_VADER.add(newMouthPiece);
DARTH_VADER.rise();
}
else {
// TODO - create alternative scenario for Sith revenge
}
}
Using Java compiler in your Web Start applicationPosted by kirillcool on May 27, 2005 at 05:56 AM | Permalink | Comments (6)In JAXB Workshop project, I was faced with the following problem: after the XJC generator produces a set of Java source files, I needed to compile them and load them into the running JVM in order to collect cross-reference information: The standard technique for compiling Java source files in regular standalone application is to use the tools.jar that resides under jdk/lib directory, and use com.sun.tools.javac.Main class and its compile function to do the job. Another, more sophisticated technique that doesn't require explicitly specifying the location of the tools.jar is to use java.home system property and deduce the location of tools.jar from it:
private static Method javac = null;
static {
try {
File jreHome = new File(System.getProperty("java.home"));
System.out.println("java.home = " + jreHome.getAbsolutePath());
File toolsJar = new File(jreHome.getParent(), "lib/tools.jar");
System.out.println("tools.jar = " + toolsJar.getAbsolutePath());
ClassLoader toolsJarLoader = new URLClassLoader(
new URL[] { toolsJar.toURL() });
javac = toolsJarLoader.loadClass("com.sun.tools.javac.Main")
.getMethod("compile", new Class[] { String[].class, PrintWriter.class });
hasToolsJar = true;
} catch (Exception exc) {
javac = null;
hasToolsJar = false;
}
}
In the code above lies our first problem when the code will be executed in Web Start environment - no JDK installation is needed for such environment. Moreover, all default installations of JDK 6.0 early builds (that install JRE also), point the javaws.exe to work with the JRE and not the JDK. In this case, the value of java.home will point to the JRE installation directory, that doesn't have tools.jar under lib.At this point it becomes obvious that the Java compiler must be bundled with your application. And now the bad news - the size of the tools.jar is staggering 6,590 KB! The reason - it contains a lot of tools, such as apt, javah and so on. A simple solution would be to leave only the classes necessary for javac, resulting in about 1.5 MB, but that would not sit good with the Java license, especially for commercial applications. Currently, the only fully-TCK compliant alternative (for Java 5.0 features) is the JDT compiler that comes with Eclipse 3.1M7. The relevant jar is org.eclipse.jdt.core_3.1.0.jar and its size is 3,430 KB, almost half that of tools.jar. The list of all available compiler options is available here. At this point, it appeared that all was left to do - replace the package name and the function parameters. However, the problem that laid ahead was unexpected - missing classes in the classpath. The files generated by JAXB's XJC generator, reference classes in jsr173_api.jar and jaxb-api.jar (both parts of JAXB 2.0 Early Access Reference Implementation). These files are, of course, properly referenced in the JNLP descriptor (along with the JDT compiler jar): <resources> <j2se version="1.5+" /> ... <jar href="jaxb-api.jar"/> <jar href="jsr173_api.jar"/> <jar href="org.eclipse.jdt.core_3.1.0.jar"/> ... </resources>Nonetheless, the compiler was failing with error messages indicating that the corresponding Java classes couldn't be found in the classpath. A quick look at the compiler parameters show that we can specify -cp followed by the list of jar files. The first attempt would be to simply provide the following value: -cp jaxb-api.jar;jar173_api.jarThis approach fails, because the Web Start cache (even if you manage to find its location on the remote computer) stores the file under very cryptic names. For example, on my machine the cache is under C:\Documents and Settings\Owner\Application Data\Sun\Java\Deployment\cache\6.0, and sample file name is 3595ab4e-24c448c4. The second approach attempts to retrieve the actual URL of these two jars in the current JVM. Here we are once again faced with a problem specific to Web Start - the remote jar files do not appear in the java.class.path system property. The only way to retrieve them is via manifest files. A manifest file is, in fact, the most prominent component that makes jar file different from zip file. It is located under META-INF directory, and its name is MANIFEST.MF. So, how can we obtain a list of all available jars? Very simple - ask the class loader to enumerate all META-INF/MANIFEST.MF resources:
try {
for (Enumeration<?> e = org.jvnet.jaxbw.Workspace.class
.getClassLoader().getResources("META-INF/MANIFEST.MF");
e.hasMoreElements();) {
URL url = (URL) e.nextElement();
_logger.info("Scanning " + url.getFile());
...
} catch (IOException exc) {
exc.printStackTrace();
}
Sample logger output from the above code in Web Start environment is
INFO: Scanning https://jaxb-workshop.dev.java.net/webstart/jaxb-api.jar!/META-INF/MANIFEST.MFFrom the above URL, we can obtain the URL of the jar itself (marked in red), and then give it to the compiler in the classpath: -cp https://jaxb-workshop.dev.java.net/webstart/jaxb-api.jar;https://jaxb-workshop.dev.java.net/webstart/jar173_api.jarHowever, the compiler still complains about the missing files. Obviously, the classpath should reference local files only. It becomes clear now that we must have local copies of the remote jars and provide their (local) filenames to the compiler. One way to do this is: download the relevant jar files based on their URLs and save them as temporary files. The files should be marked to be deleted on application exit. The code now becomes:
private static synchronized String getLocalClassPath(Logger _logger) {
if (isJarsLoadedLocal) {
return localJarsClassPath;
}
Set
Here, we use a static synchronized function. This function checks whether the classpath was already computed, and if so - returns the classpah immediately. Otherwise - it enumerates all available jars, and calls getLocalJarFilename function that creates a local temporary copy of the parameter jar URL (function shown below). Then, after all the jars that we were interested in have been downloaded, we create the final classpath (using File.pathSeparator). The code for getLocalJarFilename is:
private static String getLocalJarFilename(URL remoteManifestFileName,
Logger _logger) {
_logger.info("Trying to create local version of "
+ remoteManifestFileName);
// remove trailing
String urlStrManifest = remoteManifestFileName.getFile();
String urlStrJar = urlStrManifest.substring(0, urlStrManifest.length()
- MANIFEST.length() - 2);
_logger.info("Remote : " + urlStrJar);
InputStream inputStreamJar = null;
File tempJar;
FileOutputStream fosJar = null;
try {
URL urlJar = new URL(urlStrJar);
if (urlJar == null) {
_logger
.info("No remote version (probably not running in JNLP)");
return null;
}
inputStreamJar = urlJar.openStream();
String strippedName = urlStrJar;
int dotIndex = strippedName.lastIndexOf('.');
if (dotIndex >= 0) {
strippedName = strippedName.substring(0, dotIndex);
strippedName = strippedName.replace("/", File.separator);
strippedName = strippedName.replace("\\", File.separator);
int slashIndex = strippedName.lastIndexOf(File.separator);
if (slashIndex >= 0) {
strippedName = strippedName.substring(slashIndex + 1);
}
}
tempJar = File.createTempFile(strippedName, ".jar");
_logger.info("Created temporary file '" + tempJar.getAbsolutePath()
+ "'");
tempJar.deleteOnExit();
fosJar = new FileOutputStream(tempJar);
byte[] ba = new byte[1024];
int bytesWritten = 0;
while (true) {
int bytesRead = inputStreamJar.read(ba);
if (bytesRead < 0) {
break;
}
fosJar.write(ba, 0, bytesRead);
bytesWritten += bytesRead;
}
_logger.info("Written " + bytesWritten + " bytes to local file");
return tempJar.getAbsolutePath();
} catch (Exception ioe) {
System.out.println(ioe.getMessage());
ioe.printStackTrace();
} finally {
try {
if (inputStreamJar != null) {
inputStreamJar.close();
}
} catch (IOException ioe) {
}
try {
if (fosJar != null) {
fosJar.close();
}
} catch (IOException ioe) {
}
}
return null;
}
Important points - we create a temporary file using File.createTempFile (no need to guess the remote computer OS etc.), mark it with deleteOnExit() (can then reuse it as long as the application is running) and finally return its absolute path.The code for compiling a set of Java source files now becomes:
ByteArrayOutputStream baosMessages = new ByteArrayOutputStream();
ByteArrayOutputStream baosWarnings = new ByteArrayOutputStream();
long time0 = System.currentTimeMillis();
StringBuffer bigAllNames = new StringBuffer();
for (SingleClassInfo currClassInfo : allClasses) {
String currFilename = currClassInfo.getRootDirName()
+ File.separator + currClassInfo.getRelJavaFilename();
this._logger.info("Adding " + currFilename + " to compilation");
bigAllNames.append("\"" + currFilename + "\" ");
}
String warningSetting = new String("allDeprecation,"
+ "allJavadoc," + "assertIdentifier," + "charConcat,"
+ "conditionAssign," + "constructorName," + "deprecation,"
+ "emptyBlock," + "fieldHiding," + "finalBound,"
+ "finally," + "indirectStatic," + "intfNonInherited,"
+ "javadoc," + "localHiding," + "maskedCatchBlocks,"
+ "noEffectAssign," + "pkgDefaultMethod," + "serial,"
+ "semicolon," + "specialParamHiding," + "staticReceiver,"
+ "syntheticAccess," + "unqualifiedField,"
+ "unnecessaryElse," + "uselessTypeCheck," + "unsafe,"
+ "unusedArgument," + "unusedImport," + "unusedLocal,"
+ "unusedPrivate," + "unusedThrown");
// Create classpath. For compiling XJC-generated files we need two
// jar files, jaxb-api.jar and jsr173_api.jar. When Milano is run
// from the local computer, these jars should be on classpath
// (explicitly or on PATH varibable). However, when our application
// runs from Java Web Start, these jars are not available by their
// names. Here, we will try to fetch them as resources and create
// temporary files on the local machine.
String classpath = Workspace.getLocalClassPath(this._logger);
this._logger.info("Classpath is " + classpath);
// Start compilation using jdtcore
boolean compilationStatus = org.eclipse.jdt.internal.compiler.batch.Main
.compile("-1.5 -source 1.5 -warn:" + warningSetting + " "
+ classpath + " " + bigAllNames.toString(),
new PrintWriter(baosMessages), new PrintWriter(
baosWarnings));
// Parse the messages and the warnings.
long time1 = System.currentTimeMillis();
BufferedReader br = new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(baosMessages.toByteArray())));
while (true) {
String str = br.readLine();
if (str == null) {
break;
}
this._logger.info("/javac/ " + str);
compilerMessages.addLast(str);
}
BufferedReader brWarnings = new BufferedReader(
new InputStreamReader(new ByteArrayInputStream(baosWarnings
.toByteArray())));
LinkedList<String> warnings = new LinkedList<String>();
while (true) {
String str = brWarnings.readLine();
if (str == null) {
break;
}
this._logger.warning("/javac/ " + str);
compilerMessages.addLast(str);
warnings.addLast(str);
}
this._logger.info((time1 - time0) + " milliseconds, "
+ (compilationStatus ? "success" : "failure"));
return compilationStatus;
Signing jars for java.net Web Start applicationsPosted by kirillcool on May 20, 2005 at 03:22 AM | Permalink | Comments (24)It's common these days to provide a Web Start version of your application that can run on a machine without the need to download the bundle distribution manually. If your application needs special access privileges, you will need to sign your jar file(s) and then ask user to allow installing this application. Here are the basic steps to do the job. First, you need to create a key store. For this, run the following command: keytool -genkey -keystore jaxb.keys -alias https://jaxb-workshop.dev.java.net/ -validity 365Here, in dark green are the parameters. The first one is the name of the key store. This will be a local file that will hold key pairs. The second one is a symbolic name for your key (that will be created in the key store). The last parameter is a number of days that your key will be valid (90 by default). Follow a simple sequence of steps, and don't forget to write down the password to the key store and to the key itself (in case they are different). Now, you need to make sure that all jar files in your application that need privileges are signed. Here is how you sign a single file: jarsigner -keystore jaxb.keys -storepass **** jaxb-api.jar https://jaxb-workshop.dev.java.net/Here, you provide the name of the key store and its password as the first two parameters, the name of the jar file you want to sign as the third parameter, and the key alias as the fourth parameter. Repeat the above steps for all you jar files. Now it's time to create a JNLP descriptor file for your Web Start application. Here is a simple file:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="https://jaxb-workshop.dev.java.net/webstart/" href="wizard.jnlp">
<information>
<title>XJC Wizard</title>
<vendor>https://jaxb-workshop.dev.java.net/</vendor>
<description>Wizard frontend for XJC generator</description>
<description kind="short">Wizard frontend for XJC generator</description>
<offline-allowed/>
</information>
<offline-allowed/>
<security>
<all-permissions/>
</security>
<resources>
<j2se version="1.5+"/>
<jar href="jaxbw.jar"/>
<jar href="substance.jar"/>
<jar href="jaxb-api.jar"/>
<jar href="jaxb-impl.jar"/>
<jar href="jaxb-xjc.jar"/>
<jar href="jsr173_api.jar"/>
<jar href="activation.jar"/>
</resources>
<application-desc main-class="org.jvnet.jaxbw.xjcfe.wizard.WizardMainFrame"/>
</jnlp>
Note that here we asked for all permissions for our application.All that is left to do - upload all the jar files and the JNLP file to CVS repository under www and put the JNLP URL in your page. Unfortunately, this is not all. When you will run the above JNLP, you will get the following Web Start exception: JAR resources in JNLP file are not signed by same certificateThe reason for this is simple - one of the jar files that you are using was already signed by another party. Here is the way to find it: jarsigner -certs -verbose -verify activation.jarYou will see a long list of certificates (one for each file). This means that this specific jar was signed by another party (Sun in our case). The solution for the problem is simple - put this jar in a separate JNLP and reference it in your main JNLP:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="https://jaxb-workshop.dev.java.net/webstart/" href="activation.jnlp">
<information>
<title>Activation</title>
<vendor>Sun Microsystems, Inc.</vendor>
<offline-allowed/>
</information>
<offline-allowed/>
<resources>
<jar href="activation.jar"/>
</resources>
<component-desc/>
</jnlp>
As you can see, we don't ask for permissions, as this specific jar doesn't need them. Then, you reference this activation.jnlp in your main JNLP:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="https://jaxb-workshop.dev.java.net/webstart/" href="wizard.jnlp">
<information>
<title>XJC Wizard</title>
<vendor>https://jaxb-workshop.dev.java.net/</vendor>
<description>Wizard frontend for XJC generator</description>
<description kind="short">Wizard frontend for XJC generator</description>
<offline-allowed/>
</information>
<offline-allowed/>
<security>
<all-permissions/>
</security>
<resources>
<j2se version="1.5+"/>
<jar href="jaxbw.jar"/>
<jar href="substance.jar"/>
<jar href="jaxb-api.jar"/>
<jar href="jaxb-impl.jar"/>
<jar href="jaxb-xjc.jar"/>
<jar href="jsr173_api.jar"/>
<extension name="activation" href="activation.jnlp"/>
</resources>
<application-desc main-class="org.jvnet.jaxbw.xjcfe.wizard.WizardMainFrame"/>
</jnlp>
How to create pulsating buttons in your Look and FeelPosted by kirillcool on May 06, 2005 at 04:44 AM | Permalink | Comments (0)In one of the more popular commercial look and feels, Alloy, the default focused button has nice animation effect - its inner border fades in and out in cycles. In Macinstosh OSX, the default button has pulsating effect to visually indicate that the corresponding action will be taken when the user hits the "Enter" key. The underlying code is not very complicated, although there are few spots for potential resource leaks. The code below is a part of Substance Look and Feel (currently under development). You can run a light-weight Web Start application to see the pulsating effect. In the dialog, click "Open new dialog" button to open a new dialog. Click "Close all dialogs" to close all dialogs that you have opened. Here a few screenshots that show the pulsating effect: Animated version (GIF with colors lost due to compression): Note how the default button changes its lightness and still retains the 3D quality (with the soft curved lighting in its upper half). First, we need to decide what button should be pulsating. Every top component (that has a root pane) can have a default button. In our case, we track all default buttons that were not garbage-collected, but animate only the default button in the focused window. This way, user's attention will not be distracted from the top-level frame or dialog. The code below uses weak references to track all default buttons of the application (via UI delegate). Once a default button is garbage-collected, the data structures are updated automatically (we use WeakHashMap). Each button has an associated Timer. The corresponding ActionListener checks whether the associated button lies inside a focused top level container. If it's inside, the cycle count for this button is incremented, otherwise it's reverted back to 0. Finally, the repaint() function of the associated button is called. This call eventually leads to update function in the UI delegate. This delegate asks the tracker for the cycle count, and paints the button accordingly.We have three classes, button UI, painting delegate and tracker: public class SubstanceButtonUI extends MetalButtonUI public class SubstanceButtonDelegate public class PulseTracker implements ActionListenerThe painting delegate captures common functionality of regular button UI and toggle button UI and provides the graphics functions. In button UI we have the following delegate instantiation:
private SubstanceButtonDelegate delegate;
public SubstanceButtonUI() {
this.delegate = new SubstanceButtonDelegate(true);
}
and the painting code is
@Override
public void update(Graphics g, JComponent c) {
AbstractButton button = (AbstractButton) c;
long cycle = 0;
boolean isAnimating = false;
if (button instanceof JButton) {
JButton jb = (JButton) button;
if (jb.isDefaultButton()) {
PulseTracker.update(jb);
}
cycle = PulseTracker.getCycles(jb);
isAnimating = PulseTracker.isAnimating(jb);
}
this.delegate.update(g, button, cycle, isAnimating);
}
For a button that is JButton, we check whether it is a default button, and if so, we ask the tracker to update its status. After that, we fetch the cycle count and the animation status from the tracker, and ask the delegate to paint the button based on the cycle count and animation status. The code of the delegate is not relevant for this thread and can be seen in the CVS repository. Let's see the details of the tracker. All the public functions of the tracker are static, and the most important one (update) is synchronized. Each tracker instance tracks a single not-GC'd default button. The tracker itself is an ActionListener, with associated Timer object. In addition, there are two static hash maps with weakly-referenced keys (each key is a JButton):
/**
* Map (with weakly-referenced keys) of all trackers. For each default
* button which has not been claimed by GC, we have a tracker (with
* associated
The tracker constructor is private and not-synchronized (as it is called from a synchronized function):
private PulseTracker(JButton jbutton) {
// Create weak reference.
buttonRef = new WeakReference<JButton>(jbutton);
// Create coalesced timer.
timer = new Timer(50, this);
timer.setCoalesce(true);
// Store event handler and initial cycle count.
PulseTracker.trackers.put(jbutton, this);
PulseTracker.cycles.put(jbutton, (long) 0);
}
It adds the button to the tracker map and to the cycle map (with initial value equal to 0). Note that the action listener of the Timer is the tracker itself. Here is the action event function:
public void actionPerformed(ActionEvent event) {
// get the button and check if it wasn't GC'd
JButton jButton = buttonRef.get();
if (jButton == null)
return;
if (!jButton.isDefaultButton()) {
// has since lost its default status
PulseTracker tracker = trackers.get(jButton);
tracker.stopTimer();
tracker.buttonRef.clear();
trackers.remove(jButton);
cycles.remove(jButton);
}
else {
if (!PulseTracker.hasFocus(jButton.getTopLevelAncestor())) {
// no focus in button window - will restore original (not
// animated) painting
PulseTracker.update(jButton);
} else {
// check if it's enabled
if (jButton.isEnabled()) {
// increment cycle count for default focused buttons.
long oldCycle = cycles.get(jButton);
cycles.put(jButton, oldCycle + 1);
}
else {
// revert to 0 if it's not enabled
if (cycles.get(jButton) != 0)
cycles.put(jButton, (long) 0);
}
}
}
jButton.repaint();
}
Note, that if the button was GC'd, there is no need to explicitly update the hash maps. This function checks whether the associated button is in a window that is a focus owner. If yes, the cycle count is incremented, otherwise the update function is called. Finally, we call repaint function on the button, which will eventually lead to our button UI (that will fetch the cycle count) and the painting delegate (that will use the cycle count to create the matching background). Two special cases: deafult disabled button is not animated, and we check that the tracked button is still default. Tracked button may remain visible in focused window, but due to some action listener lose its "defaultness". In this case the tracker is stopped and hash maps are updated accordingly. Here is the most important function in the tracker:
public static synchronized void update(JButton jButton) {
boolean hasFocus = PulseTracker.hasFocus(jButton.getTopLevelAncestor());
PulseTracker tracker = trackers.get(jButton);
if (!hasFocus) {
// remove
if (tracker == null)
return;
if (cycles.get(jButton) == 0)
return;
cycles.put(jButton, (long) 0);
// System.out.println("r::" + trackers.size());
} else {
// add
if (tracker != null) {
tracker.startTimer();
return;
}
tracker = new PulseTracker(jButton);
tracker.startTimer();
trackers.put(jButton, tracker);
cycles.put(jButton, (long) 0);
// System.out.println("a::" + trackers.size());
}
}
It has two code flows - one for the default buttons that are in focused windows, another for default buttons that are in non-focused windows. If the window does not own focus, the cycle count is reverted to 0 (if wasn't already so). If the window owns focus, we fetch its tracker. If the the tracker hash map already contains the tracker, we start it (startTimer is a helper function that starts timer if it wasn't started already) and return. On the next iteration of the Timer, we will get back to the actionPerformed function, and, if the button is still in focused window, it will be painted according to its new incremented cycle count. If the tracker hash map doesn't contain the tracker, we create a new tracker, start it, put it in the hash map and put the initial cycle count value 0 to the cycle hash map. Note that this function is synchronized to prevent multiple trackers from interfering with one another.Two additional helper functions that are used in the button UI update functions are:
public static long getCycles(JButton jButton) {
Long cycleCount = cycles.get(jButton);
if (cycleCount == null)
return 0;
return cycleCount.longValue();
}
public static boolean isAnimating(JButton jButton) {
PulseTracker tracker = trackers.get(jButton);
if (tracker == null)
return false;
return tracker.isRunning();
}
These functions check the corresponding hash maps and retrieve the values. In the Web Start demo application, when you click on "Open new dialog" button, a new JDialog is opened with a default button. You will see that the default button in the main window stops pulsating. Clicking "Close all dialogs" will close all opened dialogs and make sure that the default buttons are garbage-collected (using WeakReference and ReferenceQueue). You can uncomment lines in the tracker's update function to see that the WeakHashMaps are updated automatically.
| ||
|
|