Porting small library from Java 5.0 to Java 1.4 - could it be any harder?
And so, last evening I set out to provide 1.4-compatible version of my Substance look-and-feel library. What promised to be an easy task (a couple of changes per file, given not too excessive use of generics), turned out to be a frustrating and valuable experience not only with Swing classes, but with Java 5.0 features in general.
First off, I should say that at work we write for JDK 1.4.2. For me, last year that amounted to roughly 2600 hours spent on writing 1.4.2-compliant code. At home (for a few projects here on java.net) it's been 5.0 from the beginning. Last year that amounted to roughly 700 hours spent on using 5.0 features. Not claiming to know my way through all new features in 5.0 (there were way too many hands raised on Joshua Bloch's "Collections Connection" when he asked to see who knows everything in 5.0), I feel comfortable with all new language features. Contrary to what some claim (he looks suspiciously familiar), generics in the "light" version (no wildcards, no "super" or "extends" etc.) make the code look better and more robust. As a collection guru, you may think that nobody can forget that exact value type stored in some map, but what about your average programmer who left last week and whose bugs you need to fix? Your best hope would be that he documented the correct types of keys and values in javadoc, otherwise you would either hunt all the puts, or debug until your fingers are blue and ready to fall off.
And so, i thought. 77 files, 440KB of code. How hard can it be? It shouldn't even matter how many files or how many lines of code. Just take Retroweaver, add ant task and lean back. The "lean back" part ended abruptly, seeing that i have used quite a handful of methods that were new in 5.0 (which i used). Here lies the main deception of Retroweaver - its abilities are very limited. It can handle generics, enhanced for loops, enums, iterables, but that's about it. A class that's new in 5.0, or a new method on existing class - you're on your own. And that certainly discourages the use of Retroweaver - I moved to 5.0 not only because of the language features. I want to use new stuff like Formatter, Scanner, concurrency, ExecutorService and even String.replace that gets two CharSequences as parameters. And so, the first big disappointment was - if you want to port your library back to 1.4, you can't use any 5.0 new stuff. That sounds obvious, but not so if you start writing a new library straight in 5.0.
The next step was to see the functions that were not present in 1.4 and replace them with the corresponding counterparts from 1.4. That sounds easy, but when you extend functionality of the existing Java classes, it's not. For look-and-feel, you extend the existing UI delegates and plug-in your stuff. The stuff you plug should involve only drawing, so that couldn't change much between different JDK versions, right? Right and wrong. It doesn't change much, but a lot of stuff gets exposed in later JDK versions that was private in the previous ones. One of the best examples here would be createScrollButton in TabbedPaneUI. When you set your tabbed pane to work in SCROLL_TAB_LAYOUT mode, the tabs that overflow the available space are not visible, and can be "scrolled to" using two small arrow buttons. The default implementation draws the button on its own, ignoring the installed ButtonUI, so you would want to override this behaviour to provide consistent look and feel to all the buttons. In 5.0 it's very easy - you are override createScrollButton() function, it's called and then Swing plugs in all the listeners on those buttons. In 1.4 - big surprise. Pretty much everything in BaseTabbedPaneUI is private, the buttons, the listener, the layout. And everything is not only wired, but also hardcoded for the class names. And you can't make your button inherit from the inner button - its class is private. What you can do - the good old "copy paste". That's what is done in JGoodies - its TabbedPaneUI is a complete rip-off (with a few tweaks to plug its own buttons), spanning 3092 lines. My original 5.0 version of TabbedPaneUI is 235 lines. And that brought the second disappointment - there are very good reasons to use new functions. That sounds obvious too, but yet again if you start out with new functionality and then force yourself to go back, you'll have to implement it yourself.
At this point, the next step was obvious - the code must be rewritten. But should it still be in 5.0? That's a tough question. You can integrate Retroweaver in your ant script and run it everytime you change something. You can pay close attention to javadoc comments of every single function that you intend to use. You can set your compiler to 1.4 settings, but it will not catch use of new 5.0 functions. Or, you can just take your code and backport it to 1.4 yourself. After all, you are not using any new stuff (apart from generics and enums), and you are not burdened with all the bookkeeping.
A few clicks in Eclipse, and the project was configured to 1.4. Thirty seconds later, the build is over, and on 77 files I had 856 errors. About 30% were on generics (both parametrized collections and missing casts on retrieving objects), 30% were on @Override (you use that a lot writing look-and-feel under 5.0), and 35% were on enums. The most unexpected part was for enums - you just get used to having name(), values() and using them in switch statements. The harsh 1.4 reality sets in, and even when you use Bloch's enum pattern, you still need to provide name(), values() and rewrite your switch statements. Couple of hours later - all the errors were gone (except the functions that were not available in 1.4), and the code was uglier than before, especially when you need to store Longs in a Map (autoboxing is nice), or iterating over entries in Set or array. The third disappointment set in during the two hours frantic coding - removing 5.0 language features takes more that it seems. Less obvious (maybe because it's sold as "syntactic change" only), but time-consuming nonetheless.
When the last errors were fixed, I run the test application that tests all component UIs. Instantly i was greated with the long despised "use getRootPane().add()" message. Quick fix, the application runs, but:
- background of menu and menu items is dark gray instead of nice light gray. The same for combobox.
- default button behaviour is transferred to the clicked button.
- the buttons are not "rollover-enabled"
- dialog root pane is not opaque
- selected items in list are not highlighted
- sliders have background for ticks
- progress bar is off by 1 pixel in both width and height
- root pane header is off by 1 pixel in width
All these were either fixed or corrected for 5.0. Now i have to introduce "bug fixes" myself. That's when the last disappointment sets in - each new JDK version fixes existing bugs in older versions. Obvious as well, but quite unexpected when all you want is to run your library in older JDK and just want it to show the same behaviour.
With all of the above, twenty four hours later, the library has not been ported back, but a few lessons have been learned.