Sunday, June 19, 2011

Unique Constraint in AppEngine Datastore

When you come to AppEngine with a relational database background, it is natural to want a unique constraint on a set of entities. You have a field that must be unique across all your entities of that type. You want to create one and only one of such entities in the face of race conditions.

The AppEngine Datastore has no unique constraint feature per se, but you can manually employ such a mechanism on your entities as you add them to the datastore. To do so requires these things:

  • You must use a transaction. (You probably expected this.)
  • You must use an ancestor query (using the transaction) to test if the entity already exists.
  • You must have a parent entity (to form an entity group).
  • You must utilize your unique field as the entity key as well. (Yes, that data will “exist twice” in the entity.)

 

A Demonstration

I don’t know about you, but when it comes to fundamental behavior such as this, I find it extremely critical to:

  1. understand how it behaves on the system (e.g. with Datastore, or Objectify, etc.)
  2. code it correctly.
  3. prove the coded implementation behaves correctly.

So I’ve built an isolated test case to demonstrate “unique constraint” behavior with Datastore (and with Objectify), and ultimately produced a canonical example of how to implement a “unique constraint” for Datastore on AppEngine. The live demo is at http://gaetestjig.appspot.com/ on the “Unique Constraint” tab. This demo shows two users “Alice” and “Bobby” trying to reserve the same seat in a reservation system. The “winning” request will write the entity to the Datastore while the other will fail.

datastore_unique_constraint_demo2

In the demo, you can alternate clicking “Advance Alice” and “Advance Bobby” buttons, moving each user closer to reserving the same seat. Once a seat has been reserved, click the “Reset Test” button, then you can experiment further.

What you will see is that two parties can find no pre-existing entity, each create their own new entity (with the same key) and not until “commit” time will one of them discover that the other has modified the entity group, and then fail with a java.util.ConcurrentModificationException. Since a ConcurrentModificationException is “normal”, you would retry your seat reservation, and you would subsequently discover there is a pre-existing entity and you will not be able perform your insert.

Note about demo app: On appengine, the underlying transaction has a timeout of approx 30+ seconds, so you need to step through all your “Advance” clicks within this timeframe, or you’ll lose your transaction and get an IllegalStateException. Furthermore, because this appengine app is not visited frequently, your visit will probably be a “cold start”, so you could easily hit this exception upon your first attempt to perform a test. Just try it again right away and it will work.

 

How to implement the “Unique Constraint”

One thing for certain will be unique per entity: its key. So we use our “seatId” as the entity key and we’re basically done. There can only be one such entity with that key. However, anyone can overwrite an entity by using the same key when they call put().

So we want to isolate our “inserts” in a transaction, and do a query inside that transaction to search for an entity that may already exist from a previous claim, and if we find such an entity, then we tell the user they were too late. (In the fine print of “What Can Be Done In a Transaction”, you will find that the only kind of queries you may perform in a transaction are “ancestor queries”, i.e., a query that has an ancestor filter.)

So this means our Seat entity must have a parent, so that all the Seat entities live in the same entity group. Remember we’re trying to stay unique across *all* Seat entities, so it makes sense that we need a common parent to blanket them all in an entity group. Datastore transactions need to be told the scope of what they can “lock”, and entity groups are that scope. (There’s really no locking; just checking of modification timestamps…)

So we have to create a single SeatRoot entity which will act as the parent of all our Seat entities. (If you have another resource with the potential of millions of “Seats”, then you could break down the SeatRoots into multiple roots so that each entity group has a reasonable amount of members.)

Here’s what the seat reservation code looks like:

package gae.testjig.server;
import java.util.ConcurrentModificationException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
public class ConstraintExample {
    static private Key seatsRootKey;
    static {
        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        Entity rootEntity = new Entity("SeatsRoot", "seats_root_key_name"); // we want only one of these entities to ever exist
        ds.put(rootEntity);
        seatsRootKey = rootEntity.getKey();
    }
    
    public void reserveSeat(String ownerName, String seatId) throws DuplicateException {
        for (int i=0;i<10;i++) {
            try {
                reserveSeatAttempt(ownerName, seatId);
                return; // we get here if reservation succeeds
            }
            catch (ConcurrentModificationException cme) {
                // stay in the loop and try again.
            }
            // you could use another backoff algorithm here rather than 100ms each time.
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
        throw new ConcurrentModificationException("failed to reserve seat "+seatId);
    }
    private void reserveSeatAttempt(String ownerName, String seatId) throws DuplicateException, ConcurrentModificationException {
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        Transaction txn = datastore.beginTransaction();
        try {
            Query testExistsQuery = new Query("Seat");
            testExistsQuery.setAncestor(seatsRootKey);
            testExistsQuery.addFilter("seatId", Query.FilterOperator.EQUAL, seatId);
            Entity exists = datastore.prepare(txn, testExistsQuery).asSingleEntity();
            if ( exists != null ) {
                throw new DuplicateException("seatId "+seatId+" already exists.");
            } else {
                Entity seatEntity = new Entity("Seat", seatId, seatsRootKey);
                seatEntity.setProperty("ownerName", ownerName);
                seatEntity.setProperty("seatId", seatId);
                seatEntity.setProperty("timeStamp", System.currentTimeMillis());
                datastore.put(txn, seatEntity);
                txn.commit(); // throws java.util.ConcurrentModificationException if entity group was modified by other thread
            }
        }
        finally {
            if (txn.isActive()) {
                txn.rollback();
            }
        }
    }
}

 

And here is what it looks like with Objectify:

package gae.testjig.ofy.dto;
import javax.persistence.Id;
import com.googlecode.objectify.annotation.Unindexed;
@Unindexed
public class OSeatsRoot {
    
    @Id private String entityId;
    
    public OSeatsRoot() {}
    
    // we want just one root, with entityId = "seats_root_key_name"
    public OSeatsRoot(String entityId) {
        this.entityId = entityId;
    }
    public void setEntityId(String entityId) {
        this.entityId = entityId;
    }
    public String getEntityId() {
        return entityId;
    }
}

 

package gae.testjig.ofy.dto;
import javax.persistence.Id;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Indexed;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.annotation.Unindexed;
@Unindexed
public class OSeat {
    
    @Id private String entityId;
    @Parent private Key<OSeatsRoot> seatRootKey;
    
    @Indexed private String seatId;
    private String ownerName;
    private long timeStamp;
    public OSeat() {}
    
    public OSeat(Key<OSeatsRoot> seatRootKey, String seatId, String ownerName) {
        this.entityId = seatId;
        this.seatRootKey = seatRootKey;
        this.seatId = seatId;
        this.ownerName = ownerName;
        this.timeStamp = System.currentTimeMillis();
    }
    
    public String getEntityId() {
        return entityId;
    }
    public void setEntityId(String entityId) {
        this.entityId = entityId;
    }
    public Key<OSeatsRoot> getSeatRootKey() {
        return seatRootKey;
    }
    public void setSeatRootKey(Key<OSeatsRoot> seatRootKey) {
        this.seatRootKey = seatRootKey;
    }
    public String getSeatId() {
        return seatId;
    }
    public void setSeatId(String seatId) {
        this.seatId = seatId;
    }
    public String getOwnerName() {
        return ownerName;
    }
    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }
    public long getTimeStamp() {
        return timeStamp;
    }
    public void setTimeStamp(long timeStamp) {
        this.timeStamp = timeStamp;
    }
}

 

package gae.testjig.ofy.dao;
import gae.testjig.ofy.dto.OSeat;
import gae.testjig.ofy.dto.OSeatsRoot;
import gae.testjig.server.ChannelLogger;
import gae.testjig.server.DuplicateException;
import java.util.ConcurrentModificationException;
import com.google.appengine.api.datastore.KeyFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Query;
public class ODaoSeats {
    
    static private Key<OSeatsRoot> seatsRootKey;
    static {
        ObjectifyService.register(OSeatsRoot.class);
        ObjectifyService.register(OSeat.class);
        
        Objectify ofy = ObjectifyService.begin();
        OSeatsRoot rootEntity = new OSeatsRoot("seats_root_key_name");
        ofy.put(rootEntity);
        seatsRootKey = new Key<OSeatsRoot>(OSeatsRoot.class, rootEntity.getEntityId());
    }
    
    static public void reserveSeat(ChannelLogger logger, String ownerName, String seatId) throws DuplicateException {
        logger.info(ownerName+": BEGIN");
        for (int i=0;i<10;i++) {
            try {
                reserveSeatAttempt(logger, ownerName, seatId);
                logger.info(ownerName+": END");
                return; // we get here if reservation succeeds
            }
            catch (ConcurrentModificationException cme) {
                logger.info(ownerName+": EXCEPTION java.util.ConcurrentModificationException");
                // stay in the loop and try again.
            }
            // you could use another backoff algorithm here rather than 100ms each time.
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
        logger.info(ownerName+": ABORT");
        throw new ConcurrentModificationException("failed to reserve seat "+seatId);
    }
    static private void reserveSeatAttempt(ChannelLogger logger, String ownerName, String seatId) throws DuplicateException, ConcurrentModificationException {
        logger.info(ownerName+": beginning transaction.");
        Objectify ofy = ObjectifyService.beginTransaction();
        logger.info(ownerName+": transactionId="+ofy.getTxn().getId());
        try {
            logger.info(ownerName+": test for existence of entity with seatId=" + seatId);
            Query<OSeat> testExistsQuery = ofy.query(OSeat.class).ancestor(seatsRootKey).filter("seatId =", seatId);
            OSeat exists = testExistsQuery.get();
            if ( exists != null ) {
                logger.info(ownerName+": sorry, that seat has already been taken.");
                throw new DuplicateException("seatId "+seatId+" already exists.");
            } else {
                logger.info(ownerName+": that seat has not been taken yet.");
                logger.info(ownerName+": create the seat entity with seatId=" + seatId);
                OSeat seat = new OSeat(seatsRootKey, seatId, ownerName);
                Key<OSeat> key = ofy.put(seat);
                logger.info(ownerName+": created seat. entity key = " + KeyFactory.keyToString(key.getRaw()));
                ofy.getTxn().commit(); // throws java.util.ConcurrentModificationException if entity group was modified by other thread
                logger.info(ownerName+": transaction committed.");
            }
        }
        finally {
            if (ofy.getTxn().isActive()) {
                ofy.getTxn().rollback();
            }
        }
    }
    
    static public String fetchSeatInfo(String seatId) {
        Objectify ofy = ObjectifyService.begin();
        Query<OSeat> testExistsQuery = ofy.query(OSeat.class).filter("seatId =", seatId);
        OSeat exists = testExistsQuery.get();
        if ( exists != null ) {
            return "Found seat entity: seatId="+exists.getSeatId()+", ownerName="+exists.getOwnerName()+", timestamp="+exists.getTimeStamp();
        } else {
            return "There is no entity in the datastore with seatId="+seatId;
        }
    }
    
    static public void deleteSeatEntity(String seatId) {
        Objectify ofy = ObjectifyService.begin();
        Key<OSeat> key = new Key<OSeat>(seatsRootKey, OSeat.class, seatId);
        ofy.delete(key);
    }
    
}

 

You can find all this source code here.

Limitations: You can only have a single “unique constraint” on your entity. The pre-exists query is an ancestor query, which forces us to search by entity parameters rather than just lookup the entity by its key. Likewise, those entity parameters must correspond directly with the unique key of the entity. It is ultimately the key that is unique, and you can only have one of them. This is also why we end up with “duplicated data” in our entity, i.e. the seatId field is also used as the entityId separately.

Tuesday, May 17, 2011

Sharing Counter alternative: Pull Queues on App Engine

At Google I/O this year I learned about Pull Queues, now available on App Engine. During the session it occurred to me that this might be an alternative method to gathering counter statistics without using a sharding counter. Just send your stats to a Pull Queue and let another periodic task aggregate and store the data where it belongs.

Clearly the appropriateness of this depends on your application, but it seems to me that statistics gathering would be suitable.

Okay, so I’ll have to try it out, see how it works, and post some code!

Wednesday, May 4, 2011

Adventures in Tristate Checkboxes, Closure, and GWT

For the last week I have been trying to find or create an HTML widget similar to the GMail ‘labels’ menu button, e.g., like this:

gmail-tristate-menu-button(Hey, I found something useful for the contents of my spam folder… to make this screenshot!) 

 

Today I have succeeded with a simple drop-down menu button containing a list of tristate checkboxes. I used (and hacked) the closure library to achieve this. Below is a screen shot of my drop-down menu button from my working demo.

tri-state-menu-button

I compiled these button widgets into a standalone javascript file, which gives some very rich functionality with a pretty small “surface area” API. You just pass in the id of a div tag where you want the menu button, a list of group names, and a callback function to receive changes. The source for the demo menu button is very straightforward. You could copy that buttonlib.x.js file and those css files, and be off and running.

<html>
  <head>
    <script src="buttonlib.x.js"></script>
    <link rel="stylesheet" href="../closure-library/closure/goog/css/common.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/custombutton.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/menu.css"> 
    <link rel="stylesheet" href="../closure-library/closure/goog/css/menubutton.css">
    <link rel="stylesheet" href="../closure-library/closure/goog/css/tristatemenuitem.css">
  </head>
  <body>
    <div style="font-size: 11px;"> 
          <div id="btn1" class="goog-custom-button goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">delete Freds</div>
        </div><div id="btn2" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">set Freds to mixed</div>
        </div><div id="btn3" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">set Pennys to checked</div>
        </div><div id="btn4" class="goog-custom-button goog-custom-button-collapse-left">
            <div style="padding: 0px 5px;">add Daves</div>
        </div>
    </div>
    <br/>
    <div id="btnGroups" style="font-size: 11px; font-family: Arial;"></div>
    <div style="margin-top:500px;"></div>
    <a href="http://www.youtube.com/watch?v=_LrlMoIzSjw" style="font-size: 11px;">the Daves I know</a>
    <script type="text/javascript">
        var mb1 = buttonlib.groupsMenuButtonCreate('btnGroups', 'Add to Groups...', function(groupKey, groupName, isChecked) {
            alert("groupKey="+groupKey+", groupName=" + groupName + ", isChecked=" + isChecked);
        });
        buttonlib.groupsMenuButtonAddItem(mb1, '14', 'Freds', true);
        buttonlib.groupsMenuButtonAddItem(mb1, '21', 'Gails', false);
        buttonlib.groupsMenuButtonAddItem(mb1, '32', 'Harrys', null);
        buttonlib.groupsMenuButtonAddItem(mb1, '47', 'Alices', false);
        buttonlib.groupsMenuButtonAddItem(mb1, '23', 'Pennys', null);
        buttonlib.groupsMenuButtonAddItem(mb1, '12', 'Toms', false);
        buttonlib.buttonBarDecorate('btn1', function() { buttonlib.groupsMenuButtonRemoveItem(mb1, '14'); }); /*freds*/
        buttonlib.buttonBarDecorate('btn2', function() { buttonlib.groupsMenuButtonUpdateItem(mb1, '14', null); }); /*freds*/
        buttonlib.buttonBarDecorate('btn3', function() { buttonlib.groupsMenuButtonUpdateItem(mb1, '23', true); }); /*pennys*/
        buttonlib.buttonBarDecorate('btn4', function() { buttonlib.groupsMenuButtonAddItem(mb1, '41', 'Daves', false); });
    </script>
  </body>
</html>

 

If you want to have a look at the original source code, load this non-compiled version of the demo and view its sources – here you’ll find the javascript is “uncompiled” and still readable. The drop-down menu button code utilizes the goog.ui.TriStateMenuItem class, which did NOT behave as desired. I ended up modifying goog/ui/tristatemenuitem.js and goog/ui/component.js to get the behavior I wanted. (I also edited goog/css/tristatemenuitem.css just to make the menu highlight color consistent with the normal menu ui.)

If I were more experienced with javascript, I would have made my own version of TriStateMenuItem and not touched google’s code. But my initial attempt at this failed, and I don’t have infinite time, so a-hacking I went. For the record, here’s the patch file for the changes I made to three source files in closure.  (If you dig in there, you’ll find the HALFCHECK state I added to component.js – this topic deserves a post of its own, if anyone wishes to discuss it.)

With respect to my earlier post seeking a “native” tri-state checkbox, I am happy to compromise with this solution. The images used to depict the checked and half-checked states do not appear to “belong” to any particular OS/platform, and I think the user is willing to accept their appearance as being “oh, I *am* in a browser…”. To nitpick, I wish these images had an actual “box” for the checkbox. When nothing is checked in the list, everything is blank and there is no visual cue that you might want to click on something. Clearly the screenshot of the GMail interface agrees with this, since they put actual boxes around their checkmarks.

I will now apply this closure widget in my GWT app using JSNI glue per my previous post.

Sunday, May 1, 2011

Mixing Google Closure with GWT

I’ve been happily using GWT for some time now, but I’ve wanted some widgets (just three**) that didn’t exist in GWT out of the box. While searching for widgets to fit my UI needs, I decided to try to mix the use Google’s Closure Library inside my GWT app. I’ve had some success and wanted to share how I got the two to work together.

**Namely I’ve needed tri-state checkboxes, a button with a dropdown menu (like the labels drop-down button in GMail), and I’ve also desired the look of buttons in a row (like the button bar in GMail that contains: Archive|Spam|Delete ).

 

The outline version of how to do this is:

  1. Write a static html page (non GWT) that isolates the desired closure widgets onto a page by themselves.
  2. Compile the closure javascript into a standalone .js file.
  3. Copy the standalone .js file and the some closure CSS files into your GWT project.
  4. Copy the static html bits into your uibinder xml file.
  5. Use JSNI to glue the Closure and GWT worlds.

 

1. Isolate the closure widgets you want.

You should follow the Closure Getting Started page to download the closure library, and to setup a simple static html page containing the Closure widgets you want to use. I wanted to use a CustomButton “button bar”, like the [Left|Center|Right] buttons shown in this demo page. (You can browse all the closure demos – click the Demos tab on the right side of this page.)

I already had a local apache httpd server running, so I just checked out the closure library directly into my apache document root, and created another directory called buttonbar where I could create my work.

cd /website/docroot
svn checkout http://closure-library.googlecode.com/svn/trunk/ closure-library
mkdir buttonbar

 

In the buttonbar directory I created a buttonbar.html file and buttonbar.js file. With Closure, I had a choice of “decorating” existing div tags, or generating all the items programmatically. I opted to “decorate” because this allowed me to still layout my page in a declarative style, i.e. supply my button names in the HTML, etc. This is also a good match with how I’ve already been using UIBinder in GWT to keep layout as declarative as possible. These two files looked like this:

buttonbar.html:
<html>
  <head>
    <script src="../closure-library/closure/goog/base.js"></script>
    <script src="buttonbar.js"></script>
    <link rel="stylesheet" href="../closure-library/closure/goog/css/common.css">
    <link rel="stylesheet" href="../closure-library/closure/goog/css/custombutton.css">
  </head>
  <body>
    <div style="font-size: 11px;">
        <div id="btn1" class="goog-custom-button goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">Dodge</div>
        </div><div id="btn2" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">Parry</div>
        </div><div id="btn3" class="goog-custom-button goog-custom-button-collapse-left">
            <div style="padding: 0px 5px;">Spin</div>
        </div>
    </div>
    <script type="text/javascript">
        buttonbar.setup('btn1', function() {alert("Dodge")} );
        buttonbar.setup('btn2', function() {alert("Parry")} );
        buttonbar.setup('btn3', function() {alert("Spin")} );
    </script>
  </body>
</html>

 

buttonbar.js:
goog.provide('buttonbar.setup');
    goog.require('goog.array');
    goog.require('goog.debug.DivConsole');
    goog.require('goog.debug.LogManager');
    goog.require('goog.debug.Logger');
    goog.require('goog.events');
    goog.require('goog.events.EventType');
    goog.require('goog.object');
    goog.require('goog.ui.Button');
    goog.require('goog.ui.ButtonRenderer');
    goog.require('goog.ui.ButtonSide');
    goog.require('goog.ui.CustomButton');
    goog.require('goog.ui.CustomButtonRenderer');
    goog.require('goog.ui.decorate');
/*
 * onClickCallback will be passed an ActionEvent when the button is clicked.
 *   http://closure-library.googlecode.com/svn/docs/class_goog_events_ActionEvent.html
 */
buttonbar.setup = function(name, onClickCallback) {
    var button = goog.ui.decorate(goog.dom.getElement(name));
    button.setDispatchTransitionEvents(goog.ui.Component.State.ACTION, true);
    goog.events.listen(button, goog.ui.Component.EventType.ACTION, onClickCallback);
};
goog.exportSymbol('buttonbar.setup', buttonbar.setup);

 

With respect to the event filtering in the above code, know that Closure does their own (browser-independent) implementation of events. You will simply have to read about Closure Events to decide the appropriate way you should handle them for your application. In my code above, I specifically only want events when a button is clicked, which is why you only see goog.ui.Component.State.ACTION and goog.ui.Component.EventType.ACTION. You will need to examine your application needs with respect to the event handing you specify here.

Finally, loading the html page in a browser yields a lovely button bar:

closure_button_bar

(Note: if you are loading the html page with a file:/// url, and you are using Chrome, you will have to set the security parameter --allow-file-access-from-files as an argument to Chrome upon startup.)

It is this dividing point between the html file and the js file where you must consider what code goes into the .js file, and what function(s) you will expose to the outside html world to call. You need to think about how your GWT app is going to interact with the closure widget and be sure to expose a function that allows to you stitch these two worlds together. In this case, I only need a callback to be called when a button is clicked. And I also want to keep the button declarations in the html side, and just have the javascript side “decorate” these elements. Therefore the only other argument is the ‘name’, which is the id of the div element that represents a button.

For your application, other closure widgets (or even multiple widgets combined ) will likely require exposing multiple setup functions.

 

2. Compile the javascript into a “standalone” script.

To compile, you use the Closure Builder. Using the Closure compiler will eliminate all the unused code, and given the right flags, it will reduce the code into a single compact javascript file.

The compiler itself is written in Java, and you have to download the latest compiler jar file separately. Unzip this archive and copy compiler.jar to the same directory that contains your closure-library directory that you checked out earlier.

cd /website/docroot
wget http://closure-compiler.googlecode.com/files/compiler-latest.zip
unzip compiler-latest.zip compiler.jar

 

Run the compiler over the buttonbar.js file (requires python):

cd /website/docroot

closure-library/closure/bin/build/closurebuilder.py \
  --root=closure-library/ \
  --root=buttonbar/ \
  --namespace="buttonbar.setup" \
  --output_mode=compiled \
  --compiler_jar=compiler.jar \
  --compiler_flags="--compilation_level=ADVANCED_OPTIMIZATIONS" \
  > buttonbar/buttonbar.x.js

 

It’s the compiler_flags option above that shrinks the output code into a nice single little file.

Now you can edit the <head> of your original html file, loading only your new single javascript file":

  <head>
    <script src="buttonbar.x.js"></script>
    <link rel="stylesheet" href="../closure-library/closure/goog/css/common.css">
    <link rel="stylesheet" href="../closure-library/closure/goog/css/custombutton.css">
  </head>

 

Again, loading the html page in a browser should yield:

closure_button_bar

 

3. Copy the standalone .js file .css files into your GWT project

At this point we depend only upon a single javascript file buttonbar.x.js, and two css files common.css and custombutton.css. Copy all three to appropriate subdirectories of the “war” directory in your GWT  project. In your html (or jsp, or whatever) be sure to load all three of these resources. E.g., your <head> should now also contain these three items:

  <head>

<script src="/path/to/js/buttonbar.x.js"></script>

    <link rel="stylesheet" href="/path/to/css/common.css">
    <link rel="stylesheet" href="/path/to/css/custombutton.css">
  </head>

 

4. Copy the static html bits into your UIBinder file, e.g. MyPage.ui.xml :

<g:HTMLPanel>
    <div style="font-size: 11px;">
        <div id="btn1" class="goog-custom-button goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">Dodge</div>
        </div><div id="btn2" class="goog-custom-button goog-custom-button-collapse-left goog-custom-button-collapse-right">
            <div style="padding: 0px 5px;">Parry</div>
        </div><div id="btn3" class="goog-custom-button goog-custom-button-collapse-left">
            <div style="padding: 0px 5px;">Spin</div>
        </div>
    </div>
</g:HTMLPanel>

(Note: the above .ui.xml file actually contains many more GWT widgets; just this bit of html is shown cut and paste into an HTMLPanel)

 

5. Use JSNI to glue the Closure and GWT worlds.

In our GWT code, we must setup our closure widgets. This requires the presence of two things: 1) the buttonbar.x.js script must be loaded, 2) the html that declares the widgets must be loaded. Some reading about GWT widgets reveals that we can override the onLoad() method which will be called after both of those items are ready.

Earlier, in our simple html file, we made a javascript call to buttonbar.setup() for each button. In GWT, we will wrap the same call using JSNI and use the $wnd prefix to access the global space where we will find our setup function. Look for the setup in the onLoad() method.

public class MyPage extends Composite {
    interface MyPageUiBinder extends UiBinder<Widget, MyPage> {}
    private static MyPageUiBinder uiBinder = GWT.create(MyPageUiBinder.class);
    public MyPage() {
        initWidget(uiBinder.createAndBindUi(this));
        // etc...
    }
    @Override
    protected void onLoad() {
        super.onLoad();
        setupGoogButtons(this);
    }
    
    private static native void setupGoogButtons(MyPage widget) /*-{
        $wnd.buttonbar.setup('btn1', function() { widget.@com.your.namespace.client.MyPage::btnDodgeClick()() });
        $wnd.buttonbar.setup('btn2', function() { widget.@com.your.namespace.client.MyPage::btnParryClick()() });
        $wnd.buttonbar.setup('btn3', function() { widget.@com.your.namespace.client.MyPage::btnSpinClick()() });
    }-*/;
    public void btnDodgeClick() {
        GWT.log("btn dodge click");
    }
    public void btnParryClick() {
        GWT.log("btn parry click");
    }
    public void btnSpinClick() {
        GWT.log("btn spin click");
    }
}

 

That’s it. Granted the above example just logs the button clicks. But it does demonstrate the use of closure widgets in a GWT app.

 

Epilogue

I have two thoughts after achieving this task:

1) I do not have a “clean” development environment to easily iterate development. Normally GWT work is completely self-contained in eclipse. But with closure, I need python (plus java) to compile, and I was using an existing apache server to test my html/javascript, and just using vi to edit files. Figuring out how I could integrate all this activity into my “normal” eclipse world does not seem worth the effort for a one-off use of closure widgets. I am just documenting how I did things so I may maintain it or repeat it in the future. But it is not a push-one-button “make all” solution.

2) One could wrap each individual closure library widget into a self standing GWT widget and be done with the “dirty work” for good. But that would defeat closure’s ability of reducing unnecessary code. Instead you have consider the whole content of a particular page load, and all the possible closure widgets you will need while on that GWT page. You need to consider your interface and export javascript functions for all of those widgets and compile them such that you GWT page will still only need to load one closure js file. This presents more “dirty work” for each GWT page that needs closure widgets, and will make me curse my disjointed development environment. But the end result is worthwhile, so the incongruent nature of it all will be okay by me.