Monday, June 15, 2009

Swing Event Notification Ordering

A problem that I run into from time to time is listeners getting callbacks in a different order than I want. For example, I might have a model that I want updated before I display something that uses it, but both the model update and the display get triggered by a mouse event. If they get handled in the wrong order, then the display will be incorrect.

There is an open bug regarding this issue and some discussion about it here and here. However, I don't believe Sun has addressed it yet.

One solution would be to rely on the fact that listeners get notified in the reverse order that they were added. But officially the order is undefined, which could cause your code to break if the Java SDK changes.

If you want to be called after other listeners have been notified, a better solution is to use SwingUtilities.invokeLater(), which adds your Runnable to the end of the EDT (event dispatcher thread). This works for the example above.

I'm not sure what would be a good way to get called before other listeners though.

Thursday, May 14, 2009

No to Java Serialization

After looking into Java's serialization API, I've decided that I wouldn't want to use it in most situations when using it to store data (mainly for application preferences and state). My main gripe is versioning. It seems too fragile when you consider what might change between versions, and between JVMs. If any errors occur when loading an object, the data is gone. I think there needs to be a way to read in as much of an object as you can, initialize other fields to defaults, and discard what you don't recognize. Sure you don't have the same level of certainty that the object is what you expected, but at least you get something if you have a bug (god forbid) in version compatibility.

There's not very much that you can change without having versioning problems. Adding fields requires no extra work, but almost any other change does. Granted there's probably a way to resolve every difference between versions, but maintaining the code after several versions seems like it would be a nightmare to me. Mark Davis wrote a good article of the considerations associated with serialization. Effective Java, one of the most recommended books on Java, starts out the chapter on serialization with the problems with serialization, and continues with techniques for getting around them (which require more work) and what to watch out for. To me, this is not a good indication. I found a good discussion of alternatives at stackoverflow.

For now, I'll stick to what I've been using: saving and loading object data to and from a java.util.Properties object. It doesn't support structured data, but I can fake it using the key strings (i.e. person_1234_firstname). Sure, not very efficient, but it's fine for the amount of data I have to save. XML would probably be my second choice.

Tuesday, September 23, 2008

Be careful when setting java.ext.dirs to include your JARs

When I found out that you can set java.ext.dirs to link a directory of JARs to your program, I was very happy, as was this guy. Normal way of linking JARs:
java -cp lib/helloworld.jar lib/foo.jar lib/bar.jar lib/jar.jar lib/cookie.jar ...
Easy way:
java -Djava.ext.dirs=lib ...
It's so much easier than adding 20 JARs to your classpath right? I found out that (surprise, surprise) this approach can cause pain too.

Sure it will work most of the time, but it turns out java.ext.dirs is already set to a directory in your JRE, such as C:\Program Files\Java\jdk1.6.0_07\jre\lib\ext. Ext stands for extension, which you can read about at Sun, but the takeaway is that you probably want to append to it rather than replace it. The more correct usage:
java -Djava.ext.dirs=jarlibdir;"C:\Program Files\Java\jdk1.6.0_07\jre\lib\ext" ...
Just make sure you have the right ext path for the JRE you're using.

In case you're wondering, I was getting an exception "com.jcraft.jsch.JSchException: Session.connect: java.security.NoSuchAlgorithmException: DH KeyPairGenerator not available" when I replaced the extension directory with my own, starting my program from the command line. It seemed strange since it was working in Eclipse. I found out after a while of poking around that SunJCE was involved. And where are the SunJCE JARs? That's right, in jre\lib\ext, which I had unwittingly unlinked. The lesson I guess is to be aware of what you're doing when you take shortcuts.

Classpath is ignored when using java -jar

I have an application that I packaged in a JAR, and I needed to add a directory to the classpath so my logging library Logback could find the configuration file I created (using a wizard). So I figured I could just add it using -cp:
java -cp mydir -jar myjar.jar
But no, the classpath doesn't get set when you use the -jar option (you can check this by calling System.getProperty("java.class.path")). It turns out you have to add it to the manifest file when creating the JAR:
Class-Path: classpath/
Also notice the slash at the end. It's not optional if you want to specify a directory. Don't ask me why, but it will only work if you add the slash.

Sunday, September 14, 2008

Using reflection to integrate with the OS X application menu

It took a while for me to find this solution, so I figured I'd post it.

The Java Swing application I've been working on at Aspera had been using the com.apple.mrj and com.apple.eawt libraries to handle OS X events such as the user selecting About, Preferences, or Quit from the Application Menu. The MRJ library is actually deprecated, but I was using it to prevent the default dialog from popping, because I didn't know you have to call setHandled() on the event, but I digress.

Apple actually provides a pretty neat solution that allows you to use com.apple.eawt without breaking builds on other platforms. It uses reflection to make the Apple-specific com.apple.eawt calls, so your application doesn't have to import them.

To integrate with their code, you just have to add the OSXAdapter.java class from their sample to your application. Then register for the events you want to handle (see the registerForMacOSXEvents() method in the irexample file MyApp.java), and specify the method you want to get callbacks for that event on.

More on integrating Java (Swing/SWT) with OS X in Apple's guide.

Friday, October 06, 2006

Beautify Ruby in Rails with Rake

I was looking for a way to automatically beautify Ruby code, and found a nice Ruby script (rbeautify.rb - version 2.1) with a little searching. However, I wanted to beautify all the Ruby code in my Rails project with a single command, so I wrote a Rake script.

My Rake script seems to work well, but some files do cause the beautifier to report Indentation errors (which I haven't looked into yet). Also, "here documents" in the code caused problems. I modified the beautifier to correctly skip over them, but it now also skips over code it shouldn't in rare cases (which should be harmless). I could not make the fix perfect because the beautifier seems to parse code on a line-by-line instead of token level. This makes it more difficult to recognize some things. As a safety measure, I also prevented the script from overwriting files on Indentation errors.


Installing:
1. Create /lib/rbeautify.rb
2. Create /lib/tasks/beautify.rake
3. Copy in the following:

beautify.rake:
require 'rbeautify'

namespace :rails do
desc "Beautifies all Ruby files in the project"
task :beautify do
_folders = Array.new
_folders.push(Dir.new(pwd)) # populate folder list with project dir
while _folder = _folders.pop # while folder list is populated
for _name in _folder # iterate through items in folder
_path = File.join(_folder.path, _name) # create full path
next if _name[0,1] == '.' # skip ".", "..", ".svn", etc.
if File.directory? _path
_folders.push(Dir.new(_path)) # add dirs to stack
elsif (_name[-3,3] == '.rb' || # only process Ruby files
_name[-5,5] == '.rake' ||
_name[-4,4] == '.rjs')
Beautify.beautifyRuby(_path) # call beatify
end
end
end
end
end



rbeautify.rb:
#!/usr/bin/ruby -w

# Ruby beautifier, version 2.1, 09/11/2006
# Copyright (c) 2006, P. Lutus
# Released under the GPL
#
# NOTE (by P. Tseng):
# Modified original to NOT back up any files, and to abort if any indentation
# errors are detected.
#
# KNOWN BUGS:
# Does not handle all cases of /<<[-'"\w]/

class Beautify
@@tabSize = 2
@@tabStr = " "

# indent regexp tests
@@indentExp = [
/^module\b/,
/^if\b/,
/(=\s*|^)until\b/,
/(=\s*|^)for\b/,
/^unless\b/,
/(=\s*|^)while\b/,
/(=\s*|^)begin\b/,
/(^| )case\b/,
/\bthen\b/,
/^class\b/,
/^rescue\b/,
/^def\b/,
/\bdo\b/,
/^else\b/,
/^elsif\b/,
/^ensure\b/,
/\bwhen\b/,
/\{[^\}]*$/,
/\[[^\]]*$/
]

# outdent regexp tests
@@outdentExp = [
/^rescue\b/,
/^ensure\b/,
/^elsif\b/,
/^end\b/,
/^else\b/,
/\bwhen\b/,
/^[^\{]*\}/,
/^[^\[]*\]/
]

def self.makeTab(tab)
return (tab <>
end

def self.addLine(line,tab)
line.strip!
line = makeTab(tab)+line if line.length > 0
return line + "\n"
end

def self.beautifyRuby(path)
hereDocumentTerminators = Array.new
hereDocumentStart = false
commentBlock = false
programEnd = false
multiLineArray = Array.new
multiLineStr = ""
tab = 0
source = File.read(path)
dest = ""
source.split("\n").each do |line|
if(!programEnd && hereDocumentTerminators.size == 0)
# detect program end mark
if(line =~ /^__END__$/)
programEnd = true
else
# combine continuing lines
if(!(line =~ /^\s*#/) && line =~ /[^\\]\\\s*$/)
multiLineArray.push line
multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
next
end

# add final line
if(multiLineStr.length > 0)
multiLineArray.push line
multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
end

tline = ((multiLineStr.length > 0)?multiLineStr:line).strip
if(tline =~ /^=begin/)
commentBlock = true
# Check for here document.
# Tries to skip some uses of <<>
# TODO: currently may wrongly assume other uses of <<>
# documents, if the <<>
# but should be safe (skips over them)
elsif(!(tline =~ /^#/) && (end_index = tline.index(/<<[-"'\w]/)) &&
!(tline =~ /class *<<[-"'\w]/) && !(tline =~ /['"\d\]] *<<[-"'\w]/))
# check if we're inside a string
current_index = 0
single_count = 0
double_count = 0
previous_c = -1
tline.each_byte do |c|
break if (current_index == end_index)
current_index += 1
# unescaped single quote
if (c == 39 &&amp;amp;amp;amp;amp;amp; !(double_count == 1) && previous_c != 92)
if single_count == 0
single_count = 1
else
single_count = 0
end
elsif (c == 34 && !(single_count == 1) && previous_c != 92)
if double_count == 0
double_count = 1
else
double_count = 0
end
end
previous_c = c
end

# if we're not in a string, get here doc terminators
if (single_count == 0 && double_count == 0)
hereDocumentTerminators = line.split('<<')
hereDocumentTerminators.delete_at(0)
for x in hereDocumentTerminators
hereDocumentTerminators.delete(x)
if (x.size != 0 && x =~ /^[-"'\w]/)
x.sub!(/-/, '')
if (x[0,1] == "'" || x[0,1] == '"')
x = x.sub(Regexp.new(x[0,1]), '').
sub(Regexp.new(x[0,1] + '.*'), '')
else
x = x.slice(/\w*/)
end
hereDocumentTerminators.push(x)
end
end
end

if hereDocumentTerminators.size != 0
hereDocumentStart = true
end
end
end
end
if((commentBlock || programEnd || hereDocumentTerminators.size > 0) &&
!hereDocumentStart)
# add the line unchanged
dest += line + "\n"
else
commentLine = (tline =~ /^#/)
if(!commentLine)
# throw out sequences that will
# only sow confusion
while tline.gsub!(/'.*?'/,"")
end
while tline.gsub!(/".*?"/,"")
end
while tline.gsub!(/\`.*?\`/,"")
end
while tline.gsub!(/\{[^\{]*?\}/,"")
end
while tline.gsub!(/\([^\(]*?\)/,"")
end
while tline.gsub!(/\/.*?\//,"")
end
while tline.gsub!(/%r(.).*?\1/,"")
end
tline.gsub!(/\\\"/,"'")
@@outdentExp.each do |re|
if(tline =~ re)
tab -= 1
break
end
end
end
if (multiLineArray.length > 0)
multiLineArray.each do |ml|
dest += addLine(ml,tab)
end
multiLineArray.clear
multiLineStr = ""
else
dest += addLine(line,tab)
end
if(!commentLine)
@@indentExp.each do |re|
if(tline =~ re && !(tline =~ /\s+end\s*$/))
tab += 1
break
end
end
end
end
if(tline =~ /^=end/)
commentBlock = false
elsif hereDocumentStart
hereDocumentStart = false
elsif(hereDocumentTerminators.size > 0)
for terminator in hereDocumentTerminators
if line.strip == terminator
hereDocumentTerminators.delete(terminator)
break
end
end
end
end
if(source != dest && tab == 0)
# make a backup copy
#File.open(path + "~","w") { |f| f.write(source) }
# overwrite the original
File.open(path,"w") { |f| f.write(dest) }
puts 'Beautified: ' + path
end
if(tab != 0)
STDERR.puts "Indentation error (#{tab}): #{path}"
end
end

#if(!ARGV[0])
# STDERR.puts "usage: Ruby filenames to beautify."
# exit 0
#end
#
#ARGV.each do |path|
# beautifyRuby(path)
#end

end