Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Saturday, December 3, 2011

PunchClock 0.2.5 Released, JFace lessons

I finally got to a point where I thought enough of the nagging items with PunchClock were resolved that I would take the time to publish a new release. I am pretty proud of this release. There are no major new features but a lot of work went into making the code better, improving the infrastructure to make it a more rounded, solid product. For details on what was done, see  the PunchClock 0.2.5 release announcement.

 I've been personally using PunchClock versions daily to keep track of time at my work, I think I've pretty much gotten it to a stable, usable point. I don't see much new work going into it, unless I have some inspiration.


A couple of interesting lessons learned I think I'll share.

Utilizing JFace's built in ImageRegistry
I needed to add a custom error message to my Edit Session dialog. I did this using two labels. One that displays an icon (info, warning, error) and the other will have the message text. I tried using Label.setImage() for the label with the message text, however, the image "takes over" and no text is displayed, so I needed to keep them separate.

I know JFace must have the icons somewhere as they are used by the WizardPage status messages. It turns out they are available in the org.eclipse.jface.resource.JFaceResources.getImage() utility method. The icon images are available as constants of the org.eclipse.jface.dialogs.Dialog class.

Here is the setMessage() method from my custom EditSessionDialog class, it is called by a Listener that is attached to all the widgets in the form, listening on any change.

    /**
     * Updates the status message of this dialog with errors or other
     * information to display to the user.
     *
     * @param messageText
     *            - text to display
     * @param status
     *            - one of IMessageProvider constants
     */
    public void setMessage(String messageText, int status) {
        _logger.fine(messageText + ", status: " + status);
        Image messageImage = null;
        // based on status, display appropriate icon
        switch (status) {
        case IMessageProvider.INFORMATION:
            messageImage = JFaceResources.getImage(Dialog.DLG_IMG_MESSAGE_INFO);
            break;
        case IMessageProvider.WARNING:
            messageImage = JFaceResources
                    .getImage(Dialog.DLG_IMG_MESSAGE_WARNING);
            break;
        case IMessageProvider.ERROR:
            messageImage = JFaceResources
                    .getImage(Dialog.DLG_IMG_MESSAGE_ERROR);
            break;
        case IMessageProvider.NONE:
        default:
            messageImage = null;
            break;

        }
        // update the labels with new values
        _messageLabel.setText(messageText);
        _messageIcon.setImage(messageImage);
        _messageIcon.redraw();
        // resize widgets in dialog
        _messageIcon.pack(true);
        _messageLabel.pack(true);
    }



JFace ErrorDialog
As I transitioned all of the code in PunchClock away from writing out direct to the console, and instead used java.util.logging API, it became apparent that the way that exceptions were being handled was inconsistent, and in fact exceptions were getting thrown that were never handled. It seems that if there is no ExceptionHandler defined for the Window class, the exceptions don't get shown in the UI, though the program continues to run, leading to some unexpected results, such as clicking on a button and nothing seeming to happen.

So first step was to create a class that implements IExceptionHandler and then set it as the ExceptionHandler for my main window, using Window.setExceptionHandler(). This lets me catch all Throwables thrown and not caught in the PunchClock code.

Then I did find that JFace conveniently has a prebuilt dialog for displaying errors to the UI, which before I was able to do in one off cases using a custom built SWT dialog function. The example I took my queue from was here http://www.java2s.com/Code/Java/SWT-JFace-Eclipse/JFacesErrorDialogclass.htm

My example handleException method in my ExceptionHandler

/* (non-Javadoc)
     * @see org.eclipse.jface.window.Window.IExceptionHandler#handleException(java.lang.Throwable)
     */
    @Override
    public void handleException(Throwable arg0) {
        _logger.log(Level.SEVERE, "Uncaught exception from UI", arg0);
        // Create the required Status object
        Status status = new Status(IStatus.ERROR, "PunchClock", 0,
            "Unhandled Exception "+arg0.getMessage(), arg0);

        // Display the dialog
        ErrorDialog.openError(Display.getCurrent().getActiveShell(),
            "Error Occured", "Program Error. See PunchClock.log for more details.", status);
    }

It wasn't too clear what all these parameters do in terms of where they get used in the ErrorDialog. Here is an example of the error dialog produced by above code with a NullPointerException
The title of the dialog, and the first message line is from the openError() method, second and third parameters respectively, the fourth parameter, the Status object, controls the "Reason:" message, and if you include the actual Throwable in the status it gets displayed in the expandable Details box, although that is not required, you can just pass null.

Editor's note: stay tuned for updates to the punchclock wiki and FAQ pages as I round out documentation with all the updates from the latest releases.


Saturday, September 24, 2011

PunchClock 0.2.3 Released

After putting PunchClock on hold to work on C coding this past summer. I've resumed work and completed a few features for PunchClock. Mainly these greatly help me in my own job, they probably could be useful for others.

Download PunchClock 0.2.3

Tour of whats new in PunchClock 0.2.3
  •  Added new feature: Stats widget in UI 
    • Tracks how long you are spending on the current task.
    • Keeps a running total of all tasks for that calendar day: "have I billed 8 hours to my client today?"
  • Brand new Export feature
    • File > Export launches a new wizard to export a file to Excel
    • Selectable Date Range
    • Excel 97-2003 format (.xls), compatible with OpenOffice, GoogleDocs
    • Exported Excel provides automatic summations of tasks for each day by project. Color coded by day for easier review.
    • Uses Apache POI library.
  • Fixed a bug on exit that wouldn't properly save the end time for a current session.
  • Behind the scenes all the UI has now been reimplemented using JFace
  • Fixed a few other dings and scratches.

I skipped a formal release of 0.2.2 as there wasn't much new for the end user, that release mainly dealt with bug fixing and refactoring for JFace.

I am getting close to finishing what I wanted to achieve now with PunchClock, It will now be something that will be an accelerator at my own job and I will try to share it with my team. There are a few things in the UI that I would like to tighten up. For example the layout changes a little too much when the main window is resized. I could add a few more options to the Export wizard. I currently don't have support .xlsx format output. Maybe when I get around to these last few areas I would be comfortable with a version 1.0 release.

Please let me know if you have any feedback or comments.

Friday, July 22, 2011

On Configuring Automated Builds on an Amazon EC2 micro-instance using Jenkins, SVN and Apache

I am a big fan of the continuous integration approach with using tools and build scripts to automate the build process for programs. Combine this with comprehensive unit testing and static analysis provides some easy ways to improve the quality of code.

So I wanted to implement a continuous integration set up, partly as a way to improve my infrastructure around the PunchClock project, as well as a chance to explore some new technologies, especially Amazon Web Services.

What I settled on was to create a "build machine" using Jenkins CI, formerly Hudson, an open source program specifically for automating building software in a continuous integration environment. On the same server I would also host my Subversion repository (SVN), and being that it is an Amazon EC2 instance I have less worry of a possibility of a catastrophic loss of data from my current Ubuntu box at home. Both SVN and the Jenkins web application would sit behind an Apache2 Web Server. All of this would be on a Debian Linux distro running as an EC2 instance. (I chose to go with Debian purely to gain exposure to other distros besides Ubuntu). I learned a lot from this set up, the AWS/EC2 was actually more challenging than you would normally find from a hosted server, but their AWS Management Console allows you to do most operations pretty easily.


Prereqs
  1. Install Apache2 if not present
    On the default Debian install Apache is present, but on the Debian EC2 AMI I used it wasn't installed.
    apt-get install apache2
  2. Install Java 1.6 (update 25)
    You'll need to edit your /etc/apt/sources.list see http://jayraj87.blogspot.com/2010/02/installing-sun-java6-jdk-in-debian.html
  3. Install Ant 1.8.0
    • installs version 1.8.0 to /usr/bin/ant (/usr/share/ant)
      apt-get install ant
    • copy junit.jar to ANT_HOME\lib (junit 3.8.2 or later download here)
  4. Install SVN and Repository
  5. Before you begin I assume you have your own working ANT script that would compile, run JUnit tests, run any static analysis utilities (e.g. FindBugs or CheckStyle) and package your project into artifacts for distribution. The PunchClock application had an existing ANT build.xml that I started from.

Installation

All of these steps I honed on a default Debian install. I tested this on VMWare Player running the 6.0.1a "Squeeze" Debian install, from an iso file found on BitTorrent. Once I had got the steps down, basically the same applied to installing on the Debian instance on Amazon EC2 except for a few issues. All of these steps were performed as root unless otherwise noted.
  1. Install Jenkins LTS

    • wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
      • Add site to debian packages /etc/apt/sources.list: deb http://pkg.jenkins-ci.org/debian binary/
      apt-get update
      apt-get install jenkins

    • Installs at http://localhost:8080/ by default
    • Followed steps to configure Jenkins to run behind Apache  https://wiki.jenkins-ci.org/display/JENKINS/Running+Jenkins+behind+Apache
      • configuration is in /etc/default/jenkins
      • I changed default port from 8080 for added security
      • add HTTP_HOST=127.0.0.1 #only takes requests from apache2
      • append to JENKINS_ARGS="... --prefix=/jenkins httpListenAddress=$HTTP_HOST"
    • To restart after config changes:
      /etc/init.d/jenkins force-reload
    • Log file will be placed in /var/log/jenkins/jenkins.log. Check this file if you are troubleshooting Jenkins.

  2. Setting up apache for Jenkins
    Apache will be a reverse proxy to the Jenkins web application. We've already set up Jenkins to only accept requests from Apache (localhost), we will also add basic authentication. See also (https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu)
    • Create or copy this file called "jenkins" to /etc/apache2/sites-available
      <VirtualHost *:80>
      ServerAdmin your.email@yourorganization.com
      ServerName your.ip.here.goes.here
      ServerAlias debian-build
      ProxyRequests Off
      <Proxy *>
      Order deny,allow
      Allow from all
      </Proxy>
      ProxyPreserveHost on
      ProxyPass /jenkins http://localhost:8089/jenkins
      ProxyPassReverse /jenkins http://localhost:8089/jenkins
      <Location /jenkins/>
      AuthType basic
      AuthName "Jenkins"
      AuthUserFile "/etc/apache2/sites-available/jenkins.htpasswd"
      Require valid-user
      </Location>
    • Run the following commands to set up Apache for virtual hosts and create the site "/jenkins"
      a2enmod proxy
      a2enmod proxy_http
      a2enmod vhost_alias
      a2ensite jenkins
      apache2ctl restart

    • Note: apache logs under /var/log/apache/error.log
    • I am no apache security configuration expert, but following this article was useful for configuring basic authentication for now. https://wiki.jenkins-ci.org/display/JENKINS/Apache+frontend+for+security
      • E.g in my case it was something like this: htpasswd -cm /etc/apache2/sites-available/jenkins.htpasswd your-username
      • In my opinion security is often overlooked in small scale projects, other general, but useful Apache security tips: http://httpd.apache.org/docs/2.0/misc/security_tips.html
    • After all of this you should be able to access Jenkins with the specified username/password at a URL like
      • http://your.ip.here.goes.here/jenkins

  3. Install checkstyle
    For CheckStyle analysis of Java code (assuming you've already configured your project and build.xml to run CheckStyle during the build)
    • Download checkstyle-5.3-bin.tar.gz (at the time of this post 5.4 is available)
    • Copy to somewhere on the server e.g. /usr/share
    • Extract the tar
      $/usr/share# tar zxf check checkstyle-5.3-bin.tar.gz
    • Log in to Jenkins and navigate to Manage Jenkins > Configure System
      • Check "Environment Variables" under Global Properties
      • Add name = "CHECKSTYLE_HOME" value = "/usr/share/checkstyle-5.3"
  4. Install findbugs
    Similarly for FindBugs analysis of Java code. (* I couldn't actually get this to successfully run on the EC2 Micro instance, it appeared to make the server run out of memory and the Jenkins build process would crash.)
    • Download findbugs-1.3.9.tar.gz
    • Copy to somewhere on the server e.g. /usr/share
    • Extract the tar
      $/usr/share# tar zxf check findbugs-1.3.9.tar.gz
    • Log in to Jenkins and navigate to Manage Jenkins > Configure System
      • For "Environment Variables" under Global Properties
      • Add name = "FINDBUGS_HOME" value = "/usr/share/findbugs-1.3.9"
    • Copy the findbugs_ant.jar to ANT_HOME/lib
      • $/usr/share/findbugs-1.3.9/lib# cp findbugs-ant.jar /usr/share/ant/lib
  5. Manage Jenkins plugins for Static Analysis
    • Log in to and navigate to Jenkins > Manage > Available >
    • Check the following plugins
      • Static Analysis Utilities
      • Checkstyle Plug-in
      • FindBugs Plug-in
      • etc...
    • Jenkins may need to be restarted manually using /etc/init.d/jenkins force-reload

  6. Configure Jenkins project: PunchClock
    • Log in to Jenkins and click New Jobs from the left menu. Fill out the following form. The details will vary per your project and needs.
    • Job Name: PunchClock
    • Set repostitory to Subversion
      • URL: http://ip.of.my.svn.repo/svn/trunk/TimeTrack
      • Will fail, need to enter credentials
    • Add Build Step > Invoke Ant
    • Check publishing options
      • Publish Checkstyle analysis results
      • Publish FindBugs analysis results
      • Publish combined analysis results
      • Publish JUnit test result report
        • My Test Result XMLs use the pattern "TEST*.xml"
      • Archive the artifacts
        • This depends on what artifacts your build.xml creates, my PunchClock complete packages are zipped in the distributions folder of my work space so: Files to archive: "distributions/*.*"
    • Optional you can set an automated checkout strategy. Under Build Triggers I chose "Poll SCM"
      • For schedule I used "@hourly"

Amazon EC2 Configuration

This is a whole other technology, which deserves its own post. I'll touch on what I did find in the end. I was motivated to look into this, Amazon AWS recently began offering the ability to have a single "micro" sized instance hosted for free. This gets you among other things, a virtual machine with 620 MB of memory, a single "virtual cpu", about up to 10GB EBS storage space, the OS of your choice* and a certain monthly bandwidth. I haven't been on the service a month yet, so we'll see how I get billed.

*The idea with these instances is that they are virtual machines that you can start from a pre-packaged snapshot referred to as an AMI. Amazon provides some (of their own linux flavor) but most appear to be community created public images for different OS's. I found a lot of information around this whole topic, as well as really well done, standardized AMI's from http://alestic.com/, which focuses mostly on Ubuntu images. The EC2StartersGuide for Ubuntu is also a great starting resource, especially for managing and configuring your EC2 instances from a Ubuntu desktop. In my case with Debian, the ec2debian google group was also a good place to start.

Long story short, when you sign up for AWS and log in to the Management Console, go to "Instances" and click on Launch Instance, you can search for an select a starting AMI. I went wit this one from the ec2debian group

us-east-1
i386 EBS: ami-0ce41865
x86_64 EBS: ami-80e915e9
i386 8gb instance: ami-c041bda9
x86_64 8gb instance: ami-1cbc4375


The distinction between instance and EBS is that instance root volume apparently doesn't survive a stoppage of the VM, not sure the benefit of that other than to keep things clean? I went with the x86_64 EBS: ami-80e915e9 after a false start with the 32 bit version.

Note these all start out with a 1GB Root volume. Needs to be reized.

After booting up, and figuring out how to connect to the instance (only can use ssh, scp...be prepared to use vi!).

Some issues I hit:
Java 6 won't install on 32-bit "micro" instance
Originally I tried a 32 bit debian instance AMI. Hit this bug everytime I installed sun-java6-jdk: https://bugs.launchpad.net/ubuntu/+source/linux-ec2/+bug/634487 which basically caused the whole instance to hang due to 100% CPU usage from a runaway install process. The only fix was to use a 64-bit AMI.

Enlarging EBS Root Volume
After finally installing everything and pressing the final key-stroke to start Jenkins, I was severly disappointed to only get a 503: Service Unavailable error from Apache. The Jenkins log wasn't created even and the Apache logs just stated that it couldn't connect to the Jenkins site. After banging my head against the wall, I found a not enough disk space error in the /var/logs/daemon log. It turned out I maxed the 1GB initial volume size. Unfortunately EBS volumes can't just be "dialed up" like you can do with a VMWare Player VM. Some guidance can be found here on how to resize a root EBS volume:  http://alestic.com/2010/02/ec2-resize-running-ebs-root

Basically what I ended up doing using the AWS Management Console
  1. Create EBS snapshot
  2. Stop Instance
  3. Detach old volume
  4. create new volume from snapshot
  5. Attach new volume (as /dev/sda1...same as old volume!)
  6. Start Instance
  7. resize2fs /dev/xvda1 (Debian is tricky that it doesn't list the volume as /dev/sda1). It did an online resize in about 5 minutes and I was good to go.

Sunday, June 19, 2011

Converting SWT to JFace

It has been a slow couple of weeks for my coding. I've had a lot of activities going on which have kept me away. Today I focused on making a major architectural change to PunchClock by reimplementing the view layer of PunchClock to completely use the JFace pattern instead of the pure SWT that was mostly there.

To do this I had to rewrite all of my custom SWT dialog shell code using various subclasses of the JFace Dialog class. All of my menu bar was replaced using a set of JFace MenuManagers with related Actions. Finally the main application Shell was implemented through a new class that extends ApplicationWindow and acts as the "central glue" for the new JFace implementation. As always the tutorials by Adrian Emmenis, "Using the Eclipse GUI outside the Eclipse Workbench", were my main guide in this process although was also able to use the SWT/JFace javadocs available in the Eclipse Platform API Specification reference site.

The purported benefits of using JFace is that it "is a UI toolkit that provides helper classes for developing UI features that can be tedious to implement". So one would hope that this could be quantified in terms of fewer lines of code, easier readability and/or maintainability. As PunchClock 0.2.1 is a fairly stable, simple application, using a couple of JFace dialogs, but not depending on it too much, I was hoping to see evidence of a benefit by switching over. To be honest, the experience today felt like rearranging deck chairs on a cruise ship. Generally what I needed to do was move methods around from a class that previously managed most of the SWT UI interactions and updates and put these chunks of code into other more object oriented classes, such as Dialogs, ApplicationWindow or Actions. A lot of my EventListeners, for menu items for example were easily converted to JFace Actions with a couple of constructor changes and converting the implementation of SelectionListener.widgetSelected() method to override the Action.run() method. For example:
Before old SWT code with Listener for the "Open" menu item

/**
* @param shell
*/
public OpenAction(Shell shell, TimeTrackController controller, DialogSettings settings) {
_shell = shell;
_controller = controller;
_settings = settings;
}

/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.SelectionAdapter#widgetSelected
* (org.eclipse.swt.events.SelectionEvent)
*/
public void widgetSelected(SelectionEvent e) {

// create new dialog where user enters the project name
final Shell dialog = new Shell(_shell, SWT.DIALOG_TRIM
| SWT.APPLICATION_MODAL);
dialog.setText("Open Project");
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = 10;
formLayout.marginHeight = 10;
formLayout.spacing = 10;
dialog.setLayout(formLayout);
// position near parent window location
dialog.setLocation(_shell.getLocation().x + 10,
_shell.getLocation().y + 10);
// text displayed in dialog
Label openProjectLabel = new Label(dialog, SWT.NONE);
openProjectLabel.setText("Type a Project name:");
FormData data = new FormData();
openProjectLabel.setLayoutData(data);

//for combo box
String[] options = new String[]{};
try{
List availableProjects = _controller.getProjects();
options = new String[availableProjects.size()];
availableProjects.toArray(options);
}catch(IOException ioe){
ioe.printStackTrace();
}


//creates cancel button
Button cancel = new Button(dialog, SWT.PUSH);
cancel.setText("Cancel");
data = new FormData();
data.width = 60;
data.right = new FormAttachment(100, 0);
data.bottom = new FormAttachment(100, 0);
cancel.setLayoutData(data);
cancel.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
System.out.println("User cancelled dialog");
dialog.close();
}
});


//creates text box
//final Text textBox = new Text(dialog, SWT.BORDER);
data = new FormData();
data.width = 200;
data.left = new FormAttachment(openProjectLabel, 0, SWT.DEFAULT);
data.right = new FormAttachment(100, 0);
data.top = new FormAttachment(openProjectLabel, 0, SWT.CENTER);
data.bottom = new FormAttachment(cancel, 0, SWT.DEFAULT);
//textBox.setLayoutData(data);
//textBox.forceFocus();

final Combo combo = new Combo(dialog, SWT.NONE);
combo.setItems (options);
//set the default text to the first option, can be overridden by user
if(options.length > 0){
String selectedOption = Configuration.getSetting(_settings, Configuration.LAST_PROJECT, options[0]);
combo.setText(selectedOption);
}
combo.setLayoutData(data);
combo.forceFocus();


//creates ok button
Button ok = new Button(dialog, SWT.PUSH);
ok.setText("OK");
data = new FormData();
data.width = 60;
data.right = new FormAttachment(cancel, 0, SWT.DEFAULT);
data.bottom = new FormAttachment(100, 0);
ok.setLayoutData(data);
ok.addSelectionListener(
// nested inner class
new SelectionAdapter() {

/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.SelectionAdapter
* #widgetSelected (org.eclipse.swt.events.SelectionEvent)
*/
public void widgetSelected(SelectionEvent e) {
String selectedProject = combo.getText();
//set the project in the controller
_controller.setProject(selectedProject);
//updated the last project setting with chosen value
_settings.put(Configuration.LAST_PROJECT, selectedProject);
// close the message dialog
dialog.close();

}
});


dialog.setDefaultButton(ok);
dialog.pack();
dialog.open();
}


Now becomes just:

/**
* @param shell
*/
public OpenAction(PunchClockWindow window) {
_window = window;
setText("&Open Project...\tCTRL+O");
ImageRegistry reg = Resources.getImageRegistry();
setImageDescriptor(reg.getDescriptor(Resources.IMAGES_FOLDER_OPEN_PNG));
}


/* (non-Javadoc)
* @see org.eclipse.jface.action.Action#run()
*/
public void run() {

// create new JFace dialog where user enters the project name
OpenProjectDialog projDialog = new OpenProjectDialog(_window);
projDialog.open();
}


Now, a lot of the code above is actually still there, just contained in the new object OpenProjectDialog which extends JFace's Dialog class. The one big benefit I notice with Dialogs is that you get the OK and Cancel buttons "for free". So instead of needing to build the popup window from scratch as shown above, I just overrode and implemented the createDialogArea() and okPressed() methods from Dialog with my custom functions. That completely removes maintaining the Cancel button code from my responsibility for example.

Whether the conversion to JFace has objectively produced less code for the same exact results, I utilized a free code line counter, CLOC, to check how much improvements there were at all.


Before (v0.2.1 code pulled from SVN)
http://cloc.sourceforge.net v 1.53  T=0.5 s (64.0 files/s, 10350.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            32            725           1680           2770
-------------------------------------------------------------------------------
SUM:                            32            725           1680           2770
-------------------------------------------------------------------------------
After JFace conversion of view layer 
http://cloc.sourceforge.net v 1.53  T=0.5 s (66.0 files/s, 9538.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            33            705           1510           2554
-------------------------------------------------------------------------------
SUM:                            33            705           1510           2554
-------------------------------------------------------------------------------
There was an 8% shrinking of the code. Not too bad, I'll take it.

JFace ApplicationWindow's mysterious separator line
One quirk that I did come across after the conversion to pure JFace implemenation of the main window, was that it seems the default behavior for ApplicationWindow is to create a separator line at the top of the window's "contents" panel. This is in case you have a menu. This wasn't there before the port, so I was pretty confused. I began coloring all the widgets in my UI to try to understand what was creating this line but had no luck, because it wasn't in my code after all. See the strange extra line on the left.
JFace application window adds this extra line, shown on the left.


In my ApplicationWindow class, it turns out you need to override the ApplicationWindow#showTopSeperator() method and make sure it returns false if you don't need it, and this depends on which OS you have e.g. for Windows 7 and Mac OSX the menu bar is separate from the application panel anyways, no separator is needed. It may mean that for some OSs like Win XP for example, I might need to make the code smarter to conditionally show the separator or not.

Saturday, June 4, 2011

Punch Clock 0.2.1 and clickable edit button for SWT tables

After the release of Punch Clock 0.2.0 I observed a few issues that needed to be solved. I've just posted a new version, Punch Clock 0.2.1 which has these minor fixes and a new dialog to view the "audit" data in the UI.

One of the new features I added since 0.2.0 which I wanted to write about is the "Fix Session" button I have added to each row of the main table of data in PunchClock. This lets you go back after the fact and modify the times or other details for a record that has been saved. This was a true life-saver for me in practice as you often forget to start the timer at the right time, or want to put clarifying comments in later, etc. I implemented this with a click-able SWT button on every row with a wrench style icon.
Clicking on that button brings up a simple form in a dialog for editing the data in that row. Since I couldn't find an example that was exactly like this, I came up with an approach, still not sure if this is the most efficient way to do it. Here is how this was accomplished.

Using TableEditor to add the Buttons
I started from Snippet126 from the Eclipse SWT snippets site: "Table example snippet: place arbitrary controls in a table".

Using a class called org.eclipse.swt.custom.TableEditor which you construct with your Table, you specify a new control and place it against a particular TableItem in that Table, and even specify which column that control should be over. One way to picture this is that the TableEditor as floating above the Table as an invisible layer which you can put controls on that overlap your Table.

For example in my case, where table is an instance of org.eclipse.swt.widgets.Table


//get all the current items (rows) from your SWT table
TableItem [] items = table.getItems ();
for (int i=0; i<items.length; i++) {

//create the editor
TableEditor editor = new TableEditor(table);

//get the row
TableItem item = items[i];
//create my button
Button button = new Button (table, SWT.PUSH);
//my code to set the button icon image from an JFace ImageRegistry
button.setImage(Resources.getImageRegistry().get(Resources.IMAGES_EDIT_PNG));
button.setSize(16, 16);
button.setToolTipText("Fix this session");
button.pack();

//not sure if necessary, prevent the editor from being smaller than button
editor.minimumWidth = button.getSize ().x;
editor.horizontalAlignment = SWT.RIGHT;
//set the button to float over column 5 for this item
editor.setEditor (button, item, 5);
}


And that is it for the set up. Note regarding the JFace ImageRegistry, I have begun to use an excellent tutorial/article called Using the Eclipse GUI outside the Eclipse Workbench which has three parts. Part 2: Using the JFace image registry, describes the approach to creating an org.eclipse.jface.resource.ImageRegistry that is then referred to whenever you need to display the same image repeatedly in the UI, as I do in the case for the little wrench icon.

Handling Button Clicks
While the above code drops the buttons on the tables, I need to then hook them up to display the edit dialog. This is done through adding a Selection Listener to the button being added to each row.

Again, I am not sure if this is the greatest way to implement this code, but I created a class called EditButtonListener which implemented the org.eclipse.swt.widgets.Listener interface, specifically calling my new dialog from the handleEvent() method. Something like this (with my form logic simplified to pseudo code).


public class EditButtonListener implements Listener {

private TableItem _tableItem;

/**
* @param tableItem - dependency injection, keeps track of the row
*/
public EditButtonListener(TableItem tableItem) {
_tableItem = tableItem;
}

/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
*/
@Override
public void handleEvent(Event event) {
//get the parent shell for my table, needed to launch my new SWT dialog
Shell lShell = _tableItem.getParent().getShell();
//in my table the first column is a unique id
String sessionIdString = _tableItem.getText(0);
System.out.println("Button click for session id: "+sessionIdString);

final TimeSession selectedSession;
try{
//retrieve the selected time session from the back end
//...
}catch(Exception e){
throw new IllegalStateException(e);
}

// create new dialog where user enters the project name
final Shell dialog = new Shell(lShell, SWT.DIALOG_TRIM
| SWT.APPLICATION_MODAL);
dialog.setText("Fix this session");
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = 10;
formLayout.marginHeight = 10;
formLayout.spacing = 10;
dialog.setLayout(formLayout);
// position near parent window location
dialog.setLocation(lShell.getLocation().x + 10,
lShell.getLocation().y + 10);
dialog.setSize(400, 400);
// text displayed in dialog i.e. field labels
//...

// create combo box drop down selection
//final Combo combo = new Combo(dialog, SWT.NONE);
//combo.setItems (options);
//set current value to selected session's value
//...
// text displayed in dialog i.e. field labels
//...
//next text box field
//final Text startTimeBox = new Text(dialog, SWT.SINGLE | SWTSWT.BORDER );
//...
//etc...

//create cancel button
//Button cancel = new Button(dialog, SWT.PUSH);
//cancel.setText("Cancel");
//layout stuff...
//add listener for when cancel is pressed:
//cancel.addSelectionListener(new SelectionAdapter() {
// public void widgetSelected(SelectionEvent e) {
// System.out.println("User cancelled dialog");
// dialog.close();
// }
//});

//creates ok button
//Button ok = new Button(dialog, SWT.PUSH);
//ok.setText("OK");
//layout stuff...
//add listener when OK button pressed
//ok.addSelectionListener(
// nested anonymous inner class for listener, could be separate class too
//new SelectionAdapter() {

/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.SelectionAdapter
* #widgetSelected (org.eclipse.swt.events.SelectionEvent)
*/
//public void widgetSelected(SelectionEvent e) {

//when OK is pressed save the current changes to the backend
//...

// close the message dialog
//dialog.close();

//}

//});

//finish set up of dialog and call open()
//...
//dialog.setDefaultButton(ok);
//dialog.pack();
//dialog.open();
}

}



To link the EditButtonListener back to the button I created above you would modify the button creation above slightly to include adding the Listener:

button.addListener(SWT.Selection, new EditButtonListener(item));

Now when clicked the form pops up



Improvements
One way that I would like to try to improve on this design is to be able to reuse the same EditButtonListener, so instead of creating a new listener instance for each TableEditor button, I would reuse the same instance and it would determine which button was pressed from the event.item passed in to handleEvent(). I also want to remove all the dialog code from the EditButtonListener and implement that using a JFace dialog in its own class.

Sunday, April 10, 2011

 when Reading a text file in Java

If you have code that processes text files using InputStreamReader, you may be puzzled why there are certain garbage characters  at the start of your resulting String data if you output that the contents of the file.

This is due to a Reader processing your input file in ASCII encoding when the data is stored in UTF-8.

While I came across this in Java, the ASCII/UTF-8 solution was actually in this aptly named blog post "Three little characters  designed to make your life hell" by Martyn at the Ventrino Blog.

The solution proposed in the post is basically "if your text file is saved as UTF-8, that is the problem, save as ASCII encoding instead". I think this isn't really a best practice, probably as UTF-8 is better for internationalization, the developer should probably change his/her code to support that format rather than restrict a program to the limited set of US/Western ASCII characters.

Here is an example of code that when reading a UTF-8 encoded file, say a .HTML file will display the 

//stores the html text, in my case I needed to pass it in to an SWT Browser.setText() method.
StringBuffer buffer = new StringBuffer();
//read the HTML to display from a resource
InputStream htmlInStream = Resources.getResourceAsStream(Resources.HTML_ABOUT_HTML);

BufferedReader bufInpStream = new BufferedReader(new InputStreamReader(htmlInStream));
String line = "";

while(line != null){
line = bufInpStream.readLine();
//System.out.println(line);
if(line!= null){
buffer.append(line);
}
}


The key seems to be the InputStreamReader conversion of the underlying InputStream to the HTML file. Readers are by design for processing text, while Streams are more concerned with binary data. So when converting from the Stream, we have to tell it what encoding to use. This correction will solve the problem.

//if we didn't specify char-encoding as UTF-8, it would show these strange chars: 
BufferedReader bufInpStream = new BufferedReader(new InputStreamReader(htmlInStream,"UTF-8"));


Punch Clock has gone live

I have wrapped up development of a beta version of my side-project "Punch Clock". I've created a launch page on Google Sites, and linked from the side bar.

https://sites.google.com/site/punchclockswt/

The released version is basically a public beta, free for anyone to download. Right now I support Windows and Linux.

The feature set is rudimentary but I am adding features as I have time. It has been a good project to learn the ins and outs of SWT for a basic app. I would like to explore refactoring more the UI to use more of the JFace patterns on top of SWT, I was just beginning to use this with the About dialog and the persistent settings modules. From the beginning this was a learning project, where I picked up Apache Derby, SWT, and created a build process in ANT, launched an entire release site with actual documentation, specified a license for the work etc...

More to follow I am sure.

Sunday, April 3, 2011

Collapsible SWT Text Area Snippet

A little sample code showing how you can have a Java SWT button that can collapse other Controls like a toggle switch. This also will show how to resize the container after "collapsing" your control.

I went this route after not finding the SWT ExpandBar widget suiting this purpose.

Screenshots from Windows 7

Source Code

/*
*
*/
package swt.snippet;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import com.brianl.timetrack.controller.TimeSession;
import com.brianl.timetrack.view.CollapseButtonListener;

/**
* Simple SWT Shell that contains a Group with some controls. An arrow button is
* used to toggle the Text area widget as collapsed or not.
*
* @author ammianus http://librixxxi.blogspot.com/
*
*/
public class CollapsibleTextBox {

private static final int NUMBER_GRID_COLUMNS = 1;

private Display _display;

private Shell _shell;

CollapsibleTextBox() {
_display = new Display();
_shell = new Shell(_display);
_shell.setSize(300, 250);
// Collapsible Group
_shell.setText("Collapsible Group Example");
// format shell as single column grid layout
_shell.setLayout(new GridLayout(NUMBER_GRID_COLUMNS, true));

// create the group
Group collapseGroup = new Group(_shell, SWT.NONE);
GridData groupGrid = new GridData(SWT.TOP, SWT.LEFT, false, false);
groupGrid.horizontalSpan = 1;
collapseGroup.setLayoutData(groupGrid);
collapseGroup.setText("Control Group");

// create a Label and Button inside the Group
GridData labelGridData = new GridData(GridData.VERTICAL_ALIGN_END);
labelGridData.horizontalSpan = 1;
String labelText = "Pressing button toggles Text. ";
final Label instructionLabel = new Label(collapseGroup, SWT.NONE);
instructionLabel.setText(labelText);
instructionLabel.setLayoutData(labelGridData);
instructionLabel.pack();

final Button collapseButton = new Button(collapseGroup, SWT.ARROW
| SWT.UP);

// multi-row Text area with word-wrap
final Text textArea = new Text(collapseGroup, SWT.MULTI | SWT.LEAD
| SWT.BORDER | SWT.WRAP);

// set height and width of Text area
GC gc = new GC(textArea);
FontMetrics fm = gc.getFontMetrics();
final int textBoxWidth = 50 * fm.getAverageCharWidth();
final int textBoxHeight = 4 * fm.getHeight();
gc.dispose();
textArea.setSize(textArea.computeSize(textBoxWidth, textBoxHeight));
GridData textBoxGrid = new GridData(SWT.TOP, SWT.LEFT, false, false);
textBoxGrid.horizontalSpan = NUMBER_GRID_COLUMNS;
textBoxGrid.heightHint = textBoxHeight;
textBoxGrid.widthHint = textBoxWidth;
textArea.setLayoutData(textBoxGrid);

// set the layout for the items contained by the Group as a two column
// grid
collapseGroup.setLayout(new GridLayout(2, false));
collapseGroup.layout(true);

// create a listener that waits for button press
collapseButton.addListener(SWT.Selection, new CollapseButtonListener(
textArea, textBoxWidth, textBoxHeight, collapseGroup,
textBoxGrid));

// open the shell and run UI loop
_shell.pack();
_shell.open();
while (!_shell.isDisposed()) {
if (!_display.readAndDispatch())
_display.sleep();
}
_display.dispose();
}

/**
* Runs the demo
*
* @param args
*/
public static void main(String[] args) {
new CollapsibleTextBox();
}

/**
* inner class handles button click events for arrow button
*
*/
public class CollapseButtonListener implements Listener {

private Text _timeNotesBox;
private int _textBoxHeight;
private int _textBoxWidth;
private Group _container;
private GridData _textBoxGrid;

/**
* @param timeNotesBox
* @param textBoxHeight
* @param textBoxWidth
* @param container
* @param textBoxGrid
*/
public CollapseButtonListener(Text timeNotesBox, int textBoxWidth,
int textBoxHeight, Group container, GridData textBoxGrid) {
super();
// save references to the various controls to update
this._timeNotesBox = timeNotesBox;
this._textBoxHeight = textBoxHeight;
this._textBoxWidth = textBoxWidth;
this._container = container;
this._textBoxGrid = textBoxGrid;
}

/*
* (non-Javadoc)
*
* @see
* org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets
* .Event)
*/
@Override
public void handleEvent(Event event) {
// toggles the Text Area as visible or not.
// When Text Area is collapsed, resize the parent group
if (_timeNotesBox.getVisible()) {
// collapse the text area
_timeNotesBox.setSize(_timeNotesBox.computeSize(1, 1));
_textBoxGrid.heightHint = 1;
_textBoxGrid.widthHint = 1;
_timeNotesBox.setLayoutData(_textBoxGrid);
_timeNotesBox.setVisible(false);
_timeNotesBox.pack(true);
System.out.println("Hiding textbox");
} else {
// expand text area back to original sizing
_timeNotesBox.setSize(_timeNotesBox.computeSize(_textBoxWidth,
_textBoxHeight));
_textBoxGrid.heightHint = _textBoxHeight;
_textBoxGrid.widthHint = _textBoxWidth;
_timeNotesBox.setLayoutData(_textBoxGrid);
_timeNotesBox.setVisible(true);
_timeNotesBox.pack(true);
System.out.println("Showing textbox");
}

// in this case the container is the Group
_container.pack(true);

// resize the parent shell too
_container.getParent().pack(true);
}

}
}


Built with:
swt-3.6.1-win32-win32-x86
Java 1.6.0_17
Eclipse 3.4.1

Built on the model of the snippets from java2s and the SWT snippets site.

I came up with this approach as I am trying to have an optional text box in my "Time Track" program. I really didn't see any other way to have this kind of dynamic visual behavior that is pretty common on websites these days. The only prebuilt widget that seemed similar was the ExpandBar, but what that did was create a large pane with expandable widgets within it. These all had a very non-native look and feel and was bit complex to set up when I had a single text box to expand/collapse.

Sunday, February 6, 2011

Introducing the Time Track Project

The Time Track application is a small side-project I have been developing since late November 2010. It is a simple application that lets a user keep track of time spent on work, projects, tasks etc.


Time Track main window, Windows Vista x64

The main requirements for the Time Track application were

  • Be a software version of a time punch clock, for any purpose

  • Keep track of time in a persistant format that can be saved

  • Cross-platform support (at least Windows, Mac OS X)

  • Needs a GUI

  • Lightweight, simple to use and set up

I developed this in Java as a standalone desktop program with a GUI using all OpenSource components. I am nearly complete with an initial version that meets all this functionality and I intend to release it as a free-ware app for now, maybe it can be made released as open source by itself in the future.


Implementation


I had really two paths I could have taken with this, one would be to build a dynamic webapp with a RDBMS backend. The second approach was to develop a standalone self-contained application that could be somewhat portable itself. I was hesitant to take on a project that required any sort of server set up and hosting, I am just not set up to do that now, so although the webapp was pretty easily cross-platform being browser based, I didn't like the idea of the dependency on the remote server.


I went with the standalone app approach, in order to be cross-platform and easy for me to develop I went with Java as the source language. I didn't want the program looking like a Java Swing application so I opted for the SWT api which is the basis for the Eclipse IDE and platform, it tends to look more "solid" in my opinion as one of its main features is native look and feel for UI components on every OS. I think so far it is coming along nicely. You can see a side-by-side comparison of the interface in Windows Vista, and on Linux (Ubuntu 10.04, 64-bit)




I have probably spent around 5 weekends and the occasional hour on a weeknight in total since I started building this application. Probably overall time spent has been under 80 hours. It was enjoyable to more or less go through the full development life-cycle for a new project. I got to do design, open-source research, coding and integration, source-control, and complete build scripts in Ant. Even though I may never release a line of code from this, I am building like I could.

In terms of the OpenSource projects that I used a variety of different components in my stack.

For my build environment, I am using Java JDK 1.6.0_17, Eclipse Ganymede IDE, Junit 3 (for the few automated unit tests that were really helpful with Derby), SVN for source control and builds are done with ANT. Testing was done on Windows Vista SP2 x64 and Ubuntu Linux 64-bit GTK.


RoadMap
On my to do list I still have a number of items I'd like to close out before really wrapping up this release.
  • The main table needs to update when changes occur. Currently it is loaded at start up.
  • Need to think about an import data function.
  • Testing on a Mac
  • Implement a more solid connection pool mechanism
  • Release strategy. Post on this site? or create an OpenSource project page?

Saturday, October 23, 2010

Java Sockets

I had the fortunate experience of getting cross-over benefit at work from my unrelated home experiments in Linux (Ubuntu Experiment, 8/1/2010). A few weeks ago I had gone through Chapter 13 of GNU/Linux Application Programming, covering BSD4.4 Sockets API. I had gone through the sample code for the Daytime protocol server/client sockets in C and made my own socket server examples.

At work there was a customer issue related to a workflow engine that appeared to hang on sending emails through the client's SMTP servers. We had some basic questions on whether or not the workflow engine would hang or eventually timeout if it stayed connected to the server? Or cause exceptions?

It dawned on me I could easily set up a socket server that listened on the STMP port and simulated a server not releasing a connection just for a test.

The socket model is basically: Create Socket > Bind to an address > Listen > Accept client connection > [Data Transfer] > Close > loop back to Accept.

Given my recent experience it would be pretty trivial to replace [Data Transfer] with [Delay indefinitely] in order to see what effect that had. Since we use Windows 7 at work, and I didn't have the C code handy, I figured I would just write the same sort of socket server in Java.

I created a new class, SMTPServerSocket

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import javax.net.ServerSocketFactory;

Just a few necessary imports from java.net and javax.net packages.


public class SMTPSocketServer
{
public SMTPSocketServer(){
super();
}

Definition and default constructor


public static void main(String[] args)
{

try
{

//get the default server socket factory
ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();

//port 25 - SMTP, allow a backlog of 1000 before connections are refused.
ServerSocket smtpServer = socketFactory.createServerSocket(25, 1000);

System.out.println("Started: "+smtpServer.getLocalSocketAddress().toString());
//TODO need a way to break out of this infinite loop
boolean listen = true;
while(listen){
System.out.println("Waiting for connection "+(new Date()));
Socket sock = smtpServer.accept();
//print some info on the connection
System.out.println("Accepted: "+sock.getInetAddress().toString());
System.out.println("SO_TIMEOUT: "+sock.getSoTimeout());
System.out.println("Data: "+sock.getReceiveBufferSize());

System.out.println("Sleeping "+(new Date()));
//sleep for n * 1000 milliseconds
Thread.sleep(2000 * 1000 );
System.out.println("Done "+(new Date()));
sock.close();
}
//close
smtpServer.close();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}

This was the main body. The socket is created using the ServerSocketFactory. I set it up to listen on port 25 (default SMTP port). I entered an infinite while loop. For the purposes of this demo I didn't build anything in that would allow it to break out of the loop. Ctrl+C seems to do the trick. The socket calls accept() which blocks until a client connects. Then when the client connects I printed some debug info, then put the process to sleep for a long amount of time to test the effects on our workflow engine. After the sleep period the connection is closed.

This actually ended up working ok. We proved that the workflow doesn't appear to have any timeout when it tries to connect to the SMTP server. I could reproduce the behavior of the hanging process while it waited on my SMTPServerSocket during the long sleep.

Monday, September 20, 2010

Java method to copy file to UNC network location

Small update on something I used for work. I needed a way to copy a file stored locally from one machine to another machine on the network with a shared folder. This is an all Windows environment.

To copy a file with Java (JRE 1.4 or higher) the method found here was really helpful:

...
//File in = new File("local-file.txt")
//File out = new File("\\\\network\folder\file.txt")
FileChannel inChannel = new
FileInputStream(in).getChannel();
FileChannel outChannel = new
FileOutputStream(out).getChannel();
try {
inChannel.transferTo(0, inChannel.size(),
outChannel);
}
catch (IOException e) {
throw e;
}
finally {
if (inChannel != null) inChannel.close();
if (outChannel != null) outChannel.close();
}
...

Credit: http://www.rgagnon.com/javadetails/java-0064.html

Used the "new" technique using the java.nio package for transferring the file. See FileChannel.transferTo()

The example in the how-to page describes copying an input File in to an output File out

All I had to do was make sure that Java would understand a UNC style network file path for the out File that I wanted to copy to. It was really as simple as that, and worked perfectly. Just remember to double-escape the "\" including the starting "\\" prefix in your UNC file path.

File out = new File("\\\\host-name\\path\\to\\target-file.zip");

That page also discusses an issue on Windows where files of sizes greater than 64 MB fail with an "java.io.IOException: Insufficient system resources exist to complete the requested service" error. I initially tried with the suggested workaround to transfer the file in 64 MB chunks, which worked. For the record the file transfer for a larger file (167 MB) took around 18sec with this method. Given that this issue is several years and java versions old I thought I would try this out without splitting up the file.

My development system is Windows 7, JRE 1.5.0_22, the target host with the network share was on Windows XP. Using the original transferTo method without splitting the file successfully copied my large test file with no errors. There was also a slight performance gain as the copy took about 16 seconds that time.

Doing a little research in the Oracle Sun Developer Network Bug Database, it seems this may have been an issue fixed in: 6431344. Or it may have been that this was an OS problem and doesn't manifest on the Win 7 platform. In either case seems to be working.

Related bugs:
6822107
6486606

Saturday, May 15, 2010

Enter the Dojo

I was struggling with a project at work last week involving Dojo, and elements of AJAX, JSON.

One downside, the Dojo documentation is all over the place, a lot of it is incomplete and when you google search you often see out of date pages or broken links come up first.

My project was cool in a way and I can go into what it involved. But first to give credit where credit is due, I found this really helpful tutorial to add checkbox inputs to a Dojo/Dijit tree (yes it is much harder than one would think)

http://www.thejekels.com/dojo/Dijit_Tree_Chkbox.html

Examples were easy to follow and worked (note that the first steps in the tutorial don't actually work in Dojo 1.4, but he helpfully points that out the workarounds near the end).

Wednesday, March 3, 2010

Local java tile server in SVN

I've checked in the local tile servlet I developed last summer to test WorldWind download code and wrote about here.

This is now under the ammianus branch in world wind SVN.

Sunday, July 26, 2009

Creating Local Tile Servlet

I meant to write my updates when I got back from my trip a week ago, but there are a lot of things at home to take care of after a three week vacation. As promised I did some work on the WorldWind while I was away.

Creating Local Tile Server
With an unsteady internet connection I needed to be able to continue to test WorldWind changes that related to the download code. If I could set up a local server, and point my WorldWind program to check that for tiles I could still check the download code if it works. A side benefit is that in theory if my local tile server is something I coded myself, I'd be able to insert dummy error response codes like 404 or 500 errors in order to test the behavior for those errors in the new download code.

I tried using existing scripts and code that was posted on the WorldWindCentral wiki. But I either don't have the right application server software to run them (PHP, Perl,Python) or the know-how to set it up and use it (IIS 7/aspx.cs). There is also a WorldWind server, written in java posted on the World Wind forums (NASA World Wind WMS Server Config), I tried that too, this would allow me to set up a local server but because it was a self-contained application, but wouldn't let me achieve my second goal of inserting my own custom errors for testing. Even so, it has dependencies on all or some of WorldWind Java SDK. Now on my own, but understanding better what needs to get done, I thought "Why not just write this in a language I know, Java and set up Tomcat with a simple web app?"

Taking the .aspx.cs example, I ported that to a Java Servlet class.

package org.wwc.ammianus.tomcat.tileserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.HashMap;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
*

This servlet handles requests from the World Wind.NET application and returns
* tile images or error codes as appropriate. This was a port to Java of a ASP.NET file posted
* at http://worldwindcentral.com/wiki/Data_serving_scripts


*
* @author ammianus
* @version 0.1
*
*
*/
public class TileRequestHandler extends HttpServlet {

/**
* Generated by Eclipse
*/
private static final long serialVersionUID = -6596187549520730077L;
/**
* settings Map which theoretically could be extended for any layer used in WW
* for each layer, use the <DataSetName></DataSetName> tag value from @Images.xml file
* create three map entries <DataSetName>, <DataSetName>_EXT, <DataSetName>_MIME
*/
private HashMap _settings;

private int _requestNumber;

/**
* @see HttpServlet#HttpServlet()
*/
public TileRequestHandler() {
super();
//use these setting which theoretically could be extended for any layer used in WW
//for each layer, use the tag value from @Images.xml file
//create three map entries , _EXT, _MIME
_settings = new HashMap();
_settings.put("geocover2000", "C:\\Users\\Public\\WorldWindCache\\Cache\\Earth\\Images\\NASA Landsat Imagery\\Geocover 2000");
_settings.put("geocover2000_EXT", ".jpg");
_settings.put("geocover2000_MIME", "image/jpeg");

_requestNumber = 0;
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//increment counter
_requestNumber++;

//Un-comment this to test whether WW handles 503 return code with Retry-After header
//
//first request we will send an error with a retry-after header
//
/*if(_requestNumber == 1){
response.setStatus(503);
//this appears to be case insensitive for WorldWind, I just picked a random retry-after period
response.setHeader("retry-after", "38");
System.out.println(_requestNumber+":Response: 503, with retry-after: 38");
return;
}*/

try{


//column
int X = Integer.parseInt(request.getParameter("X").toString());
//row
int Y = Integer.parseInt(request.getParameter("Y").toString());
//level
int L = Integer.parseInt(request.getParameter("L").toString());
//DataSet name
String T = request.getParameter("T").toString(); //T = Dataset in WorldWind parlance


//Un-comment this to test whether WW handles 500 return code
//
//if(T.equals("geocover2000")){
// throw new Exception("Force return of 500 - Server Error");
//}

//Arguments are in this format URL sent from world wind
//e.g. http://localhost:8080/TomcatTileServer/TileRequestHandler?T=geocover2000&L=0&X=47&Y=21
String filePath = _settings.get(T);//WebConfigurationManager.AppSettings[T];

String fileExt = _settings.get(T + "_EXT");
if(L < 3){
DecimalFormat df = new DecimalFormat("0000");
filePath += "\\"+L + "\\" + df.format(Y) + "\\" + df.format(Y) + "_" + df.format(X) + fileExt;
}else{
filePath += "\\level"+(L+1)+fileExt;
}
//request.get
System.out.println(_requestNumber+":Requested File: "+filePath);
File file = new File(filePath);

//if file (image for requested tile) is not found on server, return 404
if (!file.exists())
{
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found");
}
else
{
//set content length to file size
response.setContentLength((int) file.length());
//get mime type for this layer
response.setContentType(_settings.get(T + "_MIME"));
//get the output stream for the response, this will have the file written to it
ServletOutputStream stream = response.getOutputStream();

//read the file contents and write them to the outputstream
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024];
int counter = 0;
while(counter < file.length()){
fis.read(bytes);
counter += 1024;
stream.write(bytes);
}
fis.close();
stream.flush();
stream.close();

//done with response
}
}catch(Exception e){
//uncaught Exception return 500 error
response.sendError(500,e.getMessage());
System.out.println(_requestNumber+":Response: send 500 error");
}

}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}

}

As you can see, not much there. In my WEB-INF/web.xml, I included the new servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>TomcatTileServer</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>TileRequestHandler</display-name>
<servlet-name>TileRequestHandler</servlet-name>
<servlet-class>org.wwc.ammianus.tomcat.tileserver.TileRequestHandler</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TileRequestHandler</servlet-name>
<url-pattern>/TileRequestHandler</url-pattern>
</servlet-mapping>
</web-app>
Now I was typically running this from my Eclipse IDE (Eclipse J2EE 3.4.1, Apache Tomcat 6), but I also could export the project in Eclipse as a war and just drop it into Tomcat's webapps folder.

To access this server there is the format that the URL would take:
http://localhost:8080/TomcatTileServer/TileRequestHandler?T=geocover2000&L=0&X=48&Y=21

To break this down:
  • T = the name of the layer, or DataSet name ( tag value from @Images.xml)
  • L = level, (0 - x) where 0 is the highest level
  • X = column
  • Y = row
From this the TileServer, based on its implementation and organization of the images, will try to look up locally a file that matches that. In my case it was something like C:\Users\Public\WorldWindCache\Cache\Earth\Images\NASA Landsat Imagery\Geocover 2000\0\0021\0021_0049.jpg".

Integrating Local Server with WorldWind
Thanks to WorldWind forums for giving me this information. Now to make WorldWind point to my local tile servlet instead of the default NASA WMS server, you need to edit the @Images.xml file in your WorldWind's Config\Earth directory. There is probably an @Images.tmp, copy this and rename it to @Images.xml.

Edit the file, and for the DataSetName your local server will handle, update the tag such as:
<ServerUrl>http://localhost:8080/TomcatTileServer/TileRequestHandler</ServerUrl>
And then restart World Wind you should be good to go

Saturday, April 18, 2009

Java HashMap vs. C# Hashtable

One of the things I have found has been more frustrating while picking up C# .NET for the World Wind project is that some of the simple basic programming constructs that are automatic for me in Java don't seem to be directly parallel in C#. On top of that the online documentation at the MSDN site and other places is either not intuitive or I am looking in the wrong areas.

One of the basic data structures I depend on in Java are Maps. One of the simple classes that implement the Map interface in Java is java.util.HashMap.

Like other data structure objects in Java, there is no special syntax to interact with this object, it has methods like other objects where you pass in arguments and get return values.

The most basic usage of the HashMap generally goes like this:

//construct the new map
HashMap javaMap = new HashMap();
//enter a new key value pair
javaMap.put("key","value");
//to get the all values out of the map
Set keys = javaMap.keySet();
Iterator keyIter = keys.iterator();
while(keyIter.hasNext()){
String key = (String) keyIter.next();
String value = (String) javaMap.get(key);
System.out.println(key+" = "+value);
}






So it is pretty straightforward usage (in my mind), the put method "puts" data into the Map and the "get" method retrieves data from the Map given a key. Great

C# on the other hand uses a more elaborate syntax and combination of properties, keywords and methods. Not that this is more difficult but it took some time for me to figure out for example, that there was no equivalent to the "get" method in Hashtable, that you needed to use the "[]" syntax as if this were like an array

For the equivalent C# as the above, you can use the System.Collections.Hashtable class which implements the IDictionary interface:

//construct the Hashtable
Hashtable cSharpTable = new Hashtable();
//enter new key value pairs
cSharpTable.Add("key", "value");
cSharpTable.Add("hello", "world");
//for all Keys, a public property on Hashtable class, print out the key and value
foreach (String key in cSharpTable.Keys)
{
String value = (String) cSharpTable[key];
System.Console.WriteLine(key + " = " + value);
}


So in some ways the C# code is definitely more concise. But for a Java programmer it feels a bit strange, instead of 3 methods in Java(all with clear Javadoc descriptions of use, IDE auto-complete and tool tips, same syntax as all other method calls in Java etc..) for put(),get(),keySet(), C# requires you to use three different language features, a method, Add(), a property on the object Keys (why not just use a method?), and a [] operator. Granted in Visual Studio 2005 typing "cSharpTable." and you would see a list of the public methods and properties including the Add() and Keys, but you would only see the auto-assist tool-tip if you knew to type "cSharpTable[" and then it would give you the basic description of the syntax "object Hashtable[object key]".

I guess it is just something I'll have to learn.

I basically learned Java on the fly on my first job. I learned it fairly easily because once you got it that everything is just a method its pretty easy to find and locate what you need, plus the clear and standardized format of Javadoc based sites makes learning new classes or finding methods fairly simple. Try comparing the ease of use of Sun's Java API site with Microsoft's MSDN .NET Framework Reference(see link to HashMap and Hashtable classes above). Is it really an apples to oranges comparison? Or is the MSDN site just not very readable (for that one class there is no list of methods, just pages of examples in multiple languages.

References:
Holzner, Steven Microsoft Visual C# .NET 2003 Kick Start. Indianapolis: Sams Publishing, 2004. pp243-245

Friday, January 30, 2009

Log4j explanation

This article helped me get around a problem at work. It was so simple and so helpful all at once.

Thank you sir!

Specifically what I was trying to do was to find out what logging was being done by some classes I was trying to test with JUnit Testing with an Eclipse plugin. I didn't know how the server I was interacting with was trying to find the log4j properties (what file name at what path) and I was having no luck seeing the log messages for the classes I was trying to test because there was no category set for them.

I would receive warning like this:
log4j:WARN No appenders could be found for logger(somePackageName.someClassName).
log4j:WARN Please initialize the log4j system properly.

In order to add a specific appenders to DEBUG and ERROR level messages from my class I need to find the log4j configuration.

In the Eclipse Debug Configurations on my JUnit test configuration, click on the Arguments tab. For VM Arguments I had the following:
-ea -Xmx256m -Dlog4j.debug

That did the trick, I was able to see in the console a message stating the xxx.properties was not found.
With that information I updated the properties file in the path it was looking for to add a category for the packages under test, and to change the appenders to point to new log files in my Eclipse project.