Monday, January 19, 2009

Using Ant as a library

Ant has a lot of predefined tasks that could be a great help. You can use them directly from your Java code. Here are some examples using Ant to download a file, unzip a package and exec some shell command.

You would need to have
+ ant-1.7.1.jar
+ ant-launcher-1.7.1.jar
+ commons-io-1.3.2.jar (for exec example, found at http://commons.apache.org/io)

in your classpath.


public class AntDemo {

/**
* Download a file at sourceUrl to dest
*
*/
public static void download(URL sourceUrl, File dest) {
Project dummyProject = new Project();
dummyProject.init();

Get antGet = new Get();
antGet.setProject(dummyProject);
antGet.setVerbose(true);
antGet.setSrc(sourceUrl);
antGet.setDest(dest);
antGet.execute();
}

/**
* Unzip a zip file
*
*/
public static void unzip(File src, File dest) {
Project dummyProject = new Project();
dummyProject.init();

Expand antUnzip = new Expand();
antUnzip.setProject(dummyProject);
antUnzip.setSrc(src);
antUnzip.setDest(dest);
antUnzip.execute();

/* ant doesn't preserve permission bits
need to restore them manually */

Chmod chmod = new Chmod();
chmod.setProject(dummyProject);
chmod.setDir(new File(src.getAbsolutePath().replaceAll(".zip", "")));
chmod.setPerm("a+rx");
chmod.setIncludes("**/**");
chmod.execute();
}


/**
* Run a shell command and return the output as String
*
*/
public static String exec(String command, List params, File workDir) {
File outputFile;
try {
outputFile = File.createTempFile("exec", ".out");
} catch (IOException e) {
throw new RuntimeException(e);
}

Project dummyProject = new Project();
dummyProject.init();

ExecTask execTask = new ExecTask();
execTask.setProject(dummyProject);
execTask.setOutput(outputFile);
execTask.setDir(workDir != null ? workDir : new File(System
.getProperty("user.dir")));
execTask.setExecutable(command);
if (params != null) {
for (String param : params) {
execTask.createArg().setValue(param);
}
}

execTask.execute();

FileReader reader = null;
try {
reader = new FileReader(outputFile);
return IOUtils.toString(reader);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(reader);
outputFile.delete();
}
}

public static void main(String[] args) {
List params = Arrays.asList(new String[] { "Hello", "World" });
System.out.println(exec("echo", params, null));
}
}


Just note that you'll need a dummy project for the Ant task. Otherwise you'll get an NullPointerException.

There are a lot of tasks that you could use. The list is here http://ant.apache.org/manual/tasksoverview.html

Sunday, January 04, 2009

Java Webstart Demos with Terracotta enabled

For easy deployment, Terracotta enabled Java applications can also be launched via web start. There are certain issues to look out for but it's certainly possible. I wrote a small demo to demonstrate it.

Issue 1: Terracotta installation required on user's computer before running.

This is solved easily by downloading a Terracotta zip file and extract it to a known location on user computer. In my demo, I install it to $HOME/terracotta_ws.

Issue 2: Java Webstart applications don't allow modification of boot classpath.

Since you can't append or pre-append to boot classpath of the webstart VM, which Terracotta requires, you are forced to spawn an external JVM for your app. This is solvable by signing your jars with a keystore, that will allow your webstart app to spawn external process. This is a good blog showing how to sign your jars.

Note that only jars that are part of your main webstart GUI frame and its libraries are required to be signed. Jars that are part of your Terracotta applications are not required.

When I use "webstart app", I mean the control app that is downloaded and run by Java webstart.

Wehn I use "main app" or "TC enabled app", I mean the Terracotta enabled applications that are spawned in external processes by the "webstart app"

Issue 3: You can't declare your main application jars as part of resources to download in JNLP file.

The main reason is that Java Webstart obfuscates those jars and stores them in a cache with variable folder name. So your webstart app has to handle the downloading and installing the main app's binaries (zip file) to a known location. This is also doable. Take a look at the JNLP file I have:

<jnlp spec="1.0+" codebase="http://localhost:8080/webstartdemo"
href="demo.jnlp">
<information>
<title>TC webstart demo</title>
<vendor>Terracotta</vendor>
<description>Demo of how to start Terracotta from webstart
</description>
<description kind="short">TC Webstart example</description>
<offline-allowed />
</information>
<resources>
<j2se version="1.5+" />
<jar href="TCWebStartDemo.jar" download="eager" />
<jar href="ant-1.7.1.jar" />
<jar href="ant-launcher-1.7.1.jar" />
<jar href="miglayout-3.6.2-swing.jar" />

<property name="tcws.codebase" value="http://localhost:8080/webstartdemo" />
<property name="tcws.terracotta.zip" value="terracotta-trunk-nightly-rev11190.zip" />
<property name="tcws.demo.list" value="sharededitor,mandelbrot" />
<property name="tcws.sharededitor.mainClass" value="demo.sharededitor.Main" />
<property name="tcws.mandelbrot.mainClass" value="mandelbrot.Main" />

</resources>
<application-desc main-class="demo.MainFrame" />
<security>
<all-permissions />
</security>
</jnlp>

From line 13 to 16, those jars are for the webstart app which will be downloaded automatically by java webstart. And those are the ones that need to be signed.
Line 25 indicates the main class of the webstart app.

From line 18 to 22, that is how I communicate to my webstart app (via system properies) about where to download Terracotta and the demos (Terracotta enabled apps). Each demo is a zip file which contains a tc-config.xml, a "lib" folder with all the jars it requires.

You can play with the demo here.

The webstart app Eclipse project can be checked out at SVN repo http://svn.terracotta.org/svn/forge/projects/labs/TCWebStartDemo

Your main interests should be the 2 classes:


Enjoy the demos :)

Friday, January 02, 2009

Redirecting System.out and System.err to JTextPane or JTextArea

In Swing, if you want to redirect System.err and System.out to a JTextPane or a JTextArea, you only need to override the write() methods of OutputStream to append the text to the text pane instead.

The example below shown how to do it with JTextPane. For JTextArea, it's quite simpler, just call JTextArea.append()

This is useful when you spawn external process from a Swing application and want to see its output in your own text component.

Jan 04 2009: Revised regarding Eric Burke's comment below.
JTextPane version:
  private void updateTextPane(final String text) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        Document doc = textPane.getDocument();
        try {
          doc.insertString(doc.getLength(), text, null);
        } catch (BadLocationException e) {
          throw new RuntimeException(e);
        }
        textPane.setCaretPosition(doc.getLength() - 1);
      }
    });
  }

  private void redirectSystemStreams() {
    OutputStream out = new OutputStream() {
      @Override
      public void write(final int b) throws IOException {
        updateTextPane(String.valueOf((char) b));
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
        updateTextPane(new String(b, off, len));
      }

      @Override
      public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
      }
    };

    System.setOut(new PrintStream(out, true));
    System.setErr(new PrintStream(out, true));
  }
JTextArea version:
  private void updateTextArea(final String text) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        textArea.append(text);
      }
    });
  }

  private void redirectSystemStreams() {
    OutputStream out = new OutputStream() {
      @Override
      public void write(int b) throws IOException {
        updateTextArea(String.valueOf((char) b));
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
        updateTextArea(new String(b, off, len));
      }

      @Override
      public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
      }
    };

    System.setOut(new PrintStream(out, true));
    System.setErr(new PrintStream(out, true));
  }

[+] Old versions