Monday, May 21, 2007

Avoid Copy & Paste with Ant MacroDef

Keywords:
ant java copy paste macrodef gwt compile task

Problem:
There's no convenient ant task to allow you to compile (or re-compile) the GWT client files, but you can get some decent up-to-date check & compile behaviour going with a few lines of ant

... problem is, you need to duplicate these lines of code for every module in the project. It would be nice if you could modularise this.

Solution:
The Ant "macrodef" task lets you define a sequence of ant steps in a parameterised way and then call it any number of times with different parameters.

As can be seen in the example below (which handles 2 modules), adding a new module will require:
  1. add a new entry to the "depends" list for gwt-generate (eg 'gwt-generate-newmod')
  2. create a gwt-compile target 'gwt-generate-newmod' with an "unless" attribute of 'gwtGenerate.newmod.notRequired'
  3. create a gwt-uptodate target to set the property 'gwtGenerate.newmod.notRequired' accordingly.
Yes, there's still some copy and paste, but you're just defining the module name and location rather that repeating the gwt-compile definition so there's less chance for a mistake.
<!-- this is the root target which will do all the generation (if required) -->
<target name="gwt-generate" description="Generate GWT code"
  depends="gwt-generate-common, gwt-generate-demo"/>

<target name="gwt-generate-common" depends="compile, gwt-check" unless="gwtGenerate.common.notRequired">
    <gwt-compile module="com.example.gwt.common.MyCommonModule"/>
</target>
<target name="gwt-generate-demo" depends="compile, gwt-check" unless="gwtGenerate.demo.notRequired">
    <gwt-compile module="com.example.gwt.demo.MyDemoModule"/>
</target>

<!-- this sets the *.notRequired properties if gwt code is uptodate -->
<target name="gwt-check">
  <!-- won't have any effect if the dirs are already there -->
  <mkdir dir="${build.home}/gwt"/>
  <mkdir dir="${build.home}/gwt/tmp"/>
  <mkdir dir="${build.home}/gwt/out"/>

    <gwt-uptodate property="gwtGenerate.common.notRequired"
            module="com.example.gwt.common.MyCommonModule"
            package="src/com/example/gwt/common"/>

    <gwt-uptodate property="gwtGenerate.demo.notRequired"
                    module="com.example.gwt.demo.MyDemoModule"
                    package="src/com/example/gwt/demo"/>          
</target>

<!-- this defines a macro task 'gwt-uptodate' -->
<macrodef name="gwt-uptodate" description="sets a property indicating if the module is up to date">
 <attribute name="property"/>
 <attribute name="module"/>
 <attribute name="package"/>
 <sequential>
         <!-- the module is uptodate if the .nocache.html is not older than any of the module src files -->
      <uptodate property="@{property}"
              targetfile="${build.home}/gwt/out/@{module}/@{module}.nocache.html">
          <srcfiles dir="@{package}" includes="**/*.*"/>
      </uptodate>                      
 </sequential>
</macrodef>  
<!-- this defines a macro task 'gwt-compile' -->
<macrodef name="gwt-compile" description="generates the GWT client code">
 <attribute name="module"/>
 <sequential>
         <!-- you must fork or it will fail -->
         <java dir="${build.home}/gwt/tmp" classname="com.google.gwt.dev.GWTCompiler" fork="true">
          <classpath>
              <!-- src directory containing module definition must be first! -->
              <pathelement location="src"/>
              <pathelement location="${build.home}/classes"/>
              <pathelement location="${gwt.user}"/>
              <pathelement location="${gwt.dev}"/>
              <pathelement location="${gwt.widgets}"/>
            </classpath>
          
          <sysproperty key="java.awt.headless" value="true"/>             
          <arg value="-out"/>
          <arg value="../out"/>
          <arg value="@{module}"/>  
      </java>
 </sequential>
</macrodef>


Notes:
Note, you only need to GWT-compile modules with an EntryPoint class in them. This is because it's only EntryPoint classes that you need to reference from your HTML and on compilation all the classes (and files in com.example.gwt.package/public!) are compiled/copied to this module. This means if you have multiple EntryPoint modules in your project (as I do) that reference a common Module, they will have their own compiled copy of this (and the /public files) rather than referencing it in a common location.

Note also, this means that the gwt-uptodate won't trigger a recompile if there's a change to a referenced module - as it's only checking the source folder containing the EntryPoint module ... so in those cases, you'll need to do a clean of the build folder containing generated source to guarantee everything is compiled up to date.

New Note: the sysproperty to set awt to headless is crucial on linux (if you're using ImageBundles). See Does the GWT 1.4 Compiler Need an X11 Window in Linux?

Monday, May 07, 2007

JavaMail Exception 550 relaying mail is not allowed

Keywords:
JavaMail Exception 550 relaying mail to is not allowed

Problem:
Running server side code that sends an email is being rejected by the SMTP server:

javax.mail.SendFailedException: Invalid Addresses;
  nested exception is:
        com.sun.mail.smtp.SMTPAddressFailedException: 550 relaying mail to example.com is not allowed
        at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:1196)
        at com.sun.mail.smtp.SMTPTransport.sendMessage(SMTPTransport.java:584)
        at javax.mail.Transport.send0(Transport.java:169)
        at javax.mail.Transport.send(Transport.java:98)


Contrary to the error message, its not an issue isolated to the "To" recipient address. No matter what the "To" field was, the relay error was the same with the ???.com substituted in.

Other posts/support-resources on the web point to issues with authentication - but this SMTP server does not require authentication, just that the host is one of a set of IP addresses. Other web applications on the same host, running in the same (tomcat) application server are sending mail to this SMTP server with no problems.

Solution:
In this case, the difference was the spec of the JavaMail libraries. The apps that were working were all using spec 1.2 (deployed with activation.jar, mail.jar and mailapi.jar).

Our app getting the relay error was using spec 1.3 (implementation version 1.4). Downgrading the libraries (ie putting the 3 older jars in WEB-INF/lib) resolved the problem - no more errors from the SMTP server.

There may be a change in spec to do with the way the host is identified. If the libraries were identifying the client code's host as anything other than the IP address then this may be the problem. Perhaps the new libraries require additional properties to configure this behaviour? For the moment things are working but I may get to the bottom of this later ...