Monday, November 26, 2007

Deploying Web Applications Outside the Tomcat Install Directory

Suppose you have Tomcat installed somewhere, e.g. /opt/tomcat6 or /usr/local/tomcat5. Now you want to give users the ability to deploy their applications and start/ stop the Tomcat server if needed (e.g. in case of an unrecoverable crash of the Tomcat server). You can start modifying the Tomcat install directory to support multiple users (e.g. in a given group) to write in the webapps directory and to be able to deploy context files under conf and maybe add dummy users to the tomcat-users.xml to test authentication. But that is breaking open the main Tomcat install directory and potentially allowing users to break things. Another drawback is that having a single instance will cause all applications running on this instance to be unavailable if the server is shut down.

I will describe an alternative way of supporting multiple users using the Tomcat install, by means of each user/ application having its own server. The drawback of this approach is that multiple port numbers are needed and this requires a bit of coordination between users. The advantages however are that not a single change is needed in the Tomcat installation directory whatsoever, users can start stop their own instances without impacting other users/ applications and .

First you'll need a strategy of where to put the user's or application's Tomcat stuff. You could use /app or /opt, e.g. /app/user1, /app/user2, /opt/application1, etc. /app might be a better choice as some software installs in /opt and this could potentially cause the user's/ application's Tomcat stuff to be mixed with other software that you may or want to have installed in /opt. In this example we will have Tomcat 6 installed in /usr/local/tomcat6 and use /app for the applications. We will have two applications, acmeweb (Acme corporation's E-Business website) and acmehr (Acme corporation's human resources application). Create users acmeweb and acmehr with groups acmeweb and acmehr respectively and set their passwords:

adduser --system --shell /bin/bash --group acmeweb
adduser --system --shell /bin/bash --group acmehr
passwd acmeweb
passwd acmehr


Underneath /app create the acmeweb directory. Underneath acmeweb, create the directories bin, conf, webapps, temp, work and logs:

mkdir -p /app/acmeweb
cd /app/acmeweb
mkdir bin conf webapps temp work logs


Copy the following Tomcat scripts to bin:

cd /usr/local/tomcat6/bin/
cp startup.sh shutdown.sh setclasspath.sh catalina.sh /app/acmeweb/bin


Copy the following Tomcat configuration files to conf:

cd ../conf
cp server.xml web.xml /app/acmeweb/conf


Recursively copy /app/acmeweb to /app/acmehr:

cd /app
cp -R acmeweb acmehr


Recursively change ownership of the directories to the proper user and group:

chown -R acmeweb.acmeweb acmeweb
chown -R acmehr.acmehr acmehr


Set the user to acmeweb and create a .profile:

su - acmeweb
vi .profile


Put the following contents in the .profile:

export JAVA_HOME=/usr/local/java6
export CATALINA_HOME=/usr/local/tomcat6
export CATALINA_BASE=/app/acmeweb


Logout and set user to acmeweb again. Edit /app/acmeweb/conf/server.xml:

ctrl-d
su - acmeweb
vi /app/acmeweb/conf/server.xml


Modify the server, AJP and HTTP connector ports and change them to 20105, 20109 and 20180 respectively (you may also consider turning off the AJP connector altogether):

<Server port="20105" shutdown="SHUTDOWN">
...
<Connector port="20180" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
...
<Connector port="20109" protocol="AJP/1.3" redirectPort="8443" />


Create a ROOT context, add an index.html to the context and start Tomcat:

cd /app/acmeweb
mkdir webapps/ROOT
echo Acme Web > webapps/ROOT/index.html
bin/startup.sh


Logout as the acmeweb user, set the user to acmehr and create a .profile:

ctrl-d
su - acmehr
vi .profile


Put the following contents in the .profile:

export JAVA_HOME=/usr/local/java6
export CATALINA_HOME=/usr/local/tomcat6
export CATALINA_BASE=/app/acmehr


Logout and set user to acmehr again. Edit /app/acmehr/conf/server.xml:

ctrl-d
su - acmehr
vi /app/acmehr/conf/server.xml


Modify the server, AJP and HTTP connector ports and change them to 20205, 20209 and 20280 respectively (you may also consider turning off the AJP connector altogether):

<Server port="20205" shutdown="SHUTDOWN">
...
<Connector port="20280" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
...
<Connector port="20209" protocol="AJP/1.3" redirectPort="8443" />


Create a ROOT context, add an index.html to the context and start Tomcat:

cd /app/acmehr
mkdir webapps/ROOT
echo Acme HR > webapps/ROOT/index.html
bin/startup.sh


Open a browser and connect to the IP address of your Tomcat server, ports 20180 and 20280:

http://<tomcat server ip address>:20180/
http://<tomcat server ip address>:20280/


The first should display Acme Web in your browser, the second Acme HR. Both instances are running under their own user, these users can restart their own instance of Tomcat and the configuration of the server can be done by those users:

acmeweb 5295 1 0 23:02 ? 00:00:04 /usr/local/java6/bin/java -Djava.endorsed.dirs=/usr/local/tomcat6/endorsed -classpath :/usr/local/tomcat6/bin/bootstrap.jar:/usr/local/tomcat6/bin/commons-logging-api.jar -Dcatalina.base=/app/acmeweb -Dcatalina.home=/usr/local/tomcat6 -Djava.io.tmpdir=/app/acmeweb/temp org.apache.catalina.startup.Bootstrap start

acmehr 5333 1 0 23:05 ? 00:00:03 /usr/local/java6/bin/java -Djava.endorsed.dirs=/usr/local/tomcat6/endorsed -classpath :/usr/local/tomcat6/bin/bootstrap.jar:/usr/local/tomcat6/bin/commons-logging-api.jar -Dcatalina.base=/app/acmehr -Dcatalina.home=/usr/local/tomcat6 -Djava.io.tmpdir=/app/acmehr/temp org.apache.catalina.startup.Bootstrap start


In this setup the main Tomcat install directory in /usr/local/tomcat6 remains untouched. The trick is setting the CATALINA_HOME to the tomcat install in /usr/local/tomcat6 and CATALINA_BASE to the user specific Tomcat directory in /app/acmeweb and /app/acmehr.

Monday, November 19, 2007

Method Name in the URL File Name with Spring MVC's MultiActionController

Spring's MultiActionController is great for allowing a single controller to be able to handle multiple types of requests. What method to dispatch to is determined based on the method name resolver. You have several options here. E.g. with the ParameterMethodNameResolver you specify what method to call in the request parameters. You can specify that a certain request parameter, e.g. action, is going to contain the method name to dispatch to:

http://localhost:8080/app/someController.do?action=someMethod

The InternalPathMethodNameResolver resolves the controller from the path and the method from the file name without extension, e.g. the following URL would cause someMethod to be called on the controller. It basically strips the file name from the path (the part after the last slash) and strips the extension (.do):

http://localhost:8080/app/someController/someMethod.do

The PropertiesMethodNameResolver allows you to specify the method in a "value of the page" kind of way. E.g. you have someController.do and you add =someMethod to the URL in order for it to resolve to the controller and the method that is to be called:

http://localhost:8080/app/someController.do=someMethod

Besides the above strategies, one could also imagine a strategy where both the controller and the method are passed in the file name part of the path. The construct would contain the name of the controller, e.g. someController, suffixed with what method to call, e.g. .someMethod, suffixed with the extension, in this case .do:

http://localhost:8080/app/someController.someMethod.do

The above can also be extended to support dispatching to a default method in case no method name was given, e.g. one could specify someMethod to be the default and then call:

http://localhost:8080/app/someController.do

Spring does not have support for this scheme, but it can be easily extended to do so and this article will show how. The first thing to do is setup the web.xml file of your web application (I have created a dynamic web project in Eclipse 3.3 called SpringTests, so replace any references to this project with references to your own). First register the DispatcherServlet. In the example below the DispatcherServlet is registered under servlet name mvc:

<servlet>
  <servlet-name>mvc<servlet-name>
  <servlet-class>
  org.springframework.web.servlet.DispatcherServlet
  <servlet-class>
  <init-param>
    <param-name>contextConfigLocation<param-name>
    <param-value>
    classpath:com/acme/mvc.xml
    <param-value>
  <init-param>
  <load-on-startup>1<load-on-startup>
<servlet>


Please note in the above snippet the location of the Spring configuration file. It is on the classpath in com/acme/mvc.xml, so unless you are making an exact copy of the setup described here, refer to your Spring configuration file that is going to have the URL mappings of the DispatcherServlet. Next step is to add the servlet mapping. To stay inline with the example scenarios described we will map *.do to our mvc servlet:

<servlet-mapping>
  <servlet-name>mvc<servlet-name>
  <url-pattern>*.do<url-pattern>
<servlet-mapping>


The next thing we will need to do is create and register a controller. For this example, we will use a very simple controller called MyController that will have just two methods and that writes what method was called to the output stream. In our source folder we create a new package, com.acme, and add the MyController class. We will also be using the com.acme package to contain our Spring configuration file, mvc.xml:

package com.acme;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

public class MyController extends MultiActionController {

  public void defaultMethod(HttpServletRequest request,
    HttpServletResponse response) throws IOException
  {
    PrintWriter pw = new PrintWriter(response.getOutputStream());
    pw.print("Default method was called");
    pw.flush();
  }

  public void nonDefaultMethod(HttpServletRequest request,
    HttpServletResponse response) throws IOException
  {
    PrintWriter pw = new PrintWriter(response.getOutputStream());
    pw.print("Non default method was called");
    pw.flush();
  }
}


We will now create the mvc.xml file, which will register our controller, contain the DispatcherServlet URL mapping to our controller, but also the necessary configuration for our custom method name resolver class. First we will register our controller. Please note that we specify a methodNameResolver as being the defaultUrlMethodNameResolver. This will be a reference to our custom method name resolver which we will need to implement once we are done configuring. It is injected in our controller's methodNameResolver property (which is a property of MultiActionController), and any attempt to resolve which method name is to be used will be sent to our own implementation:

<bean id="myController" class="com.acme.MyController">
  <property name="methodNameResolver"
            
ref="defaultUrlMethodNameResolver" />
<bean>


The next step is to configure the method name resolver to point to our DefaultUrlMethodNameResolver class, which we will create later on in the com.acme package. We inject a default method into our method name resolver. This property will be used to return a default method name in case the URL does not contain one (e.g. someController.do). In our example we want the default method name to be defaultMethod:

<bean id="defaultUrlMethodNameResolver"
      class="com.acme.DefaultUrlMethodNameResolver">
  <property name="defaultMethod"
            
value="defaultMethod" />
<bean>


Now we add the URL mappings for the DispatcherServlet and map myController URL's to the myController bean. Note that we map to myController.* and not myController.*.do as the latter will force us to always specify a method name as it forces two dots in the file name. E.g. myController.do will fail as it does not match the latter pattern. The former pattern matches with both myController.do and myController.someMethod.do. Not adding the .do part in the pattern is not a big deal, as *.do is already configured in the servlet mapping in the web.xml above, meaning that not supplying the .do on the URL would not have been handled by the DispatcherServlet anyways.

<bean id="urlMapping"
  
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  <property name="mappings">
    <props>
      <prop key="/myController.*">myController<prop>
    <props>
  <property>
<bean>


We now get to the actual implementation of the method name resolver. We create the DefaultUrlMethodNameResolver in the com.acme package. As we want to inject a default method name we will need to have the defaultMethod property and a setter for this property. We will also need to implement the MethodNameResolver interface and implement the getHandlerMethodName method:

package com.acme;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

public class DefaultUrlMethodNameResolver implements MethodNameResolver {

  private String defaultMethod = null;

  public String getHandlerMethodName(HttpServletRequest request)
    throws NoSuchRequestHandlingMethodException
  {
    String path = request.getServletPath();
    String[] pathParts = path.split("\\.");
    if (pathParts.length > 2) {
      if (!"".equals(pathParts[1])) {
        return pathParts[1];
      } else {
        return defaultMethod;
      }
    } else {
      return defaultMethod;
    }
  }

  public void setDefaultMethod(String defaultMethod) {
    this.defaultMethod = defaultMethod;
  }
}


The above code splits the file name on the dot. The path parts array contains the different segments of the file name. If the size of the array is greater than two, we know that we have at least three parts in the file name, the first part being the controller name, the second the method name and the third being .do. We therefore return the second element of the array as the method name (pathParts[1]), unless this element were to be empty, in which case we return the default method (this is in order to handle e.g. myController..do). If we had only two elements in the array, we have something like myController.do and thus return the default method. We can now run this web application on a server and try out a couple of URL's to see how it works.

http://localhost:8080/SpringTests/myController.do results in "Default method was called".

http://localhost:8080/SpringTests/myController..do results in "Default method was called".

http://localhost:8080/SpringTests/myController.defaultMethod.do results in "Default method was called".

http://localhost:8080/SpringTests/myController.nonDefaultMethod.do results in "Non default method was called".

So here we have a strategy that allows both the controller and the method name to dispatch to in the file name part of the URL.

Tuesday, November 13, 2007

Solving java.io.FileNotFoundException in Spring and iBATIS

I'm happily hacking about with iBATIS Spring DAO classes. I exposed the DAO classes using RPC services with GWT and the GWT server library and I run these RPC services in Tomcat. I also wanted to have unit tests that test my DAO classes, and there is this wonderful thing called gienah-testing on Google code, that allowed me to use Spring to inject the DAO classes into my unit test classes.

Now at this stage I started to get a FileNotFoundException on either my Spring configuration files or my SqlMapConfig.xml. When I referred to the configuration files using the scheme "/com/acme/config.xml", this caused loading the SqlMapConfig configuration to fail when running in Tomcat. I could solve the problem in Tomcat by referring to "WEB-INF/classes/com/acme/config.xml", but this caused my gienah-testing unit test classes to fail with a FileNotFoundException on the Spring configuration.

To solve this problem, I had to make sure that everything was loaded from the classpath using the ClassPathXmlApplicationContext. This is very simple, just prefix the location of the configuration file with "classpath:" without the quotes, e.g. "classpath:/com/acme/config.xml". This way both the gienah-test unit tests will load the configuration from the classpath using the classloader (e.g. in /classes if that were to be your output folder in Eclipse) and the web container will also load the configuration from the classpath using the classloader (in this case WEB-INF/classes). Here is a snippet of the gienah-testing test class, where data.xml is the configuration file containing the iBATIS Spring DAO configuration:

@RunWith(value = SpringRunner.class)
@Configuration(locations = {"classpath:/com/acme/data/data.xml"
"classpath:/com/acme/test/test.xml"})
public class AcmeTest {


All other configuration files also refer to any underlying configuration files using the "classpath:" prefix, again in order to make sure that the ClassPathXmlApplicationContext is used. This is a snippet from the web.xml, injecting data.xml into the application context configuration file location:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:/com/acme/data/data.xml</param-value>
</context-param>


The data.xml containing the iBATIS Spring DAO configuration injects the SqlMapConfig.xml, also with the "classpath:" prefix, as shown in the snippet below:

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="dataSource" ref="myDataSource" />
    <property name="configLocation" value="classpath:/com/acme/data/SqlMapConfig.xml" />
</bean>


This should solve any FileNotFoundException problems in both the gienah-testing unit tests and when running in Tomcat. It works like a charm for me :-)

Sunday, October 28, 2007

Mapping PostgreSQL Arrays with iBATIS

I needed to map a PostgreSQL integer array column to a Java integer array using iBATIS. The following is what seems to do the trick. Suppose you have a table with an integer array:

create my_table (
  id integer primary key,
  array_column integer[]
);

And some data was inserted using e.g. the following insert statement:

insert into my_table (id, array_column) values (1, array[1, 2, 3]);

Now lets have a Java class that can map to the above table, as follows:

public class MyTable {

  private int id = 0;
  private int[] arrayColumn = null;
  
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
  public int[] getArrayColumn() { return arrayColumn; }
  public void setArrayColumn(int[] arrayColumn)
    { 
this.arrayColumn = arrayColumn; }
  
}

We will now specify a select map, and we pass a result map to the select which is going to contain the mappings from the various columns to the properties in the Java bean.

<select id="getMyTable" parameterClass="int" resultClass="MyTable"
        resultMap="myTableResult">
    select id, array_column from my_table where id = #id#
</select>

The result map that we refer to in the resultMap attribute in the above select map is specified below:

<resultMap id="myTableResult" class="MyTable">
    <result property="id" column="id" />
    <result property="arrayColumn" column="array_column" jdbcType="ARRAY"
            
javaType="java.sql.Array" typeHandler="ArrayTypeMapper" />
</resultMap>

In this result map, we map the id column to the id property and the array_column column to the arrayColumn property. There is however additional configuration needed for this second mapping. We specify that the JDBC type is ARRAY (as in java.sql.Types.ARRAY). We also specify that the Java type is java.sql.Array, but more importantly we specify the typeHandler attribute, which refers to the handler that is going to handle this array, and it is called ArrayTypeMapper. It implements TypeHandlerCallback, and its contents are as follows:

import java.sql.Array;
import java.sql.SQLException;

import com.ibatis.sqlmap.client.extensions.ParameterSetter;
import com.ibatis.sqlmap.client.extensions.ResultGetter;
import com.ibatis.sqlmap.client.extensions.TypeHandlerCallback;

public class ArrayTypeMapper implements TypeHandlerCallback {

  public void setParameter(ParameterSetter setter, Object parameter
        throws SQLException {
    throw new UnsupportedOperationException("Not implemented");
  }

  public Object getResult(ResultGetter getterthrows SQLException {
    Array array = getter.getResultSet().getArray(getter.getColumnName());
    if (!getter.getResultSet().wasNull()) {
      return array.getArray();
    else {
      return null;
    }
  }

  public Object valueOf(String s) {
    throw new UnsupportedOperationException("Not implemented");
  }
}

It is very convenient that the getter object has the column name available (so we can avoid having to explicitely name the column or use indexes). Do not get the array from the array object immediately, you should first check whether is was null. If that was the case, return null (causing the MyTable object's array to be set to null), otherwise return array.getArray(). This will automatically cause the int[] in MyTable to be filled with the contents of the array column of my_table.

Saturday, October 20, 2007

Smart Version Class with Version Increment Validation and Suggestion

For a build management system I am working on, I want a smarter way of doing version numbering for versioned builds. Right now a user can enter any version number, but I would like the system to suggest a next version number. E.g. if the last versioned build had a version number 1.2.3, I would like the system to suggest 1.2.4 as next version number when a new versioned build is started. I would also like to give a warning when a user tries to go from 1.2.3 to 1.2.5, skipping a number.

For this I have created a "smart" version class that works with numerical version numbers only and that can have any amount of version digits. Such version numbers include 1.2.3, 2.1.0, 2.33.24.18.65, etc. Valid transitions of one version number to another are from 1.2.3 to 1.2.4, 1.2.3 to 1.3.0, 1.2.3 to 1.2.3.0, 1 to 2, 1.2.3 to 2, 1.2.3 to 2.0, 1.2.3 to 2.0.0, 1 to 2.0, 2.0 to 3. The basics of this smart version class is an integer array, although that is quite the overkill, and a byte would probably have been enough (640KB anyone?). The constructor of this class takes an integer array, and per version class I want the array to become immutable (if you need a new version, instantiate a new version object).

public class Version {

  private final int[] versionDigits;
  
  public Version(int[] versionDigits) {
    if (versionDigits == null) {
      throw new IllegalArgumentException("versionDigits is null");
    }
    if (versionDigits.length == 0) {
      throw new IllegalArgumentException("versionDigits length is 0");
    }

    this.versionDigits = versionDigits;
  }

To complement the foundation, a helper method is added that is able to parse a version string such as "1.2.3" into an array of digits. This just makes our life easier when we have version strings and want to convert them into a version object. This is done by the parse method.

  public static Version parse(String versionString) {
    if (versionString == null) {
      throw new IllegalArgumentException("versionString is null");
    }
    
    StringTokenizer st = new StringTokenizer(versionString, ".");
    int[] versionDigits = new int[st.countTokens()];
    int i = 0;
    while (st.hasMoreTokens()) {
      versionDigits[i++= Integer.valueOf(st.nextToken()).intValue();
    }
    return new Version(versionDigits);
  }

Now we will get to the interesting stuff. The following method checks whether a transition was a valid one and returns true if so, otherwise false.

  public boolean isValidNextVersion(Version newVersion) {
    int i = 0;
    for (; i < versionDigits.length; i++) {
      if ((newVersion.getVersionDigits()[i- versionDigits[i]) == 1) {
        i++;
        break;
      else if (newVersion.getVersionDigits()[i!= versionDigits[i]) {
        return false;
      else if ((i + 1== newVersion.getVersionDigits().length) {
        return false;
      }
    }
    for (; i < newVersion.getVersionDigits().length; i++) {
      if (newVersion.getVersionDigits()[i!= 0) {
        return false;
      }
    }
    return true;
  }

The above method deserves some explanation. It goes through all the digits and first checks whether the difference is positive one (new digit - old digit = 1). If that's the case, then we have found the one digit that changed and that it had increased with one. We will then need to increase the position in the array with one, break out of the initial loop and start checking whether the remainder of digits are zero in the new version number. E.g. if the second digit in 1.2.3 was increased by one, the resulting version number must be 1.3.0.

If there was no difference of one, then the new digit and the old digit must be the same (e.g. in the case of the second digit in 1.2.3 to 1.2.4). If that is not the case (new digit != old digit), we know one of the digits did an invalid transition, e.g. from 1.2.3 to 1.4.0, and we can safely return false.

The last condition in the first loop checks whether the next run in the loop would cause the array index to be out of bounds for the new version number. As the first check did not break yet, we are pretty confident that the version number remained the same, e.g. from 1.2 to 1.2, or went backward losing digits, e.g. from 1.2.3 to 1.2. The condition that the old version number has less digits than the new version number is already covered by the fact that the first for loop loops through the digits of the old version number.

The second loop in the method just checks whether all remaining digits in the new version number are zeros. If that is not the case, we would have gone from e.g. 1.2.3 to 1.2.3.1 or 1.2.3 to 1.2.4.1. Return false if that's the case for any of these digits, else we can safely say the version number transition was valid and return true. As a bonus we can implement a method that suggests a new version. Because we do not want the old version number's digits to be touched, we first create a getVersionDigits method that returns a copy of the internal array:

  public int[] getVersionDigits() {
    int[] newVersionDigits = new int[versionDigits.length];
    System.arraycopy(versionDigits, 0, newVersionDigits, 0,
        versionDigits.length
);
    return newVersionDigits;
  }

The method that suggests the new version will actually return a new version object with the suggested version digits. This is done by calling the getVersionDigits method, increasing the last digit of that array by one and returning a new version number object with the given array, e.g. in the case of 1.2.3 the suggested new version is 1.2.4. Variations on this theme could include a mechanism that suggests valid transitions for any of the digits, e.g. in the case of 1.2.3 the new suggested version number would be 1.3.0.

  public Version getNextSuggestedVersion() {
    int[] newVersionDigits = getVersionDigits();
    newVersionDigits[newVersionDigits.length - 1]++;
    return new Version(newVersionDigits);
  }
}

This class is in no way set in stone. Variations on this class could be returning the position of the invalid digit transition (e.g. digit 2 did an invalid transition) or something that gives back reason codes as to why a transition was invalid (e.g. numbers are the same, new version is smaller than old version, etc.), or the ability to specify whether version numbers are zero based or one based (e.g. making 1.2.3 to 1.2.4.1 a valid transition and suggesting version numbers likewise). Maybe it is desirable to have something like a suffix as well, and requiring the suffix to change if the version number remains the same (e.g. 1.2.3 RC1, 1.2.3 final, etc.).

This can all be put into place in the code shown above. At least this class is a starting point that takes care of the nitty gritty details of the algorithm that does the validation on the version digit transitions. Although something like validating version transitions sounds simple, suffice to say my initial attempt consisted of 6 for loops and 24 if branches and that I am very happy to have been able to bring this down to what I have now.

Saturday, October 13, 2007

Testing Java2Html with Eclipse Europa and Blogger.com

Although the plugin is a bit dated (version 1.5.0 dates from January 2006), the Java2Html Eclipse plugin seems to be working with the Eclipse 3.3 Europa fall maintenance release (and thus very likely works with all 3.x versions). I downloaded and extracted the plugin to the plugins directory, restarted with the -clean option and I had a new context menu, called "Java2Html". And now for a little test to see what it'll look like here:


/**
 * Type comment
 */
public class Test3 {
  public static void main(String[] args) {
    // Some comments
    System.out.println("Java2Html test");
  }
}


I had to manually remove the <br /> elements as they caused empty lines and obviously I had to remove the html head, body, etc. I also got rid of the table and the div that were generated. Although I made these manual changes afterwards, I still think it's a nice little plugin that'll save lots of time when you want to generate syntax highlighted code from within Eclipse. Obviously this isn't just handy for posting, but it would work great in JavaDoc comments as well (albeit not very readable in the comment itself of course ;-))

w.bloggar blogger

Ha, got w.bloggar to work with Blogger. Seems I upgraded to the beta, and for that I am to do something a little different than just selecting the "Blogger" option in the account setup wizard. The relevant information can be found here. As always, Google is your friend, as searching for w.bloggar blogger reveals the relevant link as the first result. I'm certainly feeling lucky :-)

Thursday, July 26, 2007

Epitaph for a Certificate

Here lies wide open, expired it begot
A certificate, which obfuscated a lot

Glacial Methodology














This stone on which I stand has been carved by glaciers over tens of thousands of years, in a time long ago when our lands were covered with ice, close to the time it takes to get something into production over here.

http://www.ambysoft.com/essays/glacialMethodology.html
http://www.waterfall2006.com/ambler2.html

Ah well... I'm not in a hurry either I guess.