31 March 2009

BeanTableModel as a TableModel for Swing

I've written lots of models for JTable's in Swing. Often I have to re-learn how Swing TableModel work each time. There is also often the case that one row in the table represents a Java object with bean getters and setters. Inspired by the Spring bean configuration I wrote TableModel that uses a bean class as a base.

It makes it easy to create a table and sets up the default CellEditors automatically and can get and set data from the cell in the GUI to the Java object.


BeanTableModel:
package se.lesc.beantablemodel;

import java.beans.*;
import java.util.*;

import javax.swing.table.AbstractTableModel;

/**
* A table model where each row represents one instance of a Java bean.
* When the user edits a cell the model is updated.
*
* @author Lennart Schedin
*
* @param <M> The type of model
*/
@SuppressWarnings("serial")
public class BeanTableModel<M> extends AbstractTableModel {
private List<M> rows = new ArrayList<M>();
private List<BeanColumn> columns = new ArrayList<BeanColumn>();
private Class<?> beanClass;

public BeanTableModel(Class<?> beanClass) {
this.beanClass = beanClass;
}

public void addColumn(String columnGUIName, String beanAttribute,
EditMode editable) {
try {
PropertyDescriptor descriptor = new PropertyDescriptor(beanAttribute,
beanClass);
columns.add(new BeanColumn(columnGUIName, editable, descriptor));
} catch (Exception e) {
e.printStackTrace();
}
}

public void addColumn(String columnGUIName, String beanAttribute) {
addColumn(columnGUIName, beanAttribute, EditMode.NON_EDITABLE);
}

public void addRow(M row) {
rows.add(row);
}

public void addRows(List<M> rows) {
for (M row : rows) {
addRow(row);
}
}

public int getColumnCount() {
return columns.size();
}

public int getRowCount() {
return rows.size();
}

public Object getValueAt(int rowIndex, int columnIndex) {
BeanColumn column = columns.get(columnIndex);
M row = rows.get(rowIndex);

Object result = null;
try {
result = column.descriptor.getReadMethod().invoke(row);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

public void setValueAt(Object value, int rowIndex, int columnIndex) {
M row = rows.get(rowIndex);
BeanColumn column = columns.get(columnIndex);

try {
column.descriptor.getWriteMethod().invoke(row, value);
} catch (Exception e) {
e.printStackTrace();
}
}

public Class<?> getColumnClass(int columnIndex) {
BeanColumn column = columns.get(columnIndex);
Class<?> returnType = column.descriptor.getReadMethod().getReturnType();
return returnType;
}

public String getColumnName(int column) {
return columns.get(column).columnGUIName;
}


public boolean isCellEditable(int rowIndex, int columnIndex) {
return columns.get(columnIndex).editable == EditMode.EDITABLE;
}

public List<M> getRows() {
return rows;
}

public enum EditMode {
NON_EDITABLE,
EDITABLE;
}

/** One column in the table */
private static class BeanColumn {
private String columnGUIName;
private EditMode editable;
private PropertyDescriptor descriptor;

public BeanColumn(String columnGUIName, EditMode editable,
PropertyDescriptor descriptor) {
this.columnGUIName = columnGUIName;
this.editable = editable;
this.descriptor = descriptor;
}
}
}

A test class:
package se.lesc.beantablemodel;

import javax.swing.*;

import se.lesc.beantablemodel.BeanTableModel.EditMode;

@SuppressWarnings("serial")
public class TableTester extends JTable {

public TableTester() {
BeanTableModel<Person> model = new BeanTableModel<Person>(Person.class);
model.addColumn("Social Security Number", "socialSecurityNumber");
model.addColumn("Name", "name", EditMode.EDITABLE);
model.addColumn("Age", "age", EditMode.EDITABLE);
model.addColumn("Heigt (in cm)", "height", EditMode.EDITABLE);
model.addColumn("Has empoyment", "employed", EditMode.EDITABLE);

model.addRow(new Person("1", "David", 20, 170.5, true));
model.addRow(new Person("2", "Susan", 26, 162.0, false));
model.addRow(new Person("3", "Mark", 42, 180.5, false));
model.addRow(new Person("4", "Anna", 54, 168.0, true));
model.addRow(new Person("5", "Johan", 5, 120.5, false));

setModel(model);
}

public static class Person {
private String socialSecurityNumber;
private String name;
private Integer age;
private Double height;
private Boolean employed;
private String hiddenField;

public Person(String socialSecurityNumber, String name, Integer age,
Double height, Boolean isMale) {
this.socialSecurityNumber = socialSecurityNumber;
this.name = name;
this.age = age;
this.height = height;
this.employed = isMale;
}

public String getSocialSecurityNumber() {
return socialSecurityNumber;
}
public void setSocialSecurityNumber(String socialSecurityNumber) {
this.socialSecurityNumber = socialSecurityNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Double getHeight() {
return height;
}
public void setHeight(Double height) {
this.height = height;
}
public Boolean isEmployed() {
return employed;
}
public void setEmployed(Boolean employed) {
this.employed = employed;
}

public String getHiddenField() {
return hiddenField;
}
public void setHiddenField(String hiddenField) {
this.hiddenField = hiddenField;
}
}

public static void main(String args[]) {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

TableTester table = new TableTester();
JScrollPane scollPane = new JScrollPane(table);
frame.setContentPane(scollPane);

frame.pack();
frame.setVisible(true);

}
}

11 March 2009

Where are the numerical-less keyboards?

I want more keyboards that are full size without a numerical keyboard. Why? It is a purely physical requirement! A person that uses the mouse in the right hand does not have to use as wide angle of the arm if the keyboard is narrower. Even a slight change in angle of the arm increases the risk of mouse arm syndrome. A human person arms are its most comfortable when angling into the direction of the body. A user of the classical finger position on the keaboard (“asdf jklö” on my keyboard) has a long way to the mouse on the right side, probably risking to angle the arm outside the body.


The only keyboard that I have found is the Logitech Dinovo cordless desktop for notebooks (it is no difference between the “for notesbooks” and “laser” except the mouse). It now looks like Logitech is going to stop producing this keyboard (it can only be found in the support section on www.logitech.com).

Use Launchy to start applications

A nice program to start applications is Launchy. It is almost like the start field in Vista, except this is a standalone program. I recommend the Putty plugin to Launchy. With this, it has never been easier to start a new ssh session. Just press Alt-Space to start Launcy and then enter "ssh" to enable the putty plugin. After that enter a saved Putty session or a IP-number or host to connect to a new ssh server.



This is how it looks like. Launch can start almost all your programs (and with parameters)!

Escape illegal characters with JAXB XML serialization

The XML 1.0 specification says that some characters are illegal in XML (http://www.w3.org/TR/REC-xml/).

When performing my JAXB marshal I had an ASCII control character in my Java object. This character was written into the XML file and everything looked okay... Until I tried to make an XSLT transformation. My transformer engine could not transform the XML because of the character.

I searched the internet and found this thread: http://www.nabble.com/Escaping-illegal-characters-during-marshalling-td20090044.html. This code can escape special characters. I've made a small modification to the code so that is worked better. I changed it so it work with only JDK API and added the UTF-8 parameter so it also can handle latin-1 characters regardless on which locale the Java VM is executing.

There is one drawback with this: You don't get a nice indented code. All the marshaller.setProperty(...) will probably not work anymore.

I'm a bit disappointed with the Sun implementation of the JAXB marshaller that it cannot handle this problem.

import java.util.HashSet;

import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

/**
* Delegating {@link XMLStreamWriter} that filters out UTF-8 characters that
* are illegal in XML.
*
* @author Erik van Zijst (small change by Lennart Schedin)
*/
public class EscapingXMLStreamWriter implements XMLStreamWriter {

private final XMLStreamWriter writer;
public static final char substitute = '\uFFFD';
private static final HashSet<Character> illegalChars;

static {
final String escapeString = "\u0000\u0001\u0002\u0003\u0004\u0005" +
"\u0006\u0007\u0008\u000B\u000C\u000E\u000F\u0010\u0011\u0012" +
"\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C" +
"\u001D\u001E\u001F\uFFFE\uFFFF";

illegalChars = new HashSet<Character>();
for (int i = 0; i < escapeString.length(); i++) {
illegalChars.add(escapeString.charAt(i));
}
}

public EscapingXMLStreamWriter(XMLStreamWriter writer) {

if (null == writer) {
throw new IllegalArgumentException("null");
} else {
this.writer = writer;
}
}

private boolean isIllegal(char c) {
return illegalChars.contains(c);
}

/**
* Substitutes all illegal characters in the given string by the value of
* {@link EscapingXMLStreamWriter#substitute}. If no illegal characters
* were found, no copy is made and the given string is returned.
*
* @param string
* @return
*/
private String escapeCharacters(String string) {

char[] copy = null;
boolean copied = false;
for (int i = 0; i < string.length(); i++) {
if (isIllegal(string.charAt(i))) {
if (!copied) {
copy = string.toCharArray();
copied = true;
}
copy[i] = substitute;
}
}
return copied ? new String(copy) : string;
}

public void writeStartElement(String s) throws XMLStreamException {
writer.writeStartElement(s);
}

public void writeStartElement(String s, String s1) throws XMLStreamException {
writer.writeStartElement(s, s1);
}

public void writeStartElement(String s, String s1, String s2)
throws XMLStreamException {
writer.writeStartElement(s, s1, s2);
}

public void writeEmptyElement(String s, String s1) throws XMLStreamException {
writer.writeEmptyElement(s, s1);
}

public void writeEmptyElement(String s, String s1, String s2)
throws XMLStreamException {
writer.writeEmptyElement(s, s1, s2);
}

public void writeEmptyElement(String s) throws XMLStreamException {
writer.writeEmptyElement(s);
}

public void writeEndElement() throws XMLStreamException {
writer.writeEndElement();
}

public void writeEndDocument() throws XMLStreamException {
writer.writeEndDocument();
}

public void close() throws XMLStreamException {
writer.close();
}

public void flush() throws XMLStreamException {
writer.flush();
}

public void writeAttribute(String localName, String value) throws XMLStreamException {
writer.writeAttribute(localName, escapeCharacters(value));
}

public void writeAttribute(String prefix, String namespaceUri, String localName, String value)
throws XMLStreamException {
writer.writeAttribute(prefix, namespaceUri, localName, escapeCharacters(value));
}

public void writeAttribute(String namespaceUri, String localName, String value)
throws XMLStreamException {
writer.writeAttribute(namespaceUri, localName, escapeCharacters(value));
}

public void writeNamespace(String s, String s1) throws XMLStreamException {
writer.writeNamespace(s, s1);
}

public void writeDefaultNamespace(String s) throws XMLStreamException {
writer.writeDefaultNamespace(s);
}

public void writeComment(String s) throws XMLStreamException {
writer.writeComment(s);
}

public void writeProcessingInstruction(String s) throws XMLStreamException {
writer.writeProcessingInstruction(s);
}

public void writeProcessingInstruction(String s, String s1)
throws XMLStreamException {
writer.writeProcessingInstruction(s, s1);
}

public void writeCData(String s) throws XMLStreamException {
writer.writeCData(escapeCharacters(s));
}

public void writeDTD(String s) throws XMLStreamException {
writer.writeDTD(s);
}

public void writeEntityRef(String s) throws XMLStreamException {
writer.writeEntityRef(s);
}

public void writeStartDocument() throws XMLStreamException {
writer.writeStartDocument();
}

public void writeStartDocument(String s) throws XMLStreamException {
writer.writeStartDocument(s);
}

public void writeStartDocument(String s, String s1)
throws XMLStreamException {
writer.writeStartDocument(s, s1);
}

public void writeCharacters(String s) throws XMLStreamException {
writer.writeCharacters(escapeCharacters(s));
}

public void writeCharacters(char[] chars, int start, int len)
throws XMLStreamException {
writer.writeCharacters(escapeCharacters(new String(chars, start, len)));
}

public String getPrefix(String s) throws XMLStreamException {
return writer.getPrefix(s);
}

public void setPrefix(String s, String s1) throws XMLStreamException {
writer.setPrefix(s, s1);
}

public void setDefaultNamespace(String s) throws XMLStreamException {
writer.setDefaultNamespace(s);
}

public void setNamespaceContext(NamespaceContext namespaceContext)
throws XMLStreamException {
writer.setNamespaceContext(namespaceContext);
}

public NamespaceContext getNamespaceContext() {
return writer.getNamespaceContext();
}

public Object getProperty(String s) throws IllegalArgumentException {
return writer.getProperty(s);
}
}


Here is the test class:
import static org.junit.Assert.*;

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;

import javax.xml.bind.*;
import javax.xml.bind.annotation.*;
import javax.xml.stream.*;

import org.junit.Test;

/**
* Test class to escape special characters from XML
*
* @author Lennart Schedin
*/
public class EscapeJaxb {
@Test
public void testEvilXml() throws Exception {
//Store the serialized data in memory
ByteArrayOutputStream out = new ByteArrayOutputStream();

//Serialize the test XML class
JAXBContext jaxbContext = JAXBContext.newInstance(EvilXml.class);
Marshaller marshaller = jaxbContext.createMarshaller();
XMLStreamWriter xmlStreamWriter =
XMLOutputFactory.newInstance().createXMLStreamWriter(out, "UTF-8");
EscapingXMLStreamWriter filter = new EscapingXMLStreamWriter(xmlStreamWriter);
marshaller.marshal(new EvilXml(), filter);

assertEquals(59, out.size());

//Check that the latin-1 char is intact and the control char substituted
String expectedXmlString =
"<?xml version=\"1.0\" ?><evilXml>" +
new EvilXml().content +
"</evilXml>";
expectedXmlString = expectedXmlString.replace('\u0007',
EscapingXMLStreamWriter.substitute);
String xmlString = new String(out.toByteArray(), Charset.forName("UTF8"));
assertEquals(expectedXmlString, xmlString);
}

@XmlRootElement
public static class EvilXml {
@XmlValue
/* Illegal control ASCII character and a latin-1 A with a ring above */
private String content = "Hello World \u0007 \u00c5";
}
}

06 March 2009

Top-down vs Bottom-up in design

Programmers and technical oriented people often make the mistake of designing a new feature bottom-up. That is to start with the technical lower bits and then piece everything together in the end. When transforming a design to code I think that a bottom-up approach often is very good, since each piece can be tested before pieced together. But when designing I think that the top-down approach in most cases are better.

If you are adding a nice cool feature, for example the user should be able to configure an avatar in a profile on a website, start with the GUI! How should the GUI look like? When starting with the GUI it is easier to capture all the demands a product owner has. After the GUI has been made, then it is time to start thinking on about how it should be implemented in the database, for example to add a column to a table or create a new table.

The only time (I can think of right now) when the database table design must be first is when there is some hidden requirement that the database cannot be changed freely.

Read more on Top-down design on Wikipedia

05 March 2009

OpenID and Google

Google has introduced support for OpenID. If you have a Google account (for example if you are using Gmail or Blogger) you can sign in to other sites that have support for OpenID. Use this URL as OpenID-URL to do so: https://www.google.com/accounts/o8/id. (There is no point using the URL as a standalone in a web browser).

Google has previously had criticism that it was not 100% OpenID compatible, making it impossible to use your Google account on some sites. Some say that Google have fixed these problems now (http://google-code-updates.blogspot.com/2008/10/moving-another-step-closer-to-single.html). I'm not sure until I've developed my own site using OpenID (When I do, I will get back on that issue).

Just for fun I used my Google openID account to register a user on the fun site http://stackoverflow.com/ (it is like, www.experts-exchange.com, focused on questions and answers from users in different computer related topics, but free). It worked very well to login to stackoverflow with my Google-OpenID-account.

Use an automated continuous integration system

A automated continuous integration system is a system that can build and run all kinds of tests and report tools on your software. My opinion is that you should always use this if you are more then 1 person developing a software. It can help you greatly to visualize when things goes wrong. For example it can automatically run JUint tests on the code and report when a test is broken. If you have many reporting tools (for example JUint, Findbugs, PMD, Cobertura code coverage etc) it is sooo much easier to rely on a automated continuous integration system to run all these tools.

Another example when this is useful is when you have some kind of license restriction on how a compiler can be used (ARM compiles for example can be expensive and you can only afford to install it on one machine).

I've tried two continuous integration systems: CruiseControl and Hudson. I first tried CruiseControl since it is the mother of continius intregration system. CruiseControl probably has the best support for 3rd party tools. But is has a big drawback: it is difficult to use. You have to manually edit CruiseControl build control files when creating new projects.

A better alternative is Hudson, the tool I'm currently using. Everything can be done in the Web GUI and it is rather easy to do so. The 3rd party support for tools is probably not as good, but is has the most essential so most should find it sufficient.

Both Hudson and CruiseControl have a API that can be used to create own plugins to extend the system.

04 March 2009

Crazy Outlook removes extra line breaks

One thing that always drives me crazy is that Outlook removes extra line breaks from plain text mails. This is extra clear when someone has made a nice list of items like this:
First do this
Then do this

Then it will look like this when opened:
First do this Then do this

How to disable this "feature":
1. Open Outlook.
2. On the Tools menu, click Options.
3. On the Preferences tab, click the E-mail Options button.
4. Click to clear the Remove extra line breaks in plain text messages check box.
5. Click OK two times.

03 March 2009

Compress database data with Mysql and Innodb plugin

Install a specific version of Mysql 5.1 that is binary compatible with the version of innodb plugin. They must match to the last digit in the version number!

Edit my.ini (of my.cnf if on Linux)

Comment out this row if it is InnoDb (and exists):
#default-storage-engine=INNODB

Make sure the default Innodb code delivered with Mysql is deactivated:
skip-innodb

Follow the installation instructions on http://www.innodb.com/doc/innodb_plugin-1.0/innodb-plugin-installation.html

Add the following and restart mysql
innodb_file_per_table=1
innodb_file_format=barracuda
innodb_strict_mode=1

It is now possible to create a new table that is compressed with innodb plugin:
CREATE TABLE MYTABLE (
TM timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
COMMAND varchar(20) DEFAULT NULL,
) ENGINE=innodb ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;

Don't forget to benchmark! The KEY_BLOCK_SIZE can be 1, 2, 4, 8 and 16. I got the best compression with 2 for my data. The other sizes were not so good. Some values even made the table bigger then uncompressed!

Overall conclusion: The Innodb plugin compression can be useful for some. But it might be useful to look into other compression techniques as well (for example compress data in a single TEXT/BLOB column on the client).

02 March 2009

Compress database data with Mysql

Problem: the database is too large; it requires too much hard disc space. What can be done to solve this? Well of course more hardware can be bought. But what can be done in software?

To narrow the problem: I have very special table construction. I have tables that contain 5-15 ordinary columns with some integers and some VARCHAR:s. Typically some of the cells are empty for each row. Then I have a special column with raw data of some kind. This data is typically from 100 bytes to 3000 bytes in size. It is defined as a BLOB in Mysql table definition.

I tried different compression methods:
• No compression (for reference)
• Use Mysql COMPRESS function to compress only the raw data column
• Used Myisampack to compress the entire table
• Used both Mysql COMPRESS and Myisampack
• Used compression in my Java client with ZLIB


For pure text data in the raw column
                       TEXT_BLOB: 57,8 MiB (100,0 % of original size)
TEXT_BLOB_MYSQL_COMPRESS: 6,0 MiB ( 10,4 % of original size)
TEXT_BLOB_MYISAMPACK: 44,6 MiB ( 77,2 % of original size)
TEXT_BLOB_MYSQL_COMPRESS_AND_MYISAMPACK: 5,8 MiB ( 10,1 % of original size)
TEXT_BLOB_JAVA_ZLIB: 6,0 MiB ( 10,4 % of original size)


For data that is more binary then text-alike in the raw column and where the raw data is a bit smaller then the above example.
                              BINARY_BLOB:  5,7 MiB (100,0 % of original size)
BINARY_BLOB_MYSQL_COMPRESS: 4,3 MiB ( 75,7 % of original size)
BINARY_BLOB_MYISAMPACK: 3,6 MiB ( 63,3 % of original size)
BINARY_BLOB_MYSQL_COMPRESS_AND_MYISAMPACK: 3,3 MiB ( 58,4 % of original size)
BINARY_BLOB_JAVA_ZLIB: 4,2 MiB ( 74,3 % of original size)


From the results it is clear that Myisampack does a poor job of compressing larger BLOB-data. It performs rather ok compressiong the other column, but why doesn’t it compress the BLOB as good as the alternatives? If I’m guessing speed is probably a factor here.

For my specific type of tables I’m probably going for the Java Zlib compression, since it also has the benefit of reducing network traffic between my java client and mysql server.