Matt RaibleMatt Raible is a writer with a passion for software. Connect with him on LinkedIn.

The Angular Mini-Book The Angular Mini-Book is a guide to getting started with Angular. You'll learn how to develop a bare-bones application, test it, and deploy it. Then you'll move on to adding Bootstrap, Angular Material, continuous integration, and authentication.

Spring Boot is a popular framework for building REST APIs. You'll learn how to integrate Angular with Spring Boot and use security best practices like HTTPS and a content security policy.

For book updates, follow @angular_book on Twitter.

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: Angular, Bootstrap, and Spring Boot. All of these frameworks are wrapped up in an easy-to-use project called JHipster.

This book shows you how to build an app with JHipster, and guides you through the plethora of tools, techniques and options you can use. Furthermore, it explains the UI and API building blocks so you understand the underpinnings of your great application.

For book updates, follow @jhipster-book on Twitter.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.
You searched this site for "read Economics 22nd Edition online free". 1,100 entries found.

You can also try this same search on Google.

Adding Search to AppFuse with Compass

Over 5 years ago, I recognized that AppFuse needed to have a search feature and entered an issue in JIRA. Almost 4 years later, a Compass Tutorial was created and shortly after Shay Banon (Compass Founder), sent in a patch. From the message he sent me:

A quick breakdown of enabling search:

  1. Added Searchable annotations to the User and Address.
  2. Defined Compass bean, automatically scanning the model package for mapped searchable classes. It also automatically integrates with Spring transaction manager, and stores the index on the file system ([work dir]/target/test-index).
  3. Defined CompassTemplate (similar in concept to HibernateTemplate).
  4. Defined CompassSearchHelper. Really helps to perform search since it does pagination and so on.
  5. Defined CompassGps, basically it allows for index operation allowing to completely reindex the data from the database. JPA and Hiberante also automatically mirror changes done through their API to the index. iBatis uses AOP.

Fast forward 2 years and I finally found the time/desire to put a UI on the backend Compass implementation that Shay provided. Yes, I realize that Compass is being replaced by ElasticSearch. I may change to use ElasticSearch in the future; now that the search feature exists, I hope to see it evolve and improve.

Since Shay's patch integrated the necessary Spring beans for indexing and searching, the only thing I had to do was to implement the UI. Rather than having an "all objects" results page, I elected to implement it so you could search on an entity's list screen. I started with Spring MVC and added a search() method to the UserController:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView handleRequest(@RequestParam(required = false, value = "q") String query) throws Exception {
    if (query != null && !"".equals(query.trim())) {
        return new ModelAndView("admin/userList", Constants.USER_LIST, search(query));
    } else {
        return new ModelAndView("admin/userList", Constants.USER_LIST, mgr.getUsers());
    }
}

public List<User> search(String query) {
    List<User> results = new ArrayList<User>();
    CompassDetachedHits hits = compassTemplate.findWithDetach(query);
    log.debug("No. of results for '" + query + "': " + hits.length());
    for (int i = 0; i < hits.length(); i++) {
        results.add((User) hits.data(i));
    }
    return results;
}

At first, I used compassTemplate.find(), but got an error because I wasn't using an OpenSessionInViewFilter. I decided to go with findWithDetach() and added the following search form to the top of the userList.jsp page:

<div id="search">
<form method="get" action="${ctx}/admin/users" id="searchForm">
    <input type="text" size="20" name="q" id="query" value="${param.q}"
           placeholder="Enter search terms"/>
    <input type="submit" value="<fmt:message key="button.search"/>"/>
</form>
</div>

NOTE: I tried using HTML5's <input type="search">, but found Canoo WebTest doesn't support it.

Next, I wrote a unit test to verify everything worked as expected. I found I had to call compassGps.index() as part of my test to make sure my index was created and up-to-date.

public class UserControllerTest extends BaseControllerTestCase {
    @Autowired
    private CompassGps compassGps;
    @Autowired
    private UserController controller;

    public void testSearch() throws Exception {
        compassGps.index();
        ModelAndView mav = controller.handleRequest("admin");
        Map m = mav.getModel();
        List results = (List) m.get(Constants.USER_LIST);
        assertNotNull(results);
        assertTrue(results.size() >= 1);
        assertEquals("admin/userList", mav.getViewName());
    }
}

After getting this working, I started integrating similar code into AppFuse's other web framework modules (Struts, JSF and Tapestry). When I was finished, they all looked pretty similar from a UI perspective.

Struts:

<div id="search">
<form method="get" action="${ctx}/admin/users" id="searchForm">
    <input type="text" size="20" name="q" id="query" value="${param.q}"
           placeholder="Enter search terms..."/>
    <input type="submit" value="<fmt:message key="button.search"/>"/>
</form>
</div>

JSF:

<div id="search">
<h:form id="searchForm">
    <h:inputText id="q" name="q" size="20" value="#{userList.query}"/>
    <h:commandButton value="#{text['button.search']}" action="#{userList.search}"/>
</h:form>
</div>

Tapestry:

<div id="search">
<t:form method="get" t:id="searchForm">
    <t:textfield size="20" name="q" t:id="q"/>
    <input t:type="submit" value="${message:button.search}"/>
</t:form>
</div>

One frustrating thing I found was that Tapestry doesn't support method="get" and AFAICT, neither does JSF 2. With JSF, I had to make my UserList bean session-scoped or the query parameter would be null when it listed the results. Tapestry took me the longest to implement, mainly because I had issues figuring out how it's easy-to-understand-once-you-know onSubmit() handlers worked and I had the proper @Property and @Persist annotations on my "q" property. This tutorial was the greatest help for me. Of course, now that it's all finished, the code looks pretty intuitive.

Feeling proud of myself for getting this working, I started integrating this feature into AppFuse's code generation and found I had to add quite a bit of code to the generated list pages/controllers.

So I went on a bike ride...

While riding, I thought of a much better solution and added the following search method to AppFuse's GenericManagerImpl.java. In the code I added to pages/controllers previously, I'd already refactored to use CompassSearchHelper and I continued to do so in the service layer implementation.

@Autowired
private CompassSearchHelper compass;

public List<T> search(String q, Class clazz) {
    if (q == null || "".equals(q.trim())) {
        return getAll();
    }

    List<T> results = new ArrayList<T>();

    CompassSearchCommand command = new CompassSearchCommand(q);
    CompassSearchResults compassResults = compass.search(command);
    CompassHit[] hits = compassResults.getHits();

    if (log.isDebugEnabled() && clazz != null) {
        log.debug("Filtering by type: " + clazz.getName());
    }

    for (CompassHit hit : hits) {
        if (clazz != null) {
            if (hit.data().getClass().equals(clazz)) {
                results.add((T) hit.data());
            }
        } else {
            results.add((T) hit.data());
        }
    }

    if (log.isDebugEnabled()) {
        log.debug("Number of results for '" + q + "': " + results.size());
    }

    return results;
}

This greatly simplified my page/controller logic because now all I had to do was call manager.search(query, User.class) instead of doing the Compass login in the controller. Of course, it'd be great if I didn't have to pass in the Class to filter by object, but that's the nature of generics and type erasure.

Other things I learned along the way:

  • To index on startup, I added compassGps.index() to the StartupListener..
  • In unit tests that leveraged transactions around methods, I had to call compassGps.index() before any transactions started.
  • To scan multiple packages for searchable classes, I had to add a LocalCompassBeanPostProcessor.

But more than anything, I was reminded it always helps to take a bike ride when you don't like the design of your code. ;-)

This feature and many more will be in AppFuse 2.1, which I hope to finish by the end of the month. In the meantime, please feel free to try out the latest snapshot.

Posted in Java at Mar 15 2011, 05:11:12 PM MDT 1 Comment

JSR 303 and JVM Web Framework Support

Emmanuel Bernard recently sent an email to the JSR 303 Experts Group about the next revision of the Bean Validation JSR (303). Rather than sending the proposed changes privately, he blogged about them. I left a comment with what I'd like to see:

+1 for Client-side validation. I'd love to see an API that web frameworks can hook into to add "required" to their tags for HTML5. Or some service that can be registered so the client can make Ajax requests to an API to see if an object is valid.

Emmanuel replied that most of the necessary API already exists for this, but frameworks have been slow to adopt it.

Hi Matt,

The sad thing is that the API is present on the Bean Validation side but presentation frameworks are slow to adopt it and use it :(

RichFaces 4 now has support for it but I wished more presentation frameworks had worked on the integration. If you can convince a few people or have access to a few people, feel free to send them by me :)

The integration API is described here. Let me know if you think some parts are missing or should be improved. We should definitely do some more buzz around it.

In the interest of generating more buzz around it, I decided to do some research and see what JVM Frameworks support JSR 303. Here's what I've come up with so far (in no particular order):

Struts 2 has an open issue, but doesn't seem to support JSR 303. Since I did a quick-n-dirty google search for most of these, I'm not sure if they support client-side JavaScript or HTML5's required. If you know of other JVM-based web frameworks that support JSR 303, please let me know in the comments.

Posted in Java at Mar 08 2011, 11:33:24 AM MST 4 Comments

Upgraded to Apache Roller 5.0, RC4

Last Sunday, Dave Johnson released Apache Roller 5.0, RC4. Since I'm an Apache Roller committer, and I've been seeing issues with comments throwing exceptions on this site, I decided to upgrade. In doing so, I discovered a number of issues. Hopefully by documenting them here, you'll be able to upgrade from Roller 4 to Roller 5 without any issues.

To upgrade, I took a snapshot of my existing site and database and copied them locally. After getting everything setup locally (importing existing database and upgrading it), I started Tomcat and began solving problems.

Database settings - JNDI vs. Properties
With Roller 4, I configured by database settings in a ROOT.xml so they'd be read using JNDI. With Roller 5, I got the following error when I tried to do this.

<openjpa-2.0.1-r422266:989424 fatal user error> org.apache.openjpa.persistence.ArgumentException:
A JDBC Driver or DataSource class name must be specified in the ConnectionDriverName property.
       at org.apache.openjpa.jdbc.schema.DataSourceFactory.newDataSource(DataSourceFactory.java:76)

I was able to get around this issue by adding the following to my roller-custom.properties:

database.configurationType=jdbc
database.jdbc.driverClass=com.mysql.jdbc.Driver
database.jdbc.connectionURL=jdbc:mysql://localhost/rollerdb
database.jdbc.username=root
database.jdbc.password=
mail.configurationType=properties
mail.hostname=localhost

After making this change, I received an error when Planet tried to startup:

ERROR 2011-03-02 09:56:08,502 DatabaseProvider:errorMessage - ERROR: unable to obtain database connection. 
Likely problem: bad connection parameters or database unavailable.
FATAL 2011-03-02 09:56:08,502 RollerContext:contextInitialized - Roller Planet startup failed during app preparation
org.apache.roller.planet.business.startup.StartupException: ERROR: unable to obtain database connection. 
Likely problem: bad connection parameters or database unavailable.

I don't remember why I enabled planet, but turning it off in roller-custom.properties seemed to solve the problem.

planet.aggregator.enabled=false

Password Encyrption
The next thing I tried to do was login. When this didn't work, I figured it must be related to password encryption. With Roller 4, I had to have "passwds.encryption.enabled=true" in roller-custom.properties. In Roller 5, I also had to add the encryption algorithm.

passwds.encryption.algorithm=SHA

GZip Compression
In November 2009, I optimized this site and used Roller's CompressionFilter and wro4j to gzip and concatenate JavaScript and CSS. With Roller 4, I used the CompressionFilter to compress *.css and *.js instead of using Wro4J's built-in gzip compression. The Roller 5 CompressionFilter seems to have issues with wro4j, so I had to disable it for *.css and *.js and use wro4j instead.

At this point, I figured I was good to go, so I zipped up my local WAR and scp'ed it to raibledesigns.com. I stopped Tomcat and attempted to upgrade my production MySQL database (version 3.23.56). Below is the error I received.

$ mysql -u raible -p raible < 400-to-500-migration.sql
Enter password:
ERROR 1064 (00000) at line 42: You have an error in your SQL syntax near 'as w set
   lastmodified = lastmodified,
   datecreated = datecreated,
   cr' at line 1

At this point, I figured my database might be slightly hosed, but since it was simply creating tables, I was probably OK. I restarted Tomcat and left the old version in place while I waited for a MySQL 5 database instance from my hosting provider, KGB Internet. Once I got the new instance, I imported my backed-up database, ran the upgrade script and everything worked just peachy.

I generally upgrade Roller by coping the new codebase over my old one. This is because I have a lot of symlinks and other files in my "ROOT" directory and like to keep those. In doing this, I found I had to do a couple things after copying everything over:

  1. Delete WEB-INF/lib and recopy from RC4's WEB-INF/lib.
  2. Delete WEB-INF/classes and recopy from RC4's WEB-INF/classes.

I then experienced some issues with JARs not being present for Roller's JSPWikiPlugin. I enabled this long ago, but don't use it anymore. However, to keep old posts still working, I wanted to enable it. The downloads for the plugin seem to be gone, but luckily I found a copy and put all the JARs into my WEB-INF/lib directory.

After starting Tomcat and browsing around a bit, I discovered two more issues:

  1. Search doesn't seem to work. For example, there are no results for jQuery.
  2. My Archives page's calendar didn't work. It showed the following:
    $calendarModel.showWeblogEntryCalendarBig($weblog, $cat)

I was able to fix issue #2 by changing #showBigWeblogCalendar() to the following.

#showWeblogEntryCalendarBig($model.weblog "nil")

The first issue with search seems to remain.

If you notice any other issues on this site, please let me know. I'll try to get them fixed asap.

Update: I entered an issue for my search problem in Roller's JIRA. I also managed to figure out that the problem is due to the old version of oscache that's needed by the JSPWiki plugin. Hopefully we can get the plugin upgraded to avoid this issue for other users.

Posted in Roller at Mar 03 2011, 11:39:37 AM MST 7 Comments

The Greatest Snow on Earth

Last week, I traveled on my monthly trip to Utah to work on-site at Overstock. Unlike previous visits, snow was in the forecast and it didn't disappoint. I woke up early on Friday, worked a few hours and then met a couple co-workers at the office at 8. We arrived at Solitude by 8:40 and were in line for the lift by 8:55. We were the 5th chair on the lift and quickly skied to The Summit Lift. It was here we found thigh-deep powder and face shots on every run. The video below has shot by my co-worker, Eric. You can also view it on YouTube. The face shots start around 0:45.

After an awesome morning of skiing, I returned to work and later picked up Trish from the airport for a weekend of powder. When I started working at Overstock, I told myself that I'd buy a pair of "Utah Skis" if the powder was good. It seemed like the right time, so I picked up some Bluehouse Powder Skis on the way back from the airport. That night, we saw Hot Buttered Rum and woke up early for 27" of fresh powder at Alta.

Free Heeling

The skiing was incredible all day and it never stopped snowing. That night, we headed to The Canyons and stayed slopeside at The Hyatt. We got upgraded to a great room and enjoyed some nice views.

The Hyatt Bluehouse Awesomeness View at The Hyatt

We slept in on Sunday, grabbed some breakfast and hopped on the lift around noon. It was a Bluebird Day and we skied as much of the hard stuff as we could find.

The Canyons The 9990 Lift 9990's Fun Runs Into The Light

Hiking to the top of 9990 Top of 9990

Several weeks ago, I said I thought Colorado's powder was better than Utah's. After experiencing knee-deep powder at Solitude and sweet, fluffy powder at Alta, I'm officially changing my stance. In my opinion, Utah has the greatest powder on earth. If Colorado happens to get that much powder, and I get to ski it, I'd be more than happy to reconsider.

Posted in General at Mar 02 2011, 12:11:59 AM MST 3 Comments

Upcoming Conferences: TSSJS in Las Vegas and 33rd Degree in Kraków, Poland

It's that time of year again - the beginning of Conference Season. I generally like to speak at a few conferences a year and 2011 is no different. For March Madness, I'll be heading to Las Vegas to speak at TheServerSide Java Symposium. I'll be giving updated talks similar to the ones I gave at last year's Rich Web Experience in Fort Lauderdale:

You might remember my Comparing JVM Web Frameworks talk from Devoxx 2010 and some of the interesting debate it caused. I've done some minor updates to my video presentation and some updates to my JVM Web Frameworks presentation as well. Most notably, I'll be including some findings from Peter Thomas's perfbench project. I also hope to update AppFuse to JSF 2 and integrate extensionless URLs in AppFuse Light. Marcin Zajaczkowski was nice enough to provide an upgrade to Wicket 1.4.15, so it'll be interesting to see how well Wicket supports extensionless URLs.

In April, I'll be presenting Comparing JVM Web Frameworks at the 33rd Degree Conference in Kraków, Poland. While I studied in Russia a couple summers in college, I've never been to Poland, so I'm really looking forward to this trip. With any luck, I'll have AppFuse 2.1 released by then and my knowledge of all its web frameworks' latest versions will be update-to-date. As you know, it's unlikely I'll recommend a best web framework (because there isn't one), but I hope to provide some techniques you can use to decide the best framework for your particular needs.

In addition to Vegas and Poland, there's a couple other events I might speak at in the next few months: the Utah Java Users Group (possibly in April), Jazoon and Über Conf (if my proposals are accepted). For these events, I'm hoping to present the following talk:

Webapp Security: Develop. Penetrate. Protect. Relax.
In this session, you'll learn how to implement authentication in your Java web applications using Spring Security, Apache Shiro and good ol' Java EE Container Managed Authentication. You'll also learn how to secure your REST API with OAuth and lock it down with SSL.

After learning how to develop authentication, I'll introduce you to OWASP, the OWASP Top 10, its Testing Guide and its Code Review Guide. From there, I'll discuss using WebGoat to verify your app is secure and commercial tools like webapp firewalls and accelerators.

If you're planning on attending TSSJS or 33rd Degree, hopefully I'll see you there.

Posted in Java at Feb 25 2011, 03:14:59 PM MST 1 Comment

Implementing Ajax Authentication using jQuery, Spring Security and HTTPS

I've always had a keen interest in implementing security in webapps. I implemented container-managed authentication (CMA) in AppFuse in 2002, watched Tomcat improve it's implementation in 2003 and implemented Remember Me with CMA in 2004. In 2005, I switched from CMA to Acegi Security (now Spring Security) and never looked back. I've been very happy with Spring Security over the years, but also hope to learn more about Apache Shiro and implementing OAuth to protect JavaScript APIs in the near future.

I was recently re-inspired to learn more about security when working on a new feature at Overstock.com. The feature hasn't been released yet, but basically boils down to allowing users to login without leaving a page. For example, if they want to leave a review on a product, they would click a link, be prompted to login, enter their credentials, then continue to leave their review. The login prompt and subsequent review would likely be implemented using a lightbox. While lightboxes are often seen in webapps these days because they look good, it's also possible Lightbox UIs provide a poor user experience. User experience aside, I think it's interesting to see what's required to implement such a feature.

To demonstrate how we did it, I whipped up an example using AppFuse Light, jQuery and Spring Security. The source is available in my ajax-login project on GitHub. To begin, I wanted to accomplish a number of things to replicate the Overstock environment:

  1. Force HTTPS for authentication.
  2. Allow testing HTTPS without installing a certificate locally.
  3. Implement a RESTful LoginService that allows users to login.
  4. Implement login with Ajax, with the request coming from an insecure page.

Forcing HTTPS with Spring Security
The first feature was fairly easy to implement thanks to Spring Security. Its configuration supports a requires-channel attribute that can be used for this. I used this to force HTTPS on the "users" page and it subsequently causes the login to be secure.

<intercept-url pattern="/app/users" access="ROLE_ADMIN" requires-channel="https"/>

Testing HTTPS without adding a certificate locally
After making the above change in security.xml, I had to modify my jWebUnit test to work with SSL. In reality, I didn't have to modify the test, I just had to modify the configuration that ran the test. In my last post, I wrote about adding my 'untrusted' cert to my JVM keystore. For some reason, this works for HttpClient, but not for jWebUnit/HtmlUnit. The good news is I figured out an easier solution - adding the trustStore and trustStore password as system properties to the maven-failsafe-plugin configuration.

<artifactId>maven-failsafe-plugin</artifactId>
<version>2.7.2</version>
<configuration>
    <includes>
        <include>**/*WebTest.java</include>
    </includes>
    <systemPropertyVariables>
      <javax.net.ssl.trustStore>${project.build.directory}/ssl.keystore</javax.net.ssl.trustStore>
      <javax.net.ssl.trustStorePassword>appfuse</javax.net.ssl.trustStorePassword>
    </systemPropertyVariables>
</configuration>

The disadvantage to doing things this way is you'll have to pass these in as arguments when running unit tests in your IDE.

Implementing a LoginService
Next, I set about implementing a LoginService as a Spring MVC Controller that returns JSON thanks to the @ResponseBody annotation and Jackson.

package org.appfuse.examples.web;

import org.appfuse.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/api/login.json")
public class LoginService {

  @Autowired
  @Qualifier("authenticationManager")
  AuthenticationManager authenticationManager;

  @RequestMapping(method = RequestMethod.GET)
  @ResponseBody
  public LoginStatus getStatus() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null && !auth.getName().equals("anonymousUser") && auth.isAuthenticated()) {
      return new LoginStatus(true, auth.getName());
    } else {
      return new LoginStatus(false, null);
    }
  }

  @RequestMapping(method = RequestMethod.POST)
  @ResponseBody
  public LoginStatus login(@RequestParam("j_username") String username,
                           @RequestParam("j_password") String password) {

    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
    User details = new User(username);
    token.setDetails(details);

    try {
      Authentication auth = authenticationManager.authenticate(token);
      SecurityContextHolder.getContext().setAuthentication(auth);
      return new LoginStatus(auth.isAuthenticated(), auth.getName());
    } catch (BadCredentialsException e) {
      return new LoginStatus(false, null);
    }
  }

  public class LoginStatus {

    private final boolean loggedIn;
    private final String username;

    public LoginStatus(boolean loggedIn, String username) {
      this.loggedIn = loggedIn;
      this.username = username;
    }

    public boolean isLoggedIn() {
      return loggedIn;
    }

    public String getUsername() {
      return username;
    }
  }
}

To verify this class worked as expected, I wrote a unit test using JUnit and Mockito. I used Mockito because Overstock is transitioning to it from EasyMock and I've found it very simple to use.

package org.appfuse.examples.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class LoginServiceTest {

  LoginService loginService;
  AuthenticationManager authenticationManager;

  @Before
  public void before() {
    loginService = new LoginService();
    authenticationManager = mock(AuthenticationManager.class);
    loginService.authenticationManager = authenticationManager;
  }

  @After
  public void after() {
    SecurityContextHolder.clearContext();
  }

  @Test
  public void testLoginStatusSuccess() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(true);
    SecurityContext context = new SecurityContextImpl();
    context.setAuthentication(auth);
    SecurityContextHolder.setContext(context);

    LoginService.LoginStatus status = loginService.getStatus();
    assertTrue(status.isLoggedIn());
  }

  @Test
  public void testLoginStatusFailure() {
    LoginService.LoginStatus status = loginService.getStatus();
    assertFalse(status.isLoggedIn());
  }

  @Test
  public void testGoodLogin() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(true);
    when(authenticationManager.authenticate(Matchers.<Authentication>anyObject())).thenReturn(auth);
    LoginService.LoginStatus status = loginService.login("foo", "bar");
    assertTrue(status.isLoggedIn());
    assertEquals("foo", status.getUsername());
  }

  @Test
  public void testBadLogin() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(false);
    when(authenticationManager.authenticate(Matchers.anyObject()))
        .thenThrow(new BadCredentialsException("Bad Credentials"));
    LoginService.LoginStatus status = loginService.login("foo", "bar");
    assertFalse(status.isLoggedIn());
    assertEquals(null, status.getUsername());
  }
}

Implement login with Ajax
The last feature was the hardest to implement and still isn't fully working as I'd hoped. I used jQuery and jQuery UI to implement a dialog that opens the login page on the same page rather than redirecting to the login page. The "#demo" locator refers to a button in the page.

Passing in the "ajax=true" parameter disables SiteMesh decoration on the login page, something that's described in my Ajaxified Body article.

var dialog = $('<div></div>');

$(document).ready(function() {
    $.get('/login?ajax=true', function(data) {
        dialog.html(data);
        dialog.dialog({
            autoOpen: false,
	       title: 'Authentication Required'
        });
    });

    $('#demo').click(function() {
      dialog.dialog('open');
      // prevent the default action, e.g., following a link
      return false;
    });
});

Instead of adding a click handler to a specific id, it's probably better to use a CSS class that indicates authentication is required for a link, or -- even better -- use Ajax to see if the link is secured.

The login page then has the following JavaScript to add a click handler to the "login" button that submits the request securely to the LoginService.

var getHost = function() {
    var port = (window.location.port == "8080") ? ":8443" : "";
    return ((secure) ? 'https://' : 'http://') + window.location.hostname + port;
};

var loginFailed = function(data, status) {
    $(".error").remove();
    $('#username-label').before('<div class="error">Login failed, please try again.</div>');
};

$("#login").live('click', function(e) {
    e.preventDefault();
    $.ajax({url: getHost() + "/api/login.json",
        type: "POST",
        data: $("#loginForm").serialize(),
        success: function(data, status) {
            if (data.loggedIn) {
                // success
                dialog.dialog('close');
                location.href= getHost() + '/users';
            } else {
                loginFailed(data);
            }
        },
        error: loginFailed
    });
});

The biggest secret to making this all work (the HTTP -> HTTPS communication, which is considered cross-domain), is the window.name Transport and the jQuery plugin that implements it. To make this plugin work with Firefox 3.6, I had to implement a Filter that adds Access-Control headers. A question on Stackoverflow helped me figure this out.

public class OptionsHeadersFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET,POST");
        response.setHeader("Access-Control-Max-Age", "360");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");

        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {
    }

    public void destroy() {
    }
}

Issues
I encountered a number of issues when implementing this in the ajax-login project.

  • If you try to run this with ports (e.g. 8080 and 8443) in your URLs, you'll get a 501 (Not Implemented) response. Removing the ports by fronting with Apache and mod_proxy solves this problem.
  • If you haven't accepted the certificate in your browser, the Ajax request will fail. In the example, I solved this by clicking on the "Users" tab to make a secure request, then going back to the homepage to try and login.
  • The jQuery window.name version 0.9.1 doesn't work with jQuery 1.5.0. The error is "$.httpSuccess function not found."
  • Finally, even though I was able to authenticate successfully, I was unable to make the authentication persist. I tried adding the following to persist the updated SecurityContext to the session, but it doesn't work. I expect the solution is to create a secure JSESSIONID cookie somehow.
    @Autowired
    SecurityContextRepository repository;
    
    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public LoginStatus login(@RequestParam("j_username") String username,
                             @RequestParam("j_password") String password,
                             HttpServletRequest request, HttpServletResponse response) {
    
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        ...
    
        try {
            Authentication auth = authenticationManager.authenticate(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
            // save the updated context to the session
            repository.saveContext(SecurityContextHolder.getContext(), request, response);
            return new LoginStatus(auth.isAuthenticated(), auth.getName());
        } catch (BadCredentialsException e) {
            return new LoginStatus(false, null);
        }
    }
    

Conclusion
This article has shown you how to force HTTPS for login, how to do integration testing with a self-generated certificate, how to implement a LoginService with Spring MVC and Spring Security, as well as how to use jQuery to talk to a service cross-domain with the window.name Transport. While I don't have everything working as much as I'd like, I hope this helps you implement a similar feature in your applications.

One thing to be aware of is with lightbox/dialog logins and HTTP -> HTTPS is that users won't see a secure icon in their address bar. If your app has sensitive data, you might want to force https for your entire app. OWASP's Secure Login Pages has a lot of good tips in this area.

Update: I've posted a demo of the ajax-login webapp. Thanks to Contegix for hosting the demo and helping obtain/install an SSL certificate so quickly.

Posted in Java at Feb 23 2011, 04:55:55 PM MST 13 Comments

Making Code Generation Smarter with Maven

As you might've read in my last entry, I recently started a new gig with Overstock.com. On my first day, I was quickly immersed into the development process by joining the Conversion Team. The Conversion Team is responsible for developing the checkout UI and handling payments from customers. I quickly discovered Overstock was mostly a Linux + Eclipse Shop and did my best to get my favorite Mac + IntelliJ + JRebel installed and configured. Thanks to my new Team Lead, I was able to get everything up and running the first day, as well as checkin my first contribution: making mvn jetty:run work so I didn't have to use my IDE to deploy to Tomcat.

In setting up my environment, I couldn't help but notice running jetty:run took quite a while to run each time. Specifically, the build process took 45 seconds to start executing the Jetty plugin, then another 23 seconds to startup after that. The first suspicious thing I noticed was that the UI templates were being re-generated and compiled on each execution. The UI Templating Framework at Overstock is Jamon, and is described as follows:

Jamon is a text template engine for Java, useful for generating dynamic HTML, XML, or any text-based content. In a typical Model-View-Controller architecture, Jamon clearly is aimed at the View (or presentation) layer.

Because it is compiled to non-reflective Java code, and statically type-checked, Jamon is ideally suited to support refactoring of template-based UI applications. Using mock objects -like functionality, Jamon also facilitates unit testing of the controller and view.

To generate .java files from .jamon templates, we use the Jamon Plugin for Maven. Remembering that the Maven Compiler Plugin has an incremental-compile feature, I turned to its source code to find out how to implement this in the Jamon plugin. I was pleasantly surprised to find the StaleSourceScanner. This class allows you to easily compare two files to see if the source needs to re-examined for generation or compilation.

I noticed the Jamon Plugin had the following code to figure out which files it should generate into .java files:

private List<File> accumulateSources(File p_templateSourceDir)
{
  final List<File> result = new ArrayList<File>();
  if (p_templateSourceDir == null)
  {
    return result;
  }
  for (File f : p_templateSourceDir.listFiles())
  {
    if (f.isDirectory())
    {
      result.addAll(accumulateSources(f));
    }
    else if (f.getName().toLowerCase(Locale.US).endsWith(".jamon"))
    {
      String filePath = f.getPath();
       // FIXME !?
      String basePath = templateSourceDir().getAbsoluteFile().toString();
      result.add(new File(filePath.substring(basePath.length() + 1)));
    }
  }
  return result;
}

I changed it to be smarter and only generate changed templates with the following code:

private List<File> accumulateSources(File p_templateSourceDir) throws MojoExecutionException
{
  final List<File> result = new ArrayList<File>();
  if (p_templateSourceDir == null)
  {
    return result;
  }
  SourceInclusionScanner scanner = getSourceInclusionScanner( staleMillis );
  SourceMapping mapping = new SuffixMapping( ".jamon", ".java");

  scanner.addSourceMapping( mapping );

  final Set<File> staleFiles = new LinkedHashSet<File>();

  for (File f : p_templateSourceDir.listFiles())
  {
    if (!f.isDirectory())
    {
      continue;
    }

    try
    {
      staleFiles.addAll( scanner.getIncludedSources(f.getParentFile(), templateOutputDir()));
    }
    catch ( InclusionScanException e )
    {
      throw new MojoExecutionException(
        "Error scanning source root: \'" + p_templateSourceDir.getPath()
          + "\' " + "for stale files to recompile.", e );
    }
  }

  // Trim root path from file paths
  for (File file : staleFiles) {
    String filePath = file.getPath();
    String basePath = templateSourceDir().getAbsoluteFile().toString();
    result.add(new File(filePath.substring(basePath.length() + 1)));
  }
}

This method references a getSourceInclusionScanner() method, which is implemented as follows:

protected SourceInclusionScanner getSourceInclusionScanner( int staleMillis )
{
  SourceInclusionScanner scanner;

  if ( includes.isEmpty() && excludes.isEmpty() )
  {
      scanner = new StaleSourceScanner( staleMillis );
  }
  else
  {
      if ( includes.isEmpty() )
      {
          includes.add( "**/*.jamon" );
      }
      scanner = new StaleSourceScanner( staleMillis, includes, excludes );
  }

  return scanner;
}

If you're using Jamon and its Maven Plugin, you can view my patch at SourceForge. If you're looking to include this functionality in your project, I invite you to look at the code I learned from in the Maven Compiler's AbstractCompilerMojo class.

After making this change, I was able to reduce the build execution time by over 50%. Now it takes 20 seconds to hit the Jetty plugin and 42 seconds to finishing starting. Of course, in an ideal world, I'd like to get this down to 20 seconds or less. Strangely enough, the easiest way to do this seems to be simple: use Linux.

On the Linux desktop they provided me, it takes 12 seconds to hit the Jetty plugin and 23 seconds to finish starting. I'd like to think this is a hardware thing, but it only get 20% faster on OS X when using an 8GB RAM + SSD machine (vs. a 4GB + 5400 drive). Since Overstock has provided me with a 4GB MacBook Pro, I'm considering installing Ubuntu on it, just to see what the difference is.

Sun over the Snowbird In related news, Overstock.com is looking to hire a whole bunch of Java Developers this year. The pictures of the new Provo office look pretty sweet. Of course, you can also work at HQ, which is a mere 25 minutes from some of the best skiing in the world. Personally, I think Colorado's powder is better, but I can't argue with the convenience of no traffic. In addition to full-time gigs, they've started hiring more remote contractors like myself, so they pretty much have something for everyone. So if you love Java, like to get some turns in before work, and aren't an asshole - you should and I'll try to hook you up.

Update: After writing this post, I received an email from Neil Hartner questioning these numbers. Basically, he was able to get his MacBook Pro to run just as fast as Linux. Turns out, the reason my Mac was so much slower was because JRebel was configured in my MAVEN_OPTS. JRebel's FAQ does state the following:

Does JRebel make the server start up slower?
JRebel needs to do more work on startup (search more places for classes and resources, instrument classes, etc), so some slowdown can be expected. If it's larger than 50% please contact [email protected].

Since it's right around 50% slower, I guess there's no reason to call them. My guess is the best thing to do is remove JRebel from MAVEN_OPTS, but have an alias that can enable it, or simply run it from your IDE.

Posted in Java at Jan 21 2011, 03:26:43 PM MST 7 Comments

2010 - A Year in Review

2010 was a heckuva year, possibly one of my all-time favorites. It started with lots of anticipated fun and ended with lots of travel, skiing and relaxing and I'm only just now finding time to write this post. I had a lot of goals when I started 2010; the top two being my favorite:

  • Happiness
  • Girl

The rest of my goals involved running, skiing, mountain biking and finishing The Bus. I also had some professional goals that involved open source, conferences, publishing articles and learning new technologies. I accomplished about 75% of my personal goals and 50% of my professional goals. I'm pretty happy with these percentages considering how much fun I had last year.

In December 2009, I started working with Time Warner Cable as their Chief Architect of Web Development. I was hired to help them build a team of hard-working developers that could quickly build their online video presence. In January, we started getting our ducks in a row with some Agile Training in Virginia, followed by hiring some Agile Coaches. The only technical post I had during this process was about development infrastructure stacks. Both at TWC and my current gig, I've found myself enjoying the following stack:

  • Source Control: SVN
  • Source Viewer: FishEye
  • Wiki: Confluence
  • Bug Tracker: JIRA
  • Continuous Integration: Hudson

We tried Git for a couple months at TWC, but ended up reverting to SVN after we had "code deleted" issues during one of our most intense development cycles. When the kids and I weren't skiing, we worked on documenting How to be a Super Hero and The Adventures of Upside Down Man.

In February, I refreshed my Grails knowledge, later learning that it was tough to teach Grails to developers that didn't know Java. First of all, a lot of Grails and Groovy books are targeted at Java Developers. Secondly, the developers I was trying to sell it to had more interest in learning Java. Since I failed to sell Grails, we ended up using Spring + jQuery to build our app. I don't think was a bad decision as most of our development ended up being UI: ActionScript/Flex, Objective-C and jQuery/HTML.

My laptop was stolen from my living room in early February. It worked out nicely in the end since I didn't lose any data (thanks to good backups) and my business insurance covered the loss. My parents came out and helped me finish remodeling my guest room. I posted about My Future of Web Frameworks Presentations and became a proud father when "Jack was flying past both Abbie and I and giggling while doing it".

By the beginning of March, we'd hired a team of 10 at TWC and I took a trip to Jackson Hole to celebrate.

The thing I enjoyed the most about this trip was how well the group jelled. Kudos to Chris for assembling such an awesome group and putting such a spectacular trip together. Can't wait for next year.

Top o' Jackson Hole

The next week, I flew to Las Vegas for The ServerSide Java Symposium and enjoyed sessions on Cloud, Web Service APIs, Flex, Spring 3 and Cameron's Lessons Learned from the Internet Age. I posted my Future of Web Frameworks and GWT vs. Flex Smackdown presentations after the conference.

I ended March with The Trifecta.

You are about to experience Copper's High Alpine Nice ride up Storm King Spaulding Bowl View from Enchanted Forest

For Easter, I purchased an iPad and reviewed it a few days later. Seven months later, I'm not enthralled with the iPad, but I do think it's a heckuva lot better than the Galaxy Tab. I don't use it much, except for movies when traveling. My kids are its primary users, mostly using it for games and Netflix.

We visited my parents in Oregon and celebrated my Mom's retirement.

Huckleberry Aficionado Oregon Garden Brewfest Happy Beer Drinkers Old Friends

The picture I took of my Mom and Abbie that weekend is one of my all-time favorites.

Abbie and Mimi

At the end of April, I said farewell to the ski season, getting in 25 days; a personal best. I'm happy to report I have 10 days in so far this year, so my goal of 30 days looks to be well on track. No helicopter attempts yet, but hopefully soon.

I ended the month with a work trip to Seattle and painting the town red with my sister and Mya.

Sweet Seats at the Mariner's Game Rally Caps! Midnight Wheeeeee!!

I returned to Denver and turned off my TV for a month. I wrote about my experience in early June. For Memorial Day Weekend, I went mountain biking in Moab and had a blast at the Desert Rocks Music Festival. The Porcupine Rim ride took us 4.5 hours and we tracked 26.75 miles. The several points in the trail with "death on the right" were truly epic.

Hazard County Trail Close to The Edge Awesome Singletrack Sweet View

June started with our annual Ride to Red Rocks. I slept six hours and did the 25-mile off-road Elephant Rock ride the next morning. The next morning I hopped on a plane to Ireland for the Irish Software Show. My sister joined me and we had an incredible time with Jamie, Rob, John and Josh.

Straight to the top! Mmmmm, Guinness The Storehouse is shaped like a pint glass Brainwave

Upon returning to work, I got to have an awkward conversation with my client about all the vacation I was taking.

Fire in Background, 100 foot tall flames For Father's Day, my parents drove to Denver and we packed up the camping gear for a fun weekend at The Great Sand Dunes. After The Dunes, my parents and kids drove to The Cabin, camping out and touring Yellowstone along the way. That Thursday, June 24th, I attended my good friend Jason's birthday party in Lodo.

That's the night I met Trish.

I saw her switch from a Martini to a Guinness and I knew I had to talk to her. I introduced myself and quickly found myself conflicted with having a soon-to-expire iPhone 4 reservation at the Apple Store. I rushed out to grab the phone, returning because there was something special about Trish. We talked for a couple hours that night ... me mesmerized, her smiling a lot. I was dumbfounded to find such a cool person existed in the world. At the end of the night, I got her number and drove home with a feeling that my life was about to change.

The next day, I began a road trip to The Last Best Place for a Montana Summer Vacation.

Big Sky Country Ready for the Parade Chris Auchenbach Meadow Lake Golf Course in Columbia Falls

During that trip, Trish and I exchanged occasional text messages and I told many friends about the kick-ass girl I met. It was another great Montana vacation.

My favorite part of this year's trip to The Cabin was seeing it as a home again. My Mom retired in April and my parents moved back to Montana shortly after. Seeing how happy they are there is truly magical. I especially enjoy the thought of visiting them and all the wonderful folks in the Swan Valley many, many times in the future.

The kids and I drove like bats out of hell and made it back to Denver (950 miles) in 14.25 hours. I quickly scheduled a first date with Trish that Wednesday, and went back to work at TWC with a renewed energy. After our first date, I formed a sort of perma-grin. That Friday was my birthday. I've been having a blast with Trish ever since.

July ended with a trip to the Lost Coast for Jess and Lili's Wedding.

The reception afterwards was a truly spectacular party that lasted well into the evening. Clint and I vowed to go to bed early, but we ended up having so much fun we closed the place down. Jess and Lili were an instrumental part in creating a spectacular night, especially with their wedding dance and infectious happiness.

Lili and Jess

In August, I celebrated this blog's 8th birthday and we started our "60-Day Push" at work. The goal of our 60-Day Push was to re-write our Video Portal, iPad and Sony Blu-ray apps from scratch, without politics dictating their features. We hired Method for design, chose our own features and went to work. I wrote about the success of this effort in October.

I neglected to write about the Denver Cruisers Saints and Sinners ride or how much fun we had listening to B.B. King at Red Rocks.

Abbie and Jack's first day of school was on August 16th.

Pretend like you're playing Wii Jack!

At the end of August, we celebrated Jack's 6th Birthday and attended my Cousin Amy's Wedding in Missouri.

Wheee! Super Mario Bros. Cake Tebow Fan

The Happy Couple Jack and Abbie The Cousins

September rolled in, we finished the majority of the work in our 60-Day Push and Trish and I drove up to Estes Park for an epic weekend at the Scottish-Irish Festival. We rode our bikes in the parade (by accident), enjoyed a few pints and even did a bit of fly-fishing near our riverside accommodations.

Beautiful View Plane in Sky Ride to Stanley Hotel View from The Stanley Hotel

We listened to the 1st 2010 Broncos game while driving back from Estes and slipped into a Rockies game shortly after losing. Our sadness over the Broncos loss was erased within a couple hours as we celebrated Jason Giambi's walk-off home run. Unfortunately, the Rockies didn't make it to the post-season, but the Broncos looked good at their home opener.

Fighter Jets

October came quickly and I wrote How's the ol' Team Doing and defended the Age of Frameworks. At the end of October, we moved into TWC's Lodo Office on Wynkoop. That weekend, we dressed up as superheros for Halloween.

November started off with Abbie's birthday and Trish got to meet the kids for the first time. From there, we went into traveling-like-madmen mode. We enjoyed suite seats at the Broncos vs. Kansas City game, then flew to Amsterdam for Devoxx the next morning (performing an AppFuse release along the way). I presented on Thursday and posted my Comparing JVM Web Frameworks presentation shortly after. I wrote about our trip while at The Cabin for Thanksgiving.

Sunset in Amsterdam Waffles at Désiré de Lille Partying with the Adobe Crew Ghent

I flew back from Montana, stopped in at the Lodo office for a couple days, then hopped on a plane to the Rich Web Experience with Trish. My presentations went well, sparked some controversy, and we raced to Key West to celebrate the end of the conference season.

Dreams do come true. Sunset in Key Largo Piña Coladas in Key Largo Key Largo Sunset

I returned to Denver for my last week at TWC, enjoyed a couple days of skiing, then headed to Utah for an interview with Overstock.com. While I enjoyed my time at TWC, my contract duration was up and being a full-time employee didn't give me the vacation time I tend to enjoy. My interview with Overstock.com was two days, with the 2nd day on the slopes at Snowbird. I was very impressed by the company, people and interview process and agreed to work there on the way to the airport.

I returned to Denver for Trish's Birthday Weekend at Breckenridge, then flew down to Naples on Wednesday to spend Christmas with her family. It was my first time meeting her parents, but that didn't stop us from having a great time talking, beach-going, golfing and relaxing.

Trish and her awesome parents Hot Santa Scotch: Making White Men Dance since 1494. Sunset in Naples

We returned to Denver, I tried to get AppFuse 2.1 finished and then we celebrated New Years with friends in Steamboat.

Good Morning from Steamboat! Sunrise over Steamboat

As I reminisce about last year, I can't help but smile. While I've been a happy person for a while, having someone to share your life with is a special thing. I feel like I dreamed up Trish a couple years ago. I was looking for someone that liked to do my favorite activities: mountain biking, skiing, traveling the world and enjoying good beer. I found that and more in Trish and couldn't be happier.

I didn't write as many technical posts on this blog as I'd like to, but I attribute that mostly to the lack of learning new things at TWC. At Overstock.com, I expect that to change and hope to have more technical articles in the coming year.

At the end of last year's Year in Review, I wrote:

I hope to speak at (or attend) 3 conferences, finish up The Bus and do a whole bunch of skiing and mountain biking.

I accomplished all but one goal: finishing The Bus. In 2011, I plan on doing two main things: keep rockin' it with Trish and finishing The Bus. Everything else is gravy. ;-)

It's gonna be a spectacular year.

Double Rainbow

Posted in Roller at Jan 10 2011, 11:42:01 AM MST 5 Comments

How I Calculated Ratings for My JVM Web Frameworks Comparison

When I re-wrote my Comparing JVM Web Frameworks presentation from scratch, I decided to add a matrix that allows you to rate a framework based on 20 different criteria. The reason I did this was because I'd used this method when choosing an Ajax framework for Evite last year. The matrix seemed to work well for selecting the top 5 frameworks, but it also inspired a lot of discussion in the community that my ratings were wrong.

I expected this, as I certainly don't know every framework as well as I'd like. The mistake I made was asking for the community to provide feedback on my ratings without describing how I arrived at them. From Peter Thomas's blog:

What you are doing is adjusting ratings based on who in the community shouts the loudest. I can't help saying that this approach comes across as highly arrogant and condescending, you seem to expect framework developers and proponents to rush over and fawn over you to get better ratings, like waiters in a restaurant trying to impress a food-critic for Michelin stars.

I apologize for giving this impression. It certainly wasn't my intent. By having simple numbers (1.0 == framework does well, 0.5 == framework is OK and 0 == framework not good at criteria) with no rationalization, I can see how the matrix can be interpreted as useless (or to put it bluntly, as something you should wipe your ass with). I don't blame folks for getting angry.

For my Rich Web Experience presentation, I documented why I gave each framework the rating I did. Hopefully this will allow folks to critique my ratings more constructively and I can make the numbers more accurate. You can view this document below or on Google Docs.

In the end, what I was hoping to do with this matrix was to simply highlight a technique for choosing a web framework. Furthermore, I think adding a "weight" to each criteria is important because things like books often aren't as important as REST support. To show how this might be done, I added a second sheet to the matrix and made up some weighting numbers. I'd expect anyone that wants to use this to downloaded the matrix, verify the ratings are accurate for your beliefs and weight the criteria accordingly.

Of course, as I and many others have said, the best way to choose a web framework is to try them yourself. I emphasized this at the end of my presentation with the following two slides.

Slide #77 from Comparing JVM Web Frameworks Talk at RWX2010

Slide #76 from Comparing JVM Web Frameworks Talk at RWX2010

Posted in Java at Dec 06 2010, 11:55:18 AM MST 10 Comments

My Everything You Ever Wanted To Know About Online Video Presentation

This week I've had the pleasure of speaking at The Rich Web Experience in Fort Lauderdale. I did two talks, one on Comparing JVM Web Frameworks and one titled Everything You Ever Wanted To Know About Online Video. Both talks had full rooms and very engaged audiences.

In the video talk, there were some audience members that knew way more than me about the topic. This made for a very interactive session and one of the most fun presentations I've ever done. It was also cool to talk about a lot of things I've learned over the last year (for more details on that, check out my team status or team hiring posts). If you don't have Flash installed, you can download a PDF of this presentation.

The first talk about Comparing JVM Web Frameworks was largely an extension of the one I presented at Devoxx two weeks ago. The main differences between this one and the last one is I extended it a bit and took into account some community feedback. However, this seemed to simply inspire anger, so I'll pass on embedding it here. You can view it on Slideshare or download the PDF.

My Comparing Web Frameworks slides often inspire harsh words, but folks really seem to like the presentation. I encourage you to watch my Devoxx presentation on Parleys.com to see for yourself.

This marks the end of 2010 conferences for me. I had a blast speaking at The Rich Web Experience, as well as TheServerSide Java Symposium, The Irish Software Show and Devoxx. Now it's time to sit back, relax, get some powder days in and find my next gig.

Hope y'all have a great holiday season!

Posted in The Web at Dec 03 2010, 10:16:44 AM MST 4 Comments