|
|
||
Evan Summers's BlogJuly 2006 ArchivesSwing and Roundabouts 4: Grid Bag GreasePosted by evanx on July 27, 2006 at 06:02 AM | Permalink | Comments (3)
We hear that Netbean's Matisse GroupLayout is state-of-the-art. It does seem to have a learning curve, for hand coding. So i haven't got around to trying it yet. Anyway, my requirements are not very stringent, and so i still just use GridBagLayout, which is my layout comfort zone. I use spacer panels ie. with fill not NONE, and assemble subpanels, and get what i want without any constraints, i mean complaints. Cay Horstmann introduced "GBC.java - a convenience class to tame the GridBagLayout" (2002) in Core Java. That must have leaked out of my subconscious at some point, and led to me to implement a similar friendly extension of GridBagConstraints, as follows.
import java.awt.GridBagConstraints; import java.awt.Insets; public class Gbc extends GridBagConstraints { public Gbc(int gridx, int gridy, int anchor, int fill, Insets insets) { super(gridx, gridy, 1, 1, 0., 0., NORTHWEST, NONE, new Insets(0, 0, 0, 0), 0, 0); anchor(anchor); fill(fill); insets(insets); } public Gbc(int gridx, int gridy, Insets insets) { this(gridx, gridy, 0, 0, insets); } public Gbc(int gridx, int gridy) { this(gridx, gridy, 0, 0, null); } public Gbc insets(Insets insets) { if (insets == null) insets = new Insets(0, 0, 0, 0); this.insets = insets; return this; } public Gbc anchor(int anchor) { if (anchor == 0) anchor = NORTHWEST; this.anchor = anchor; return this; } public Gbc fill(int fill) { if (fill == 0) fill = NONE; if (fill == HORIZONTAL || fill == BOTH) weightx = 1; if (fill == VERTICAL || fill == BOTH) weighty = 1; this.fill = fill; return this; } public Gbc none() { return fill(NONE); } public Gbc both() { return fill(BOTH); } public Gbc vertical() { return fill(VERTICAL); } public Gbc horizontal() { return fill(HORIZONTAL); } public Gbc center() { return anchor(CENTER); } public Gbc north() { return anchor(NORTH); } public Gbc northeast() { return anchor(NORTHEAST); } public Gbc east() { return anchor(EAST); } public Gbc southeast() { return anchor(SOUTHEAST); } public Gbc south() { return anchor(SOUTH); } public Gbc southwest() { return anchor(SOUTHWEST); } public Gbc west() { return anchor(WEST); } public Gbc northwest() { return anchor(NORTHWEST); } public Gbc top(int top) { insets.top = top; return this; } public Gbc bottom(int bottom) { insets.bottom = bottom; return this; } public Gbc right(int right) { insets.right = right; return this; } public Gbc left(int left) { insets.left = left; return this; } public Gbc insets(int top, int left, int bottom, int right) { insets = new Insets(top, left, bottom, right); return this; } public Gbc cloneGbc() { Gbc gbc = (Gbc) clone(); gbc.insets = (Insets) insets.clone(); return gbc; } public static String formatGbcValue(int value) { if (value == NONE) return "NONE"; if (value == HORIZONTAL) return "HORIZONTAL"; if (value == VERTICAL) return "VERTICAL"; if (value == BOTH) return "BOTH"; if (value == NORTH) return "NORTH"; if (value == NORTHEAST) return "NORTHEAST"; if (value == EAST) return "EAST"; if (value == SOUTHEAST) return "SOUTHEAST"; if (value == SOUTH) return "SOUTH"; if (value == SOUTHWEST) return "SOUTHWEST"; if (value == WEST) return "WEST"; if (value == NORTHWEST) return "NORTHWEST"; if (value == CENTER) return "CENTER"; return "gbcValue=" + value; } public String toString() { StringBuffer buffer = new StringBuffer("Gbc"); buffer.append(" x" + gridx); buffer.append(" y" + gridy); buffer.append(" " + formatGbcValue(anchor)); buffer.append(" " + formatGbcValue(fill)); buffer.append(" " + insets.toString()); return buffer.toString(); } } where i use weights that are either 0.0 or 1.0, depending on the fill. Feel free to use and abuse this class as you wish. You can find it via gridbaglady.dev.java.net, under the ASL. So the latest application for which i used the above, is the following, and this application was used to do the syntax highlighting of the above, in counjunction with Netbeans' "Print to HTML". Which i'll leave as a topic for an upcoming blog Plumber's Hack 1: Highlighting Sourcy, so this is a sneak preview :)
Here is the launcher for the above.
The screenshot below, of the source of the above, shows Gbc.java in action.
As a further example, the following code uses Gbc to create a simple button panel, as used for the above demo.
public JPanel createButtonPanel(NAction ... actions) { JPanel panel = new JPanel(new GridBagLayout()); int index = 0; for (NAction action : actions) { panel.add(new JButton(action), new Gbc(index, 0).right(4)); index++; } return panel; } where the right inset is given as 4, to space the buttons apart.
The next article is this Swing and Roundabouts series is Gooey Maker
which will present a pack of some simple foundation and helper classes for hand-coding
Swing apps (such as the above Web Start demo) including Gbc.java for layout.
Firing up the Browser PluginPosted by evanx on July 25, 2006 at 08:33 AM | Permalink | Comments (6)A recent blog by David Van Couvering discussed scripting languages, and the JVM in the browser, "if only as a virtual machine to run dynamic languages like Ruby and Groovy." He referred to Ethan Nicholas's "Java 2 Browser Edition", which is another great blog entry that i only discovered and read today. Ethan writes that the problem with Java (applets) in the browser, compared to competitors like Flex and Ajax, is that the JRE download is too big (7Mb plugin), starts up too slowly (eg. could be 30 seconds), is hard to install and upgrade (compared to the consumer-savvy Flash plugin), and is not as reliable as Flash (eg. on some machines/browsers, applets just won't run, whatever you do). Besides improving the installer and improving reliability, he suggests the following two solutions.
Herewith are my comments. The issue of reliability should of course be addressed. I imagine this an engineering integration/testing issue with all the combinations of OS'es, browsers and JRE's, which requires lots of resources. On the issue of installers, these days users definitely expect a painless "auto update" feature, and we should give it to them. On the issue of download size, maybe having a 7Mb monolithic download might be better than multiple 1Mb ones on demand, because then at least it's a once-off? And Dell et al ship Java preinstalled, and broadband keeps getting broader (and cheaper). I believe that every PC should have the latest JRE installed, and in time, every PC will. But i accept i'm sticking my head in the sand here.
Opensource Java will open opportunities for some collaborative innovation in such areas, eg. with Apple, IBM and Google. It's in everyone's interest to have the best possible Java Plugin. And Desktop Java and Web Start, would consequently also enjoy faster startup times eg. courtesy of a preloaded shared JVM with a prefetching/caching ClassLoader :) OpenOffice addressed its huge startup time in part by pre-loading itself (as a background process). Maybe the Java Plugin could do the same, and feature an optimised ClassLoader that does some preloading of the most commonly used classes from standard libraries. In future, hard drives will feature large flash RAM caches, eg. 4Gb and the like. In this case, so long as the JRE and Java Plugin can book a spot in flash, preloading isn't necessary. If Java is often used, then it'll certainly get a spot. Maybe OpenOffice, which will be often used, can see to it that Java is also always often used ;) So I want a Java Plugin with (a) the option to get loaded when the browser starts, and (b) with a class cache for preloading standard libraries. There could be different cache size settings, eg. a "cold" one for startup eg. 0 to 16Mb, a "warmer" one to kick in when the first applet tag is seen in a web page, and a "hot" one for after an applet has been launched, eg. 0 to 32Mb. Considering that the Java5 rt.jar is 32Mb uncompressed, and 7Mb compressed. In practice, i guess you would have "cache size" and then "performance" settings of "low", "medium" and "high." Maybe an option to have a compressed cache would be handy, for trading off CPU vs memory, just in case you still have one of those legacy single-core machines in the future ;)
With such optimisations, if in future you're waiting for more than a second for
the Java Plugin to kick in, concurrently with your browser loading the applet jar
via 100Mbit WiMax, then perhaps you don't have a quad-core Dell under your desk,
with a "Java Inside" sticker next to that "Designed for Opensource" one!? ;)
Trip and Tick 2: JooJ up your project page with a Web Start demoPosted by evanx on July 20, 2006 at 04:55 AM | Permalink | Comments (6)This series kicked off with Trip and Tick 1: Checking out a java.net project. So you're hosting your project on java.net, and you've uploaded some screenshots. Supoib! The next step is putting a Webstart "Launch" button on your page, innit. Oh and a screencast video thingy, see the upcoming Trip and Tick 3: The Movie for that. Since i'm too lazy to read the JNLP documentation, and to write a JNLP file from scratch by hand, i'm gonna use those firefox goggles for starters. Later we'll be forced to read Deploying Software with JNLP and Java Web Start and edit the JNLP XML by hand, when as expected, things don't work as expected right off the bat. Using the goggles we quickly see that Netbeans has a JNLP tool, woohoo! And a tutorial aptly named Using Java Web Start in NetBeans IDE which i followed as follows. We go to the Update Manager, choose the Netbeans Update Center Beta.
When in doubt press the OK button. Unfortunately the above screen does not have an OK button, so we try the Next button.
We add the Netbeans Module for Java Web Start.
I think we should meet and greet this module!
Now we see a Java Web Start item in the menu when we right click on our project. We enable this, and when we Run with Webstart, Netbeans generates our JNLP file, woohoo! It even provides a JNLP designer for for manipulating the file with the mouse, as you can see below. Oisome!
As expected from years of experience with softwarez, it doesn't work for us the first time, D'oh! We enable the Java Webstart Console to see the exception, or just click on Details and select the Exception tab. We see it's a security permissions problem with the application trying to access system properties ie. command line options. Probably preferences would also cause a security violation. So i disable properties and preferences in the application.
Trying again, there is a different error, ie. progress, woohoo!
Looks like the above security exception is caused by field.setAccessible() ie. reflection. Let's try a different angle of attack which is to disable sandbox security, for now. We do this by adding a security element with all-permissions into our JNLP as follows. If you know how to provide limited permissions, but not all permissions, eg. to allow reflection and "standard" stuff, but obviously not local file system access, please post a comment.
But we get a "jar not signed" JNLP exception, as we see when we click on Details and then the Exception tab.
For this to work we gotta sign the jar as detailed in Web Start Developer's Guide, which gives us the following keytool and jarsigner commands. (Update. Kirill Grouchnikov's "Signing jars for java.net Web Start applications" provides a great tutorial on signing your jars.)
cd /opt/java5/bin
keytool -genkey -keystore myKeystore -alias myself
keytool -selfcert -alias myself -keystore myKeystore
keytool -list -keystore myKeystore
jarsigner -keystore myKeystore /aptframework/netbeans/dist/aptframework.jar myself
javaws /aptframework/netbeans/aptframework.jnlp
where the keytool commmands we do once only, to create our "keystore," and the jarsigner command we do to prep the jar prior to trying to web start it.
Now we write a JNLP file for our web page, as follows.
<?xml version="1.0" encoding="UTF-8"?>
<jnlp codebase="http://jroller.com/resources/e/evanx/">
<information>
<title>AptFramework Demo</title>
<vendor>aptframework.dev.java.net</vendor>
<icon href="default"/>
<offline-allowed/>
</information>
<security>
<all-permissions/>
</security>
<resources>
<j2se version="1.5+" />
<jar href="aptframework.jar"/>
</resources>
<application-desc main-class="aptcomponent.common.ZViewContext"/>
</jnlp>
For going online, all that changes is the codebase, which is now an http URL, rather than a local file.
Now we can insert the JNLP link into our HTML web page as follows.
<a href="http://jroller.com/resources/e/evanx/aptframework.jnlp"> <img border=0 src="http://javadesktop.org/javanet_images/webstart.small.gif"> </a> Which looks like which hopefully works for you!? Addendum on Dependent Jar Resources Kirill Grouchnikov's "Signing jars for java.net Web Start applications" addresses dependent jars, where these might be signed by someone else, eg. activation.jar et al signed by Sun. In this case, you can't include these directly as jar resources in your JNLP. As Kirill shows, the trick is to wrap them in their own JNLP, and list that as the resource in your JNLP. I include such an example below, for completeness. But if you have dependent jars that are signed by someone else eg. Sun, then you gonna get an error because your jars are not all signed by the same certificate, ie. yours. You can inspect the signing certificates et al, using the following command. (Incidently, the following JavaDB jar isn't signed, but for the sake of this discussion, let's pretend that it is signed by Sun.)
jarsigner -certs -verbose -verify /projects/aptframework/lib/derby.jar As Kirill shows, we can create a JNLP file for dependent jars which are signed by Sun et al, as follows.
<?xml version="1.0" encoding="UTF-8"?>
<jnlp codebase="http://aptframework.dev.java.net" href="javadb.jnlp">
<information>
<title>JavaDB jar</title>
<vendor>Signed by Sun Microsystems, Inc</vendor>
<offline-allowed/>
</information>
<resources>
<jar href="derby.jar"/>
</resources>
<component-desc/>
</jnlp>
where in this case, i'm gonna check-in dependent jars under the www subdirectory of my java.net project, in which case the codebase is my java.net project homepage. And then in the resources section of our JNLP, we list dependent jars, including those wrapped in their own JNLP file, as follows.
<resources>
<j2se version="1.5+" />
<jar href="aptframework.jar"/>
<jar href="aptfoundation.jar"/>
<extension href="javadb.jnlp"/>
</resources>
Now i've just gotta update my demo to actually use JavaDB, eg. for an in-memory database :)
World Wide sWingPosted by evanx on July 14, 2006 at 07:24 AM | Permalink | Comments (2)I commented in Simon Morris' "In Defence of the Desktop", on how non-database desktop apps could use a database, for example for logging. Then queries could be performed using dates and severity, eg. for looking for exception stack traces after the fact. See my upcoming Plumber's Hack 2 article, where i undertake to demonstrate this :) Maybe you venture onsite for a meeting, and get cornered in a corridor by a user complaining that the application crashes all the time. "No worries, lemme get my magic wand from the car, and wave it over your computer, so that it can telepathically tell me what went wrong, and i can telepathically fix it for you right away - how does that sound!?" Actually i wouldn't dare say that to a user... in case they believe me! ;) Simon quite rightly reminded me that pure desktop apps don't log - they typically throw the exception up in a JOptionPane. Maybe with a "Details" button. Maybe with a "Email support desk" or "Send Error Report" button. Supoib! Simon said, "Perhaps this just highlights how the desktop and web worlds are so very very different?" I say that perhaps this just highlights how the desktop world is so very very diverse!? :) As John Reynolds points out in "Why Use A Database Instead Of", most "business" desktop apps capture, manipulate and/or present data. In short, many Swing apps out there in the wild world are very much like web apps, at least in function - eg. your database front-end business app. I would warrant that these are the ones that keep most Swing developers out there employed. Desktop Swing apps in the fields of graphics, visualisation, etcetera, and of course the "usual" desktop apps such as media, office/productivity, gaming, etcetera, make Web and Swing database apps seem like the siblings. So what i'm saying is that as a Swing/database developer, i feel closer to most of the "web world" than to the rest of the "desktop" world. In function, if not in form. Incidently, what is the definition of a "desktop" application? Is it strictly a 1-tier client application, that runs on the desktop, for personal productivity, never connecting to a server on a network? No, it is any (graphical) application whose user interface is not a web browser. Certainly that is a diverse group of applications! Interestingly, as the "web" continues towards "web services" and SOA, ie. where webservers dish up webservice XML for other programs to consume, rather than HTML for people's eyeballs to consume, this lends itself towards Swing front-ends, arguably more so than browser front-ends. Certainly more so than ever before in the "web world." Which is another reason why EJB3 is gonna be so great for Swing programmers too! :) The Java Persistence API, JAXB2, Web Services, Messaging, and other components of EJB3 are surely gonna be used increasingly by Swing clients. Is a new world of "Swing JEE web programming" dawning? Swing and Roundabouts 3: FramewarezPosted by evanx on July 13, 2006 at 06:29 AM | Permalink | Comments (0)
See preceeding Swing articles Event DTs, Turn Tables, Panel Beater and Inside Action. (Excuse my aggressive refactoring of article names, eg. from Going into Action to Inside Action.)
Our monastery operates a brewery. Our highly popular "Dosy Abbot" ale has kept us in food and, um, ale, for the past 368 years. So we need to keep track of our ingredients, products, orders, and what-not. Now some of us newer younger monks are interested in computers, in addition to beer of course. And we got the go-ahead from the Abbot to write a stock control system, woohoo! So we wrote this Swing desktop app. And I got nominated to write this article. So our application has of a whole bunch of CURD worksheets, eg. for editing products, product categories, suppliers, customers, etcetera. Then we have to capture and view transactions, representing stock movements and related financial documents, eg. purchase orders, invoices, delivery notes, stock transfers, stock takes, etcetera. So we need to assemble all these worksheets into an application framework, with access control. In the first instance, the user should login. Then we display the "menu system" to enable the user to launch "worksheets." Users typically have limited access, ie. to a specific subset of worksheets. For example, it's not a good idea to give the Abbot access to everything because a little bit of knowledge is a very dangerous thing. But stock transfers to our pantry, and the shrinkage report on our finished products, is for the Abbot's eyes only. The design presented here is an improved sugar-coated redesign of the "access system" of aptframework.dev.java.net.
Before we wrote our brewery warez, we used a spreadsheet to manage the monastery. (And we still do, to tell the truth.) So when the Abbot OK'ed us writing a computer program to do the same thing, he "requested" that we stick to the spreadsheet metaphor. So we call our "programs" worksheets, and we can open any number of worksheets as tabs at the bottom of the screen, like a spreadsheet program.
Our application framewarez is a JFrame, with a JMenuBar, and a JTabbedPane. Our worksheets are launched from the menu. Worksheets render themselves as JPanel's, which we add to the tabbed pane.
And here's a Web Start demo, woohoo! You can read about The Making Of this Web Starter in next week's Trip and Tick 2 article, including why it runs outside of the sandbox, and so is jarsigned, and see a Request for Help on sandboxing these thingymajigs.
We need to configure our menu items. That is, their labels, icons, keystrokes and the associated worksheet to launch, eg. "Edit Product" launches ZProductWorksheet. Our framework provides a GMenuConfigurator for us to extend as follows.
package org.trappist.belgium.brouwerij.menu;
...
public class ZMenuConfigurator extends GMenuConfigurator {
... // system menu eg. lock screen, logout, exit
@MenuAnnotation(
label = "Edit"
)
GMenuConfiguration topEdit = createMenu(null);
@MenuAnnotation(
label = "Edit Product",
worksheet = ZProductWorksheet.class,
icon = "yast_security",
toolTip = "Edit products",
ordinal = 2
)
GMenuConfiguration editProduct = createMenu(topEdit);
... // menus for all other worksheets in the application
@MenuAnnotation(
label = "Help"
)
GMenuConfiguration topHelp = createMenu(null);
@MenuAnnotation(
label = "Online help",
icon = "lifesaver",
ordinal = 3
)
GMenuConfiguration helpOnlineHelp = createMenu(topHelp);
@MenuAnnotation(
label = "About",
icon = "lightbulb",
ordinal = 4
)
GMenuConfiguration helpAbout = createMenu(topHelp);
public ZMenuConfigurator() {
super();
super.configure();
}
}
where the GMenuConfigurator superclass is implemented as follows.
package greenscreen.menu;
...
public class GMenuConfigurator {
protected List<GMenuConfiguration> menuList = new ArrayList();
protected GMenuConfigurator() {
}
protected GMenuConfiguration createMenu(GMenuConfiguration parentMenu) {
GMenuConfiguration menu = new GMenuConfiguration();
menu.setParentMenu(parentMenu);
menuList.add(menu);
return menu;
}
protected void configure() {
... // read externalised list of MenuConfiguration objects
for (Field field : getClass().getFields()) {
if (field.getType() == GMenuConfiguration.class) {
field.setAccessible(true);
configure(field, (GMenuConfiguration) field.get(this));
}
}
}
protected void configure(Field field, GMenuConfiguration menu) {
menu.setMenuId(field.getName());
... // configure using MenuAnnotation
... // override with externalised configuration
}
public void writeConfigurationFile(File configurationFile) {
...
}
protected void readConfigurationFile(File configurationFile) {
...
}
}
where writeConfigurationFile() is invoked by the developer to generate an externalised configuration file, as presented further below.
Using Java code to capture defaults as above, enables us to leverage the IDE, eg. enjoy auto-completion on the worksheet class names. We can take this further for icons, by generating content for an "icon class" as follows.
public class GIconClassGenerator {
...
public void generate(String iconDirectory) {
for (String fileName : getFileNameList(iconDirectory)) {
if (!ileName.endsWith(".png")) {
String camelCaseFileName = toCamelCase(fileName);
StringBuffer buffer = new StringBuffer();
buffer.append("public final GIcon " + camelCaseFileName);
buffer.append(" = createIcon(\"" + fileName + "\");");
System.out.println(buffer);
}
}
}
}
where getFileNameList() lists all the files in the given directory.
We adopt an approach where we code our defaults in the first instance, for rapid prototyping, as in the above ZMenuConfigurator. At any stage, we can then externalise these defaults, eg. by invoking writeConfigurationFile() to emit content for a resource bundle, and/or XML configuration file, in order to support translation and customisation. When the application starts up, we load the configuration file to override the coded defaults, eg. using readConfigurationFile() in the above GMenuConfigurator. We read and parse the configuration data into configuration objects, eg. using JAXB2 to bind the following configuration object to XML.
@XmlElement(name = "menu")
public class GMenuConfiguration {
@XmlAttribute protected GMenuConfiguration parentMenu;
@XmlAttribute protected Class worksheetClass;
@XmlAttribute protected String menuId;
@XmlAttribute protected String keyStroke;
@XmlAttribute protected Character mnemonic;
@XmlAttribute protected String iconName;
@XmlAttribute protected String label;
@XmlAttribute protected String toolTip;
@XmlAttribute protected Integer ordinal;
... // getters and setters
... // configure(GMenuConfiguration), to overwrite with non-null properties from another instance
}
A diatribe on XML follows. I choose Java to program the default configuration, because Java is more programmable, toolable and beautiful than XML. So i love tools like JAXB2, to map XML to Java, using annotations, so that i can program XML in Java. Which, as a Java programmer, i naturally prefer.
Since our worksheets are created by the developer, the menu configuration ie. what worksheets we have available, is not editable by the system administrator. However, the users, user roles, and menu access control lists, must be editable by an administrative user. So we provide worksheets for that, eg. ZUserWorksheet, ZUserRoleWorksheet and ZMenuAccessWorksheet. Our "access control lists" indicate which user roles have access to a given menu (or menu item). To keep things tidy, we have DAOs for menus, as well as users and user roles. Our users have a many-to-many relationship to user roles, and our user roles have a many-to-many relationship to our menus. Therefore, we might have "membership" tables, and so DAO's for those too.
public class ZAccessEntityManager extends MEntityManager {
public ZMenuInfo menu = new ZMenuInfo();
public ZMenuMembershipInfo menuMembership = new ZMenuMembershipInfo();
public ZUserInfo user = new ZUserInfo();
public ZUserRoleInfo userRole = new ZUserRoleInfo();
public ZUserRoleMembershipInfo userRoleMembership = new ZUserRoleMembershipInfo();
...
}
where meme's MEntityManager was introduced in Bean Curd 2: The SQL. We might get a filtered list of those menus available for a given user role by invoking entityManager.menuMembership.getMenuList(userRole) which might be implemented as follows.
public class ZMenuMembershipInfo extends MEntityBean<ZMenuMembership> {
...
public List<ZMenu> getMenuList(ZUserRole userRole) {
List<ZMenu> menuList = new ArrayList();
for (ZMenuMembership membership : super.getExtentEntityList()) {
if (membership.getUserRole().equals(userRole))) {
menuList.add(membership.getMenu());
}
}
return menuList;
}
}
where MEntityBean.getExtentEntityList() gets all the entities, ie. rows in this database table.
When the user logs into the application, we need to build the menu bar, including only those menus and menu items (which mostly correspond to worksheets) to which that user has access. We filter the list of all available menus down to the accessible menus, and then populate the JMenuBar as follows.
public void configure(JMenuBar menuBar, List<ZMenu> menuBeanList) {
menuBar.removeAll();
for (ZMenu parentMenuBean : getMenuBeanList(menuBeanList, null)) {
JMenu topMenu = createMenu(menuBeanList, parentMenuBean);
topMenu.setIcon(null);
if (topMenu.getMenuComponentCount() > 0) {
menuBar.add(topMenu);
}
}
menuBar.repaint();
}
protected List<ZMenu> getMenuBeanList(List<ZMenu> menuBeanList, ZMenu parentMenuBean) {
List<ZMenu> list = new ArrayList();
for (ZMenu menuBean : menuBeanList) {
if (menuBean.getParentMenu() == parentMenuBean) {
list.add(menuBean);
}
}
return list;
}
protected JMenu createMenu(List<ZMenu> menuBeanList, ZMenu parentMenuBean) {
JMenu menu = createMenu(parentMenuBean);
for (ZMenu menuBean : getMenuBeanList(menuBeanList, parentMenuBean)) {
if (getMenuList(menuBeanList, menuBean).size() != 0) {
menu.add(createMenu(menuBeanList, menuBean));
} else {
menu.add(createMenuItem(menuBean));
}
}
return menu;
}
protected JMenuItem createMenuItem(ZMenu menuBean) {
GAction action = createMenuAction(menuBean);
JMenuItem menuItem = createMenuItem(action);
return menuItem;
}
where createMenu(ZMenu) creates a JMenu component from our ZMenu entity, similar to the above createMenuItem(ZMenu). We have an ordinal property on our ZMenu Jitem. If this is nonzero, then it indicates that we wish to include this item in our tool bar, using the ordinal value as its order in the tool bar.
public void configure(JToolBar toolBar, List<ZMenu> menuBeanList) {
toolBar.removeAll();
Map<Integer, ZMenu> menuBeanMap = new TreeMap();
for (ZMenu menuBean : menuBeanList) {
if (menuBean.getOrdinal() != 0) {
menuBeanMap.put(menuBean.getOrdinal(), menuBean);
}
}
for (ZMenu menuBean : menuBeanMap.values()) {
GAction action = createMenuAction(menuBean);
JButton button = createButton(action);
button.setText(null);
toolBar.add(button);
}
toolBar.repaint();
}
where createMenuAction() creates a Swing Action using the menu configuration.
We introduce an interface for worksheets, so that our framewarez can juggle them.
public interface ZWorksheet {
public void openWorksheet();
public boolean closeWorksheet();
public String getWorksheetLabel();
public GPanel getWorksheetPanel();
public String getWorksheetHelp();
}
The openWorksheet() method might request focus for some component, so we will invoke it after the worksheet tab is realised, eg. using SwingUtilities.invokeLater().
When the framewarez opens the worksheet, it gets the label for the tab using getWorksheetLabel(), and the content JPanel of the worksheet using getWorksheetPanel(). If the user selects the help menu item, our framewarez might get the help for the currently active worksheet using getWorksheetHelp(). This might be the actual HTML text to display, or it might be a bookmark into the help manual, which makes more sense.
We make all of our worksheets implement the above interface. Let's consider our ZProductWorksheet. In MVC-speak, this mashes our MVC three-ball into one class. Additionally, we reference POJO model objects, ie. instances of ZProductBean, to complete the picture. It would be better to split this class up, eg. put the table into ZProductTable and the form into ZProductForm. But we aint gonna worry about that right now.
public class ZProductWorksheet implements ZWorksheet, GActionListener, GFieldListener {
@WorksheetContextAnnotation()
ZWorksheetContext context = ZWorksheetContext.createWorksheetContext(this);
@WorksheetConfigurationAnnotation()
ZProductWorksheetConfiguration configuration = new ZProductWorksheetConfiguration();
@ComponentAnnotation(label = "Products")
GPanel worksheetPanel = context.createPanel();
@LayoutAnnotation(gridy = 0)
GToolBar worksheetToolBar = context.createToolBar(worksheetPanel);
@ComponentAnnotation(label = "Close worksheet")
GAction closeAction = context.createAction(worksheetToolBar);
... // more actions eg. new, find, save, undo
@LayoutAnnotation(gridy = 1)
GTabbedPane tabbedPane = context.createTopTabbedPane(worksheetPanel);
@ComponentAnnotation(label = "Products")
GTabPanel tableTabPanel = context.createTabPanel(tabbedPane);
@ComponentAnnotation(label = "Details")
GTabPanel formTabPanel = context.createTabPanel(tabbedPane);
@ComponentAnnotation()
GTable<ZProductBean> productForm = context.createTable(tableTabPanel, ZProductBean.class);
@ComponentAnnotation(label = "Product Id", width = 100)
GField productIdField = context.createField(productForm);
... // other columns
@LayoutAnnotation(spacer = Gbc.BOTH, flow = Gbc.HORIZONTAL)
GForm<ZProductBean> productForm = context.createForm(formTabPanel, ZProductBean.class);
@ComponentAnnotation(label = "Product Id", width = 100)
GTextField productIdColumn = context.createTextField(productForm);
...// other fields
List<ZProductBean> productBeanList = new ArrayList();
public ZProductWorksheet() {
context.configure(this);
productTable.setDataList(context.entityManager.product.getExtentEntityList());
productForm.setBean(productTable.getBeanList().get(0));
}
public void openWorksheet() {
productTable.requestFocusInWindow();
}
public boolean closeWorksheet() {
if (productForm.isChanged()) {
if (!context.showConfirmDialog(configuration.confirmCloseWithoutSave)) {
return false;
}
}
return true;
}
public GToolBar getWorksheetToolBar() {
return worksheetToolBar;
}
public GPanel getWorksheetPanel() {
return worksheetPanel;
}
public String getWorksheetHelp() {
return configuration.worksheetHelp;
}
public String getWorksheetLabel() {
return configuration.worksheetLabel;
}
public void actionPerformed(GActionEvent event) {
context.traceLogger.entering(event);
if (event.getAction().equals(newAction)) {
...
} else if (event.getAction().equals(findAction)) {
...
} else if (event.getAction().equals(saveAction)) {
...
} else if (event.getAction().equals(closeAction)) {
context.closeWorksheet(this);
} else if (event.getAction() == helpAction) {
helpActionPerformed();
} else {
context.traceLogger.warning(event);
}
setEnabled();
}
public void helpActionPerformed() {
context.framewarez.showHelp(getWorksheetHelp());
}
public void setEnabled() {
saveAction.setEnabled(productForm.isChanged());
undoAction.setEnabled(productForm.isChanged());
}
...
}
As you can see, we annotate everything, even if we dunno why we would want to (yet). Mmmm, Duff, mmmm, annotations... Note that we use our tabbed framewarez to display our help as a new tab in helpActionPerformed(). In general, we favour opening a tab rather than popping up a JDialog. For one thing, this allows the user to switch between tabs to remind themselves what they are doing. If we really need a modal dialog, then we will use one, but otherwise we go for tabs.
We distill translatable strings into a worksheet configuration class as follows.
public class ZProductWorksheetConfiguration {
String worksheetLabel = "Products";
String worksheetHelp =
"<b>Product catalogue maintenance worksheet</b> \n\n" +
"Use this worksheet to maintain our catalogue of beer products.";
String confirmCloseWithoutSave = "Close without saving changes?";
... // other messages, eg. exception messages, dialog messages
}
This class can be externalised to an XML file for customisation and translation. In addition to the above strings, our configuration bundle implicitly includes the configuration of our components, to override the defaults specified in the annotations, notably the labels and tooltips, eg. the tooltip for closeAction, tab label for tableTabPanel, column label for productIdColumn, et cetera. The configuration might be generated for a resource bundle as follows.
productWorksheet.message.worksheetLabel = Products
productWorksheet.message.confirmCloseWithoutSave = Close without saving changes?
productWorksheet.message.worksheetHelp = <b>Product catalogue maintenance worksheet</b> ...
productWorksheet.panel.worksheetPanel.label = Products
productWorksheet.action.closeAction.toolTip = Close worksheet
productWorksheet.tab.tableTabPanel.label = Products
productWorksheet.tab.formTabPanel.label = Details
productWorksheet.field.productIdField.label = Product Id
productWorksheet.column.productIdColumn.label = Product Id
Alternatively, its XML representation might look like the following.
<worksheetConfiguration name="productWorksheet">
<message name="worksheetLabel" value="Products"/>
<message name="confirmCloseWithoutSave" value="Close without saving changes?"/>
<message name="worksheetHelp">
<b>Product catalogue maintenance worksheet</b>
Use this worksheet to maintain our catalogue of beer products.
</message>
<panel name="worksheetPanel" label="Products"/>
<action name="closeAction" toolTip="Close worksheet"/>
<tab name="tableTabPanel" label="Products"/>
<tab name="formTabPanel" label="Details"/>
<field name="productIdField" label="Product Id"/>
<column name="productIdColumn" label="Product Id"/>
</worksheetConfiguration>
In our ZWorksheetContext.configure() method, we would parse the above into a list of MessageConfiguration, ActionConfiguration, PanelConfiguration, TabConfiguration, FieldConfiguration and ColumnConfiguration objects. And then apply these configurations to our ZProductWorksheetConfiguration instance, and the components of our ZProductWorksheet, to override the defaults eg. as specified in annotations.
Our framewarez is implemented as follows. For starters, we show a login tab. Once the user has logged in, we configure the menu bar appropriately according to the user's access permissions. Then the user can start launching worksheets.
public class ZAccessFrame implements GFieldListener, ActionListener, GTableListener {
@WorksheetContextAnnotation()
ZWorksheetContext context = ZWorksheetContext.createWorksheetContext(this);
@WorksheetConfigurationAnnotation()
ZAccessFrameConfiguration configuration = new ZAccessFrameConfiguration();
@LayoutAnnotation()
GFrame mainFrame = context.createFrame();
@LayoutAnnotation()
GMenuBar mainMenuBar = context.createMenuBar(mainFrame);
@LayoutAnnotation(gridy = 0)
GToolBar mainToolBar = context.createToolBar();
@LayoutAnnotation(gridy = 1)
GTabbedPane mainTabbedPane = context.createBottomTabbedPane(mainPanel);
@LayoutAnnotation()
GPanel textPanel = context.createPanel();
@LayoutAnnotation(top = 20, gridy = 2)
GTextPane textPane = context.createTextPane(textPanel);
@LayoutAnnotation()
@ComponentAnnotation(label = "Login")
GPanel loginPanel = context.createPanel();
@LayoutAnnotation(gridy = 1)
GForm loginForm = context.createForm(loginPanel, ZLoginBean.class);
@LayoutAnnotation(gridx = 0, width = 100)
@ComponentAnnotation(label = "Username")
GTextField usernameField = context.createTextField(loginForm);
@LayoutAnnotation(gridx = 1, width = 100)
@ComponentAnnotation(label = "Password")
GPasswordField passwordField = context.createPasswordField(loginForm);
@LayoutAnnotation(gridx = 2)
GAction loginAction = context.createButton(loginForm);
ZLoginBean loginBean = new ZLoginBean(this);
public ZAccessFrame() {
context.configure(this);
loginForm.setBean(loginBean);
setEnabled();
}
public void fieldChanged(GFieldEvent event) {
context.fieldLogger.entering(event);
if (event.getField() == usernameField) {
usernameChanged();
} else if (event.getField() == passwordField) {
passwordChanged();
}
setEnabled();
}
protected void usernameChanged() {
loginBean.validateUsername();
passwordField.setEnabled(true);
passwordField.requestFocusInWindow();
}
protected void passwordChanged() {
loginForm.getBean();
loginBean.validate();
loginAction.setEnabled(true);
loginAction.requestFocusInWindow();
}
protected void setEnabled() {
loginAction.setEnabled(loginBean.validate());
}
public void actionPerformed(ActionEvent event) {
ZMenu menu = context.entityManager.menu.getNullableEntityBean(event.getActionCommand());
context.actionLogger.entering(event, menu);
if (menu != null && menu.getWorksheetClass() != null) {
openWorksheet(menu.getWorksheetClass());
} else if (loginAction.isSource(event)) {
loginActionPerformed();
} else if (context.accessData.systemLogout.isSource(event)) {
logoutActionPerformed();
} else if (context.accessData.systemExit.isSource(event)) {
exitActionPerformed();
} else if (context.accessData.helpOnlineHelp.isSource(event)) {
helpActionPerformed();
} else if (context.accessData.helpAbout.isSource(event)) {
aboutActionPerformed();
} else {
context.traceLogger.warning(event);
}
setEnabled();
}
public void loginActionPerformed() {
loginForm.getBean();
loginBean.validate();
loginUser();
}
protected void loginUser() {
context.setUser(loginBean.getUser());
showMenu();
}
protected void showMenu() {
context.configure(mainMenuBar, context.getUser());
context.configure(mainToolBar, context.getUser());
mainMenuBar.requestFocusInWindow();
}
protected void logoutActionPerformed() {
context.setUser(null);
loginBean = new ZLoginBean(this);
loginForm.setBean(loginBean);
mainTabbedPane.removeAll();
mainTabbedPane.addTab(loginPanel);
username.requestFocusInWindow();
}
protected void openWorksheet(Class worksheetClass) {
try {
ZWorksheet worksheet = (ZWorksheet) worksheetClass.newInstance();
openWorksheet(worksheet);
} catch (Exception e) {
throw new GWrappedRuntimeException(context, e, configuration.openWorksheetError, worksheetClass);
}
}
protected void openWorksheet(final ZWorksheet worksheet) {
String tabName = worksheet.getWorksheetLabel();
int index = mainTabbedPane.indexOfTab(tabName);
if (mainTabbedPane.indexOfTab(tabName) >= 0) {
mainTabbedPane.setSelectedIndex(mainTabbedPane.indexOfTab(tabName));
return;
}
JPanel worksheetPanel = worksheet.getWorksheetPanel();
mainTabbedPane.addTab(tabName, worksheetPanel);
mainTabbedPane.setSelectedComponent(worksheetPanel);
worksheet.openWorksheet();
}
public void closeWorksheet(ZWorksheet worksheet) {
if (worksheet.closeWorksheet()) {
mainTabbedPane.remove(worksheet.getWorksheetPanel());
}
}
...
}
Not rocket science, just framewarez, so... A handy trick is to enable automatic login, eg. via -DautoLoginUser=test. Then in development mode, we can press F6 to compile and run the framewarez, and straight-away click on the toolbar icon for the worksheet we are working on, to preview and test it in a tight loop without the niggle of logging in a million times a day. For this reason if nothing else, we need to minimise our application startup time, eg. by using lazy initialisation, and also not overriding our default configuration from externalised configuration files and preferences, eg. via -DsuppressExternalisedConfiguration=true
We present a design of a JFrame with a JTabbedPane for worksheets. We install a JMenuBar on the JFrame for launching worksheets. Menu items are configured in the application, and overridden with the externalised configuration, eg. using JAXB2 to persist the configuration to an XML file. This enables translation and customisation.
This design is implemented in aptframework.dev.java.net, minus some refinements presented here. If you wish to dive deeper into some undocumented non-sugar-coated code, you can look there. Here's that Web Start demo again...
The next article in this Swing and Roundabouts series might be Lookup Hookup looking at handling database lookups for fields and columns, using this framewarez. Before that, two upcoming articles in the works are Trip and Tick 2: JooJ up your project page with a Web Start demo and Plumber's Hack 1: Blog o' warez about some softwarez i'm writing to help me write technical articles like this.
Before that, you can read Trip and Tick 1: Checking out a java.net project
and The sharp end of the stick with some discussion
about languages, platforms, and what-not.
The sharp end of the stickPosted by evanx on July 12, 2006 at 01:03 AM | Permalink | Comments (17)This morning i stumbled across Dan Cresswell's "Java is not a programming language" where he mentions Java being "blind-sided" by Ruby. I got me to musing... The Java language is a general purpose programming platform, on which one can build RoR'esque and PHP'esque solutions, via libraries, frameworks, or whatever. People are naturally trying to invent easier reusable ways of solving specific classes of problems. (Often at the expense of their current employer, as talked about in Bruce Eckel's "When Reuse Goes Bad".) PHP did a fantastic job of making the transition from editing static HTML pages, to building dynamic ones, as smooth and painless as possible, and accessible to non-programmers, who quickly became highly effective programmers. And RoR hits the database web frontend nail on the head, from what i understand. Java frameworks will borrow and follow RoR. But we want the Java community to innovate and lead. Or do we? Surely there is real value in being a conversative language, that evolves at a carefully controlled pedestrian pace. Because enterprises like that. The larger the community is, the more conversative it's gonna be. Like a huge committee, innit. In a way, a happy victim of it's own success. So I say let's follow and borrow. Let C# et al innovate for the future, so that Java can innovate for the present, learning from the best of the rest, as much if not more from their mistakes as their successes. C# did it to us, now we can do it back to them. Hee hee. Trip and Tick 1: Checking out a java.net project using NetbeansPosted by evanx on July 07, 2006 at 08:59 AM | Permalink | Comments (5)I got an email on Friday from a chap in Italy asking where he could find a document on how to run aptframework in Netbeans, which is like pressing not one but two or three, of my JButtons simultaneously, and which has led to me to writing this blog article. I'm sure he'll be watching Italy in the World Cup Final this weekend, rather than doing anything else, but anyway here goes. Let's say you hear about a project that is hosted on java.net, and you go to its home page eg. http://aptframework.dev.java.net. You'll see something like the following.
With any luck there'll be a link to some screenshots, which the project owner might have uploaded into "Documents & files" under the "Project Tools" tab. That's very nice, but you'll quickly discover that projects on java.net are all about the code. Happily, you can browse the source in the CVS using "Version control - CVS". Some projects put a Webstart "Launch" button on the home page. Supoib! (Catch the episode when Bart Simpson works for the mafia, to see how to pronounce "superb" as "supoib" heh heh.) I really must read up on JNLP and do the same... see Trip and Tick 2! If you aren't a member of java.net, well that's very easily rectified. Just click on Register at the top right of the above page to get the following.
Now in Netbeans, you go to the CVS menu, and choose "Checkout..." and specify the following CVS root, with your own java.net username and password of course.
Then you specify the project name as the module, and the directory where you want to check out projects to, eg. to your home/projects directory.
The default suggestion is $HOME/nbprojects or something like that. In Windows, that would be C:\Documents and Settings\USER\nbprojects. Personally i like to keep my CVS code, and Netbeans project file directory separate. So i use a projects subdirectory for the code checked out from CVS, using Netbeans or some other CVS client. And later we can specify a separate nbprojects subdirectory for the Netbeans project files, when we create the project after the code has been checked out. Alternative directory names might be cvsprojects for the code, and netbeans for the Netbeans project files. In UNIX these would be off your home directory, and in Windows in your Documents and Settings. Incidently, if you also use Eclipse, then your code will be in your Eclipse workspace directory, and your Netbeans project files in nbprojects. And you can happily switch between Eclipse and Netbeans in this case, eg. to use Netbeans' Mattise or Profiler. Back to the game. It might take Netbeans a minute or two to check out all the sources into the chosen directory. Then Netbeans prompts us to create a project from the checked out sources. This is just like prompting us to select "New Project..." in the File menu.
We give the project name, and our netbeans project folder for this project, eg. off nbprojects.
We specify our "existing sources" as the src directory we have just checked out, as follows. We can ignore any other directories, eg. www is the project home page, and nbprojects is a Netbeans project directory, maybe checked in by mistake long ago, in a land far far away...
Finally, we select the class to run in the project properties. We right-click on the project name in the Projects window, and choose Properties, the last item in the project menu, and the following dialog pops up.
Our source folder has already been selected, so nothing to do here. We click on the Run category, and select the main class to run, as follows. In the case of the aptframework demo, it's ZViewContext.
Finally we press F6 to compile and run! Actually this will compile and run the currently flagged "Main Project" of those projects you have open in the Projects window. You right-click on the project and select the "Set Main Project" to flag that project as such. The Main Project name then goes bold. Otherwise you can right-click and select "Run Project" to run a project which is not flagged as the Main Project.
In the case of aptframework, the demo will popup as follows.
The next article in this series will be "Trip and Tick 2: JooJ up your project page with Netbeans' JNLP tool." Update: You can check out the demo without checking out the code using the following Web Starter :) | ||
|
|