Friday, February 18, 2011

Quick Tip on Logging with Java on Google App Engine

I'm in the process of messing around with some really cool stuff on Google App Engine (wonder why ...) and I ran into something that I didn't see covered in the docs (which as a rule are excellent). I came across the solution in a sample app for GAE that solved the problem so I thought I'd share.

Setting up logging in a servlet is pretty straight-forward, particularly if you're using the Eclipse Plugin for GAE since it more or less configures it for you. Here's a quick example. This is dummy code obviously, but pay attention to the package delcaration.

package org.opencfsummit;

import  java.util.logging.Logger;

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

public class MyServlet extends HttpServlet {

    private static final Logger log = Logger.getLogger(MyServlet.class.getName());

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // do whatever else your doPost method needs to do here, and write some info to the log"I'd like to write this to the log, please.");



That all seemed well and good, but when I looked for my log entries in the GAE admin console, nothing was getting logged.

Well, turns out that even though the Eclipse plugin sets up the basic logging configuration for you, there's a bit of additional configuration needed related to the package in which the class you're logging from resides.

The default WEB-INF/ file contains this:

# Set the default logging level for all loggers to WARNING
.level = WARNING

Now what didn't make sense to me until I found an example and tried it was why my calls weren't doing anything. Maybe I'm missing something, but I had to add this to to get things to log:

org.opencfsummit.level = INFO

Now why if you're explicitly calling it wouldn't log the information at the info logging level I'm not sure, but adding my package name and a level to definitely took care of the issue. 

Tuesday, February 15, 2011

Dynamically Invoking Method Names On a Java Object From CFML

A co-worker contacted me today asking how he might go about solving what turned out to be a rather interesting issue.

From a CFML application (running on Open BlueDragon) he's calling a .NET web service and getting an array of objects back. By the time all the SOAP magic happens the objects in the array on the CFML side are Java objects.

What he wanted to do next was loop over this array of Java objects and, for each object in the array, call every getXXX() method contained in the object. But the application is getting numerous different types of objects back, some of which have a large number of get methods in them, and he didn't want to have to hard-code a method call for each one. In addition, the get methods may change from time to time and while we're supposed to be notified when changes occur, we didn't want to rely on that.

So consider the following pseudocode:

<cfinvoke webservice="url to webservice"
    returnvariable="myJavaObjects" />

So at this point the variable myJavaObjects is an array of homogeneous Java objects.

Next, we want to loop over that array and for each Java object, call all of its get methods.

My first thought was that this is one of those rare cases where Evaluate() might be justified. But I also thought there had to be a better way, so perhaps against my co-worker's will we spent about an hour hammering through some experiments. I'll spare you the various things we tried and cut to the chase of the final solution.

One thing I learned while working through this is that CFINVOKE works on Java objects. Who knew? OK, maybe you knew, but I hadn't ever had cause to try it before so I didn't know.

So step one is once we get the array of Java objects back from the web service, since we know they're homogeneous objects, we'll just use some Java reflection magic on the first one to create an array of the method names beginning with get:

<!--- this returns an array of java.lang.reflect.Method objects --->
<cfset methods = myJavaObjects[1].getClass().getMethods() />
<!--- now we'll create an array of method names starting with get --->
<cfset methodNames = [] />
<cfloop array="#methods#" index="method">
    <cfif Left(method.getName(), 3) == "get">
        <cfset ArrayAppend(methodNames, method.getName()) />

So now we have an array of strings that are the method names from the Java object that start with get. In the actual application we're actually omitting some of the methods starting with get because they're not relevant (e.g. getClass(), getSerializer(), etc.) so I'm just keeping it simple for the purposes of illustration.

The next step is to loop over the array of Java objects, and on each loop iteration, call each get method and for demo purposes simply output the results. Here's where we use CFINVOKE to call methods dynamically on the Java objects:

<cfloop array="#myJavaObjects#" index="javaObject">
    <cfloop array="#methodNames#" index="methodName">
        <cfinvoke component="#javaObject#" method="#methodName#" returnvariable="foo" />
        <cfoutput>#foo#<br /></cfoutput>

And with that, we're getting an array of Java objects back from a .NET web service (with the .NET object to Java object translation being handled transparently by the web service engine of course), we're using a bit of Java reflection to get a list of the getters from the Java object, and we're then looping over the array of Java objects and calling the get methods on each Java object.

As an aside, during experimentation we went down the path of using Java reflection directly, but that got pretty messy and didn't seem to offer any benefit over doing things at a higher level in CFML. Interestingly, while we were messing with some things we had an error generated from CFINVOKE reminding me that under the hood, CFINVOKE is doing all the Java reflection nastiness for you.

Not sure how handy a tip that will be for others but I wanted to blog it while it was still fresh in my mind. There's probably several other ways to solve this problem so if others have approached this differently I'd love to hear about it.