Grails on Google App Engine – Part 4: Third-Party Libraries

October 17, 2010 at 5:11 pm | Posted in GAE, Grails, Groovy, Java, Web | Leave a comment
Tags: , , , , , , , , ,

Certain aspects of web application development have already been solved by third-party libraries. Why implement a solution yourself when it’s already out there?! Not every existing library will work with App Engine though. Google published a page listing compatible libraries and frameworks for their platform. I will describe two very common use cases that I chose to use in my application: Security with Spring Security and making HTTP calls using HttpClient.

Security

Spring Security fits right into the Grails technology stack and it a proven solution for authenticating/authorizing users. This section will only describe the steps necessary to specifically integrate Spring Security with Grails on App Engine. For a detailed configuration description please refer to the Spring Security documentation. I set up my application with version 3.0.3 by dropping in the JAR files into the lib directory. Spring Security in it’s default configuration requires your container to support sessions. This is no problem for App Engine. You have to make sure though that you enable them. The configuration file appengine-web.xml has to have this entry to make it work:

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
   ...
   <sessions-enabled>true</sessions-enabled> 
   ...
</appengine-web-app>

First of all I declared my domain objects User and Role. Please see the Spring Security database schema for more information.

package com.favalike

import com.google.appengine.api.datastore.Key
import javax.jdo.annotations.*

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
class User {
   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   Key key

   @Persistent
   String username

   @Persistent
   String password

   @Persistent
   boolean enabled

   @Persistent(defaultFetchGroup = "true")
   List<Role> authorities = new ArrayList<Role>()
}
package com.favalike

import com.google.appengine.api.datastore.Key
import javax.jdo.annotations.*

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
class Role {
   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   Key key

   @Persistent
   String authority

   @Persistent
   User user
}

Because Spring Security doesn’t define a JDO UserDetailsService implementation that would be compatible with App Engine you have to write it yourself.

package com.favalike.security

import org.apache.commons.lang.StringUtils
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.context.support.MessageSourceAccessor
import org.springframework.dao.DataAccessException
import org.springframework.dao.DataRetrievalFailureException
import org.springframework.security.core.SpringSecurityMessageSource
import org.springframework.security.core.authority.GrantedAuthorityImpl
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.authentication.BadCredentialsException

class JdoUserDetailsService implements UserDetailsService {
   static final Log log = LogFactory.getLog(JdoUserDetailsService)
   def userService
   MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor()

   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
      if(StringUtils.isBlank(username)) {
         log.error "Username was empty. Authentication failed."
         throw new BadCredentialsException(messages.getMessage("login.username.blank", [] as Object[], "Username may not be empty"), username)
      }

      def user = null

      try {
         user = userService.findUserByUsername(username)
      }
      catch(Exception e) {
         log.error "Problem retrieving user from database", e
         throw new DataRetrievalFailureException(messages.getMessage("login.database.error", [] as Object[], "User could not be retrieved from database. Please try later"))
      }

      if(user == null) {
         log.error "User for username '${username}' not found in database. Authentication failed."
         throw new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.notFound", [username] as Object[], "Username {0} not found"), username)
      }

      if(log.infoEnabled) {
         log.info "Found user for username '${username}': ${user}"
      }

      def grantedAuthorities = []

      user.authorities.each { role ->
         grantedAuthorities << new GrantedAuthorityImpl(role.authority)
      }

      if(grantedAuthorities.size() == 0) {
         log.error "User needs to have at least one granted authority!"
         throw new UsernameNotFoundException(messages.getMessage("JdbcDaoImpl.noAuthority", [username] as Object[], "User {0} has no GrantedAuthority"), username)
      }

      if(log.infoEnabled) {
         log.info "User found for username '${username}'."
      }

      return new User(user.username, user.password, user.enabled, true, true, true, grantedAuthorities)
   }
}

I was able to put all the Spring wiring for Spring Security in grails-app/conf/spring/resources.groovy. My configuration is pretty much the ordinary security setup you would have to define for every application using Spring Security except that I defined it in Groovy.

beans = {
   // Security
   xmlns security: 'http://www.springframework.org/schema/security'

   jdoUserDetailsService(com.favalike.security.JdoUserDetailsService) {
      userService = ref('userService')
   }

   passwordEncoder(org.springframework.security.authentication.encoding.Md5PasswordEncoder)

   security.'http'('auto-config': true, 'access-denied-page': '/user/index') {
      security.'intercept-url'('pattern': '/user/index*', 'filters': 'none')
      security.'intercept-url'('pattern': '/user/signup', 'filters': 'none')
      security.'intercept-url'('pattern': '/favicon.ico', 'filters': 'none')
      security.'intercept-url'('pattern': '/**/*.html', 'access': 'ROLE_USER')
      security.'intercept-url'('pattern': '/**/*.js', 'access': 'ROLE_USER')
      security.'intercept-url'('pattern': '/**/*.gsp', 'access': 'ROLE_USER')
      security.'intercept-url'('pattern': '/**', 'access': 'ROLE_USER')
      security.'form-login'('authentication-failure-url': '/user/index?login_error=1', 'default-target-url': '/', 'login-page': '/user/index')
      security.'remember-me'('key': '99sdfll74soq')
      security.'logout'('logout-success-url': '/user/index')
   }

   security.'authentication-manager'('alias': 'authenticationManager') {
      security.'authentication-provider'('user-service-ref': 'jdoUserDetailsService') {
         security.'password-encoder'('hash': 'md5')
      }
   }
}

HTTP Calls

App Engine provides you with the URL Fetch API right out of the box. Even though the API does its job it is relatively feature-less. If you need more advanced feature sets like support for cookies you can turn to HttpClient. All you need to do is to download version 4 of the HttpComponents and drop the libraries into your lib directory. HttpClient needs some additional work to make it run on App Engine. It usually works with java.net package which is not allowed on App Engine. This posting shows you a way to work around it.

package com.favalike.http

import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.apache.http.Header
import org.apache.http.HttpEntity
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.conn.ClientConnectionManager
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.params.BasicHttpParams
import org.apache.http.params.CoreConnectionPNames
import org.apache.http.params.CoreProtocolPNames
import org.apache.http.params.HttpParams
import org.apache.http.util.EntityUtils

class CommonsHttpRequestExecutor implements HttpRequestExecutor {
   static final Log log = LogFactory.getLog(CommonsHttpRequestExecutor)
   final Integer timeout

   CommonsHttpRequestExecutor(timeout) {
      this.timeout = timeout
   }

   def HttpResponse sendGetRequest(String url) {
      HttpGet httpGet = new HttpGet(url);

      if(log.debugEnabled) {
         log.debug("Sending GET request to URL '${httpget.getURI()}'")
      }

      executeHttpRequest(httpGet);
   }

   def executeHttpRequest(HttpUriRequest httpUriRequest) {
      HttpResponse httpResponse = new HttpResponse()
      HttpClient httpclient = getDefaultHttpClient()

      try {
         def response = httpclient.execute(httpUriRequest)
         populateResponseData(response, httpResponse)
      }
      catch(Exception e) {
         log.error("Error accessing site for url '${httpUriRequest.getURI()}'", e)
         httpResponse.errorMessage = e.message
         httpUriRequest.abort()
      }
      finally {
         httpclient.getConnectionManager().shutdown();
      }

      httpResponse
   }

   def getDefaultHttpClient() {
      ClientConnectionManager clientConnectionManager = new GAEConnectionManager();
      HttpParams params = new BasicHttpParams();
      params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
      params.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");    
      new DefaultHttpClient(clientConnectionManager, params)
   }

   def populateResponseData(actualResponse, httpResponse) {
      httpResponse.statusCode = actualResponse.statusLine.statusCode
      httpResponse.reasonPhrase = actualResponse.statusLine.reasonPhrase
      httpResponse.statusLine = actualResponse.statusLine.toString()
      HttpEntity entity = actualResponse.getEntity();
      
      if(entity != null) {
         byte[] content = EntityUtils.toByteArray(entity)
         Header header = entity.getContentType() 
         httpResponse.responseBody = new String(content)
      }
   }
}

More on this topic:

Create a free website or blog at WordPress.com.
Entries and comments feeds.