The Source for Java Technology Collaboration
User: Password:



Evan Summers's Blog

July 2006 Archives


Swing and Roundabouts 4: Grid Bag Grease

Posted by evanx on July 27, 2006 at 06:02 AM | Permalink | Comments (3)

layout1_200.jpg Ethan Nicholas blogged recently on "Reinventing GridBagLayout", which enjoyed many comments regarding simplifying GBL, and problems with GBL. Karsten Lentzsch notes that "SpringLayout is very powerful, ExplicitLayout is powerful, FormLayout is quite powerful, HIGLayout is quite powerful, GBL is weak."  He compares layout managers at the end of his Forms whitepaper.

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 :)

sourcyShot.png

Here is the launcher for the above.

http://weblogs.java.net/blog/evanx/archive/webstart.small.gif (Sourcy, Java5, 195k, unsandboxed)

The screenshot below, of the source of the above, shows Gbc.java in action.

sourcyFrame.png

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 Plugin

Posted 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.

  • 1Mb download. Splitting the JRE into modular downloads, eg. 1Mb for the core (excluding stuff like JNDI et cetera, that is seldom used by applets), and other modules could be loaded on demand.
  • Instant startup. Ethan says that every usability study reveals that users are impatient, "it's a fact of life, and we as software developers need to live with it."

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.

stopwatch6.jpg The issue of start-up time is an interesting one, because this has to be endured for every applet, every time it's run, so i reckon this is the crux that needs to be optimised. This engineering problem is addressed in operating systems, to speed up boot time. Maybe Sun's Solaris guys could show those tricks to the ClassLoader.

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 demo

Posted 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.

nbupdate.png

When in doubt press the OK button. Unfortunately the above screen does not have an OK button, so we try the Next button.

nbupdate2.png

We add the Netbeans Module for Java Web Start.

nbupdate3.png

I think we should meet and greet this module!

nbupdate4.png

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!

aptws.png

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.

wsconsole.png

Trying again, there is a different error, ie. progress, woohoo!

wsconsole2.png

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.

jnlp-security-all.png

But we get a "jar not signed" JNLP exception, as we see when we click on Details and then the Exception tab.

not-signed-error.png

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.

signjar.png

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.

* I found that the weblogs.java.net webserver transforms XML files somehow, like our JNLP file, so that Firefox displays the XML rather than launching Web Start. So we upload our JNLP file and our jar to jroller.com rather, because that seems to work.

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

webstart.small.gif (695k, unsandboxed, Java5)

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 sWing

Posted 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: Framewarez

Posted by evanx on July 13, 2006 at 06:29 AM | Permalink | Comments (0)


Prequels
"The People can be depended upon to meet any national crisis. Just bring them the real facts, and beer." Abraham Lincoln

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.)

gold3_crop.jpg Disclaimer: As always, this is a "noisy" article (because i like writing noisy articles and i wrote this article, so...), but luckily the noise is clearly demarcated using italicalised text this time. So please feel free to skip such text.


Introduction
"Beer is proof that God loves us and wants us to be happy." Benjamin Franklin

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.


Worksheet Framewarez
"Beer... Now there's a temporary solution." Homer Simpson

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.

menu.png

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.

webstart.small.gif (695k, unsandboxed, Java5)


Defining the Menus
"Without question, the greatest invention in the history of mankind is beer. The wheel does not go nearly as well with pizza." Dave Barry

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.


Leveraging the IDE for rapid prototyping
"Homer no function well without beer." Homer Simpson

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.


Externalising the Menu Configuration
"What I like to drink most is beer that belongs to others." Diogenes.

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.

bottle_150b.jpg Similarly with SQL, OQL et al, as addressed in Bean Curd 2: The SQL. And Bean Curd 2X will look at mapping XML to Java for the purposes of programming XML queries in Java rather than XQuery. Anyway, i expect that Dolphin will introduce querying into the language, in response to LINQ.


Users and roles
"All right, brain, let's back to killing you with beer." Homer Simpson

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.


Menu Bar Configurator
"I have feelings too - like I want beer, or I'm going crazy." Homer Simpson

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.


Worksheet Cookie Puncher
"Beer needs sports, and sports needs beer - it has always been thus." Peter Richmond

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().

beer5_250b.jpg The closeWorksheet() method confirms that the worksheet can be closed, eg. asks if the user wishes to save changes, if there are any.

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.


One of Many Worksheets
"Work is the curse of the drinking class." Oscar Wilde

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.


Worksheet Configuration Class
"Beer, the cause and solution to all of life's problems." Homer Simpson

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.


Framewarez
"Without hydrogen and oxygen, there would be no water, a vital ingredient in beer." Dave Barry.

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.

login.png

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


Conclusion
"The answers aren't at the bottom of a beer bottle, they're on TV!" Homer Simpson.

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.

leffe.jpg Access control to the menu items is based on user roles. This is persisted to the database, and maintained by the administrative user using worksheets, ie. for users and user roles, and the corresponding menu access control lists.

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...

webstart.small.gif (695k, unsandboxed, Java5)


Coming up
"I can resist everything except temptation." Oscar Wilde

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 stick

Posted 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 Netbeans

Posted 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.

apthome2.png

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.

jnregister2.png

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.

aptcheckout.png

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.

aptmodule2.png

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.

newproject-existing-source.png

We give the project name, and our netbeans project folder for this project, eg. off nbprojects.

newproject-name.png

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...

newproject-addsourcefolder.png

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.

properties-sources.png

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.

properties-run.png

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.

aptrun2.png

In the case of aptframework, the demo will popup as follows.

aptlogin.png

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 :)

webstart.small.gif (695k)



Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds