caGrid Lift/Scala Tutorial Phase 1
You can support:download the completed phase 1 web client![]()
Organization of this Tutorial
This tutorial begins by describing the things you need to have in your environment before doing anything else. It then describes the creation of infrastructure to build the caGrid client and then infrastructure that is needed so support the core of the caGrid client itself. It then concludes with the construction of the externally visible features of login and registering a new user with Dorian.
Before You Begin
Before you begin this tutorial you should have Java JDK 1.6
and caGrid 1.4
installed in your environment.
This tutorial assumes that you have some experience developing web applications. Some basic knowledge of the Scala programming language
is assumed.
Install Lift
Download the zip file
that is used to distribute Lift. Unzip the downloaded file. Lift is now installed. However, it is good to run a sample lift project, just to make sure that it runs in your environment.
To run a sample web service, cd into the top-level directory of the lift release, then cd into the lift_basic subdirectory.
If you are in a UNIX environment, run the command

./sbt update ~jetty-run
In Windows, run the command

sbt update ~jetty-run
This is the same command you will be using later on to run the caGrid client.
The command runs the Scala Build Tool. The update action tells sbt to get or update the externally maintained files that the sample needs, such as non-core Lift jars and jetty. Jetty is the container that the web service will run in.
After you issue the sbt command, you should begin seeing some log messages that look like this:
[info] Building project Lift SBT Template 0.1 against Scala 2.8.1 [info] using LiftProject with sbt 0.7.5 and Scala 2.7.7 [info] [info] == update == [info] downloading http://repo1.maven.org/maven2/net/liftweb/lift-webkit_2.8.1/2.3/lift-webkit_2.8.1-2.3.jar ... [info] [SUCCESSFUL ] net.liftweb#lift-webkit_2.8.1;2.3!lift-webkit_2.8.1.jar (6735ms) [info] downloading http://repo1.maven.org/maven2/ch/qos/logback/logback-classic/0.9.26/logback-classic-0.9.26.jar ... [info] [SUCCESSFUL ] ch.qos.logback#logback-classic;0.9.26!logback-classic.jar (235ms) [info] downloading http://repo1.maven.org/maven2/net/liftweb/lift-mapper_2.8.1/2.3/lift-mapper_2.8.1-2.3.jar ... [info] [SUCCESSFUL ] net.liftweb#lift-mapper_2.8.1;2.3!lift-mapper_2.8.1.jar (1093ms)
These downloading messages are generated as needed files are downloaded. If you have looked at caGrid build logs, the format of these messages may look familiar to you. This is because both caGrid and sbt use a piece of software called Ivy
to manage these external dependencies.
You will know that the sample web service is running when you see output from sbt like this:
[info] == jetty-run == 2011-05-20 10:27:51.477:INFO::Logging to STDERR via org.mortbay.log.StdErrLog [info] jetty-6.1.22 [info] NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet 10:27:58.931 [main] DEBUG net.liftweb.mapper.MetaMapper - Initializing MetaMapper for User 10:28:00.775 [main] DEBUG net.liftweb.db.ProtoDBVendor - Created new pool entry. name=ConnectionIdentifier(lift), poolSize=1 10:28:01.134 [main] DEBUG net.liftweb.db.ProtoDBVendor - Released connection. poolSize=1 [info] Started SelectChannelConnector@0.0.0.0:8080 [info] == jetty-run == [success] Successful. [info] [info] Total time: 11 s, completed May 20, 2011 10:28:01 AM 1. Waiting for source changes... (press enter to interrupt)
You can shut the web service down just by pressing the enter key. Before you shut it down, you should loot at the sample service just to make sure it is there.
Point your web browser at http://localhost:8080
You should see a page that looks like this:
If you see the page, then lift is working!
Create the lift_caGrid Directory
Under the top-level directory of the Lift distribution is a directory named lift_blank. Copy that directory to somewhere outside of the Lift distribution. We will call that directory lift_caGrid. For the rest of this tutorial, we will refer to this directory created by copying as lift_caGrid.
Discover the caGrid configuration
Usually, the first thing we would set up in a lift project is a project file so that sbt will know what .jar files or other external resources need to be downloaded or copied into the project. Sbt allows us to specify the external resources that a project is dependent on in a few different ways, including embedding the dependencies in Scala code and using external Ivy files. We can even embed the Ivy
Since caGrid uses Ivy files for its dependency management, there is something to be said for doing the same for our web client. However, one of the goals for this web client is for it to require no manual configuration. To make this possible, the web client will need to include code to find the caGrid installation so it can read the target grid configuration.
The web client's external dependencies will include files in the local caGrid installation. If we use external Ivy files to configure the build dependencies then we will need to manually configure the build to know where caGrid is installed. If we embed the dependencies in Scala code, we can have sbt use the same Scala code to find the caGrid installation as the web client will use for that purpose at run time.
We will begin by writing a Scala object named CaGridInstallation. CaGridInstallation will have methods for finding the caGrid installation directory. Because we want CaGridInstallation to be used by sbt we would like it to work without the Lift framework. On the other hand, it is important for code like CaGridInstallation that has no connection with the client's user interface to be able to log its activities.
To balance these concerns, we will keep references to the Lift framework out of the CaGridInstallation code, except for the framework's Logger trait
. The Logger trait includes methods for logging messages. To allow CaGridInstallation to be used by sbt, we will provide a small stand-along implementation of the trait that includes only methods actually used by CaGridInstallation.
Before we can begin actually writing CaGridInstallation, we need to know what directory to put the file in. Under the lift_caGrid directory are two subdirectories named project and src. The project directory contains files needed to run sbt and configure it to build the project. The src directory contains files that contribute the content of the project. |
|
It turns out that CaGridInstallation.scala, the file that will contain the source code for CaGridInstallation will need to be under both directories. Since the caGrid client will have a number of source files, we want keep them in a directory structure that will be well organized. Under the src directory, we will keep the source files in a directory structure that mirrors the package organization, following the same convention as Java.
The package name for CaGridInstallation will be edu.emory.cci.caGrid.liftClient.model. We will explain the package name more fully later on. Based on the package name, we will create the file CaGridInstallation.scala in the directory lift_caGrid.src.edu.emory.cci/caGrid/liftClient/model.
Under the project directory, sbt expects to find the Scala source files under the subdirectory build. Since there will be just a few source files that we provide for sbt, we will just keep all of them the in the build directory.
We will first create the file CaGridInstallation.scala in the directory lift_caGrid.src.edu.emory.cci/caGrid/liftClient/model and then copy the file to the build directory. To help us and future maintainers of the caGrid client to remember to copy the file, we put a comment at the beginning of the file.
/******************************************************************************* ** ** The object defined in this file is used both by the caGrid client service ** AND by SBT to determine where the local caGrid class repositories are. ** ** If you make any changes to this file, copy the file to ** CAGRID_CLIENT_SERVICE_HOME/project/build ** *******************************************************************************/ package edu.emory.cci.caGrid.liftClient.model import java.io._ import java.util._ import net.liftweb._ import net.liftweb.common._ import org.apache.commons.io._ /** * An object for accesssing the caGrid installation. */ object CaGridInstallation extends Logger { /* * The installed version of the caGrid */ val Version = "1.4"
The CaGridInstallation object extends the Logger trait, which means that it inherits methods such as error, info and debug that can be used to send messages to a log.
The first member of the object is Version, whose value is the caGrid version that this client will work with. The members that follow provide us with a series of values that lead us through a chain of locations from the user's home directory to the root directory of the caGrid installation. The first of these members gives us the location of the user's .cagrid directory:
/*
* A File objects that refers to the current user's .cagrid directory.
*/
lazy val DotCagridDirectory : File = {
val homeDirectory = new File(System.getProperty("user.home"))
if (!homeDirectory.isDirectory()) {
val msg = "The value of user.home is \"" + homeDirectory.getAbsolutePath() + "\" which is not a directory!"
error(msg)
throw new RuntimeException(msg)
}
val cagridDirectory = new File(homeDirectory, ".cagrid")
if (!cagridDirectory.isDirectory()) {
val msg = cagridDirectory.getAbsolutePath() + " either does not exist or is not a directory."
error(msg)
throw new RuntimeException(msg)
}
info(".cagrid directory is at " + cagridDirectory.getAbsolutePath())
cagridDirectory
}
The value DocCagridDirectory is declared lazy so that its initializer will be evaluated only once.
Under the .cagrid directory is a directory that used by the installer for the targeted version of caGrid to contain information about the configuration of the installation. The location of this installation configuration directory is the value of InstallerDirectory.
/**
* The directory in which the installer put its status and cached files.
*/
lazy val InstallerDirectory : File = {
val installerDirectoryName = "installer-" + Version
val directory = new File(DotCagridDirectory, installerDirectoryName)
if (!directory.isDirectory ) {
val msg = directory.getAbsolutePath() + " is not a directory!"
error(msg)
throw new RuntimeException(msg)
}
info("Installer directory is " + directory.getAbsolutePath())
directory
}
Under the directory identified by InstallerDirectory is a properties file that contains various pieces of information about how CaGrid was installed. The value of the object's PropertiesFile member is a File object that can be used to open the properties file.
/** * The name of the properties file in which the installer keeps properties about the caGrid installation. */ val PropertyFileName = "cagrid.installer.properties" /** * The file that contains the installation properties. */ val PropertiesFile = new File(InstallerDirectory, PropertyFileName)
A Properties object that contains the property-value pairs from the file identified by PropertiesFile is the value of the InstallerProperties member:
/**
* A properties object that contains the properties that the installer set.
*/
lazy val InstallerProperties : Properties = {
if (PropertiesFile.canRead()) {
val theProperties = new Properties()
var propertyInput : InputStream = null
try {
info("Loading properties from " + PropertiesFile.getAbsolutePath())
propertyInput = new BufferedInputStream(new FileInputStream(PropertiesFile))
theProperties.load(propertyInput)
// This will be the value of installerProperties
theProperties
} catch {
case e:IOException =>
{
val msg = "Error occurred while trying to read a properties file describing a caGrid installation: " + PropertiesFile.getAbsolutePath()
error(msg, e)
throw new RuntimeException(msg, e)
}
} finally {
if (propertyInput != null) {
IOUtils.closeQuietly(propertyInput);
}
}
} else {
val msg = "Found installer directory " + InstallerDirectory.getAbsolutePath() +
", but the directory does not contain a readable file named " + PropertyFileName
error(msg)
throw new IOException(msg)
}
}
One of the properties in the InstallerProperties Properties object has the name cagrid.home. The value of the cagrid.home property is a File object that identifies the root directory of the caGrid installation. This File object is also the value of CagridHomeProperty.
private val CagridHomeProperty = "cagrid.home" /** * The root directory of the caGrid installation. */ lazy val homeDirectory : File = { val homePath = InstallerProperties.get(CagridHomeProperty).toString() if (homePath == null) { val msg = "The installer property file does not set a value for the property " + CagridHomeProperty + ": " + PropertiesFile.getAbsolutePath() error(msg) throw new RuntimeException(msg) } new File(homePath) } }
That's all there is to the CaGridInstallation object, except for one detail. The Logger trait that CaGridInstallation extends is part of the Lift framework. We don't want to drag the entire Lift framework into the sbt environment. To eliminate the dependency on the Lift Framework we provide an alternate minimal implementation of Logger in the project/build directory that is a thin layer on top of the log4j logging package:
package net.liftweb { package common { import org.apache.log4j._ /** * This is a replacement impelmentation of the Logger trait so that * the CagridInstallation object can function when used within sbt. * Logger is a thin wrapper on top of an SLF4J Logger */ trait Logger { def loggerNameFor(cls: Class[_]) = { val className = cls.getName if (className endsWith "$") className.substring(0, className.length - 1) else className } private lazy val logger: org.apache.log4j.Logger = _logger // removed @transient 'cause there's no reason for transient on val // changed to lazy val so it only gets initialized on use rather than on instantiation protected def _logger = LogManager.getLogger(this.getClass) def assertLog(assertion: Boolean, msg: => String) = if (assertion) info(msg) /** * Log the value of v with trace and return v. Useful for tracing values in expressions */ def trace[T](msg: String, v: T): T = { logger.trace(msg+": "+v.toString) v } def trace(msg: => AnyRef) = if (logger.isTraceEnabled) logger.trace(String.valueOf(msg)) def trace(msg: => AnyRef, t: Throwable) = if (logger.isTraceEnabled) logger.trace(String.valueOf(msg), t) def isTraceEnabled = logger.isTraceEnabled def debug(msg: => AnyRef) = if (logger.isDebugEnabled) logger.debug(String.valueOf(msg)) def debug(msg: => AnyRef, t: Throwable) = if (logger.isDebugEnabled) logger.debug(String.valueOf(msg), t) def isDebugEnabled = logger.isDebugEnabled def info(msg: => AnyRef) = if (logger.isInfoEnabled) logger.info(String.valueOf(msg)) def info(msg: => AnyRef, t: => Throwable) = if (logger.isInfoEnabled) logger.info(String.valueOf(msg), t) def isInfoEnabled = logger.isInfoEnabled def warn(msg: => AnyRef) = if (logger.isEnabledFor(Level.WARN)) logger.warn(String.valueOf(msg)) def warn(msg: => AnyRef, t: Throwable) = if (logger.isEnabledFor(Level.WARN)) logger.warn(String.valueOf(msg), t) def isWarnEnabled = logger.isEnabledFor(Level.WARN) def error(msg: => AnyRef) = if (logger.isEnabledFor(Level.ERROR)) logger.error(String.valueOf(msg)) def error(msg: => AnyRef, t: Throwable) = if (logger.isEnabledFor(Level.ERROR)) logger.error(String.valueOf(msg), t) def isErrorEnabled = logger.isEnabledFor(Level.ERROR) } } }
The CaGridInstallation object provides all the information that is needed about the local caGrid installation to build the caGrid client. Because it is shared by both the sbt build tool and the caGrid client itself, we can feel confident that if it works well enough for sbt to build the client, it will work correctly within the client.
In the following section of this tutorial, we will configure the build tool sbt to find all .jar files and other external resources needed to build the caGrid client. This will include making use of the CaGridInstallation object to find .jar files that are in the caGrid directory tree.
Configure Dependencies
In this part of the tutorial, we discuss how to configure the tool we are using to build lift/scala projects to find all of the .jar file and other external dependencies needed for our caGrid client. The build tool that we are using is named sbt
(simple build tool).
Sbt is a scala-based tool for building scala-based projects (it is actually more flexible than that). There are a few ways that it can be customized. Most of these involve writing or modifying a scala object. The particular customization technique that we will focus on is providing a project class.
If sbt finds a project class, it delegates some parts of its behavior to members of the project class. The project class can have any name. Sbt find a project class by looking for a class (under the project directory) that extends DefaultWebProject. Here is the project class LiftProject:
import java.io.File import sbt._ import edu.emory.cci.caGrid.liftClient.model.CaGridInstallation class LiftProject(info: ProjectInfo) extends DefaultWebProject(info) {
Sbt supports a few different ways of specifying what external artifacts are needed to build something. Since caGrid uses ivy
to manage its dependencies (.jar files and such), we choose to specify the caGrid client's external dependencies in a way that is consistent with ivy. Sbt supportw three ways of doing this.
- We can use a separate xml file.
- We can use xml embedded in the code.
- We can embed the dependency information directly in code the build a data structure.
| How Ivy Organizes Dependencies Ivy organizes each dependency as a set of files. Each set of files is identified by either three or four pieces of information.
The set of .jar files produced by a build of caGrid's Dorian service is organized into named configurations:
When we specify configuration information for a dependency, we specify configurations in pairs. The first is the name of a configuration of the project we are building. The second is a the name of a configuration of the module that the project is dependent on. For example, suppose that we are building a hypothetical service called Homgow. The hypothetical Homgow service needs some of the internal logic used by the Dorian service, so there will be a configuration of Homgow called "default" that depends on the "common" configuration of Dorian. Homgow will also have a "test" configuration that contains all of the artifacts needed to test Homgow. If Homgow's "test" configuration includes a Dorian client that is used to validate Homgow's behavior, then Homgow's "test" configuration will depend on Dorian's "client" configuration. The way that we write this as configuration information is default->common,test->client If no configurations are explicitly defined for a module, then a configuration named "default" is implicitly defined for the module. If we want to indicate that all configurations of a project depend on a configuration of a module we can use "*" as a shorthand for all configurations: *->default See http://ant.apache.org/ivy |
Since lift is distributed with its external dependencies already embedded in code, we will expand that code to also include the caGrid client's dependencies. The way that we do this is to override the definition of a method named libraryDependencies:
val liftVersion = "2.3" override def libraryDependencies = Set( "net.liftweb" %% "lift-webkit" % liftVersion % "compile", "net.liftweb" %% "lift-mapper" % liftVersion % "compile", "org.mortbay.jetty" % "jetty" % "6.1.22" % "test", "junit" % "junit" % "4.5" % "test", "ch.qos.logback" % "logback-classic" % "0.9.26", "org.scala-tools.testing" %% "specs" % "1.6.6" % "test", "com.h2database" % "h2" % "1.2.138", "apache" % "log4j" % "1.2.14", "joda-time" % "joda-time" % "1.6.2", // caGrid dependencies "apache" % "commons-logging" % "1.1", "apache" % "commons-discovery" % "0.4", "caGrid" % "authentication-service" % CaGridInstallation.Version % "*->client", "caGrid" % "dorian" % CaGridInstallation.Version % "*->client", "caGrid" % "opensaml" % CaGridInstallation.Version % "*->default", "caGrid" % "syncgts" % CaGridInstallation.Version % "*->client" ) ++ super.libraryDependencies /***************************************************************************** ** the following class members for for CaGrid-related dependencies: *****************************************************************************/ // The caGrid repository val cagridDirectory = CaGridInstallation.homeDirectory
The overridden method libraryDependencies is called to get a list of dependencies. Each dependency is built separating the organization, module, version and optional configuration information with a percent sign ("%"). The exception to this are modules that are part of Lift. The lift documentation says that a double percent sign ("%%") is needed for these.
The remaining members of the LiftProject class specify the repositories that contain the jar files that are part of caGrid:
val irPath = new File(cagridDirectory, "integration-repository").getAbsolutePath() val localRepo = (Resolver.file("caGrid-local", new File(cagridDirectory, "integration-repository")) .ivys(irPath+"/[organisation]/[module]/ivy-[revision].xml") .artifacts(irPath+"/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]") .artifacts(irPath+"/[organisation]/[module]/[revision]/[artifact].[ext]")) val rPath = new File(cagridDirectory, "repository").getAbsolutePath() val externalRepo = (Resolver.file("caGrid-external", new File(cagridDirectory, "repository")) .ivys(rPath + "/[organisation]/[module]/ivy-[revision].xml") .artifacts(rPath + "/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]") .artifacts(rPath + "/[organisation]/[module]/[revision]/[artifact].[ext]")) }
SBT recognizes members of libraryDependencies that describe a repository by their data type. The two repositories correspond the directories under the caGrid root directory names integration-repository and repository.
For the purpose of finding files in an ivy repository, the repository contains two kinds of files:
- Artifact files are the file constitute the contents of modules.
- Ivy files describe a version of a module, the module's configuration and what artifact files comprise each configuration of a revision of a module.
Both repositories are organized so that under the root directory of the repository is a directory whose name is the same as the name of a module's organization. Under each organization are directories whose name is the same as a module. Ivy files are in the module directories with a name that begins with "ivy-" followed by a module revision number followed by ".xml" For example, if sbt is looking for a module describe by organization="apache", module="commons-lang" and revision="1.3", it will look for its ivy file under the caGrid root directory using the path
integration-repository/apache/commons-lang/ivy-1.3.xml.
Artifact files are in a directory under the module directory whose name matches the revision of the desired module. The names of the artifact files will begin with an artifact name specified by the ivy file, followed by "-", followed by the module revision, followed by "." followed by the extension specified in the ivy file; or just the artifact name specified by the ivy file followed by "." followed by the extension specified in the ivy file.
Initializing the Client
The Lift framework initializes an application by instantiating a class named Boot The Boot class begins like this.
package bootstrap.liftweb import net.liftweb._ import util._ import Helpers._ import common._ import http._ import sitemap._ import Loc._ import mapper._ import code.model._ import edu.emory.cci.caGrid.liftClient.model.CaGrid /** * A class that's instantiated early and run. It allows the application * to modify lift's environment */ class Boot extends Bootable with Logger { def boot { info("Booting up the web client service.")
The boot method is responsible for the driving the actual initialization. Most of the initialization is done by calling the LiftRules object's methods. The Lift framework uses the LiftRules object to determine many of its run-time settings and parameters. When the boot method calls one of the LiftRules object's methods, it is generally to set a parameter the will affect the future behavior of the client.
The first call to a LiftRules method is to give the Lift framework the name of a package that it should look in to find a type of class that Lift calls snippets. Snippets are classes provided by an application that List calls when its needs to fill in some blanks in a piece of html. We will look at actual snippet classes later as we discuss the parts of the client that each class supports.
// where to search for snippets
LiftRules.addToPackages("edu.emory.cci.caGrid.liftClient")
The next piece of initialization is to create a SiteMap and pass it to LiftRules so that the application uses it. The SiteMap controls the creation of all the pages in the client. If a page is not in the site map then it cannot be accessed. The site map also creates a menu for navigation of an application's pages. We also use the SiteMap to dynamically determine if a user should be able to access a page.
// Build SiteMap
def sitemap = SiteMap(
Menu("Login") / "index" >> If(() => !CaGrid.credentials.hasValidUserCertificate, "Logging in is for people who have not yet logged in."),
Menu("Home") / "home" >> If(() => CaGrid.credentials.hasValidUserCertificate, "You must be logged in."),
Menu("Logout") / "logout" >> If(() => CaGrid.credentials.hasValidUserCertificate, "You must be logged in."),
Menu("Request a User ID") / "request_uid" >> If(() => !CaGrid.credentials.hasValidUserCertificate, "You are already logged in."),
Menu("Service Selection") / "service_selection" >> If(() => CaGrid.credentials.hasValidUserCertificate, "You must be logged in.")
)
LiftRules.setSiteMap(sitemap)
Each of the pages described in the above site map has:
- A menu item
A menu item created by passing the name of the page to Menu. Menu items are display on the side of client pages. - A template.
A template is an html file that has some specially attributed HTML elements that tell the Lift framework how to replace or modify the html element. - A guard.
A guard contains a Boolean value the is used to determine if the current user is allowed to access a page. If a page's guard returns false, then its menu item will be left out of the menu and attempts to access the page will fail.
Page guards are created by calls to If. The arguments to If are a method that returns a boolean value (()=>Boolean) and a String. The method passed to If is called to determine if the user is allowed to navigate to the page. If this method returns false then the user is not allowed to navigate to the page. The string passed to If is a message explaining to the user why navigation to the page is not allowed.
There are two other call to customize the contents of LiftRules. One is to set the method that should be called to determine if the current user is logged in. The other is to tell Lift to render the html templates as HTML 5. The default is for templates to be rendered as XHTML.
When Lift was originally designed, it looked like XHTML would be the way of the future. XHTML has not been as popular as was expected. Most current browsers support HTML 5, so it is recommended that new Lift-based applications use HTML 5 rendering.
// What is the method to test if a user is logged in?
LiftRules.loggedInTest = Full(() => CaGrid.credentials.hasValidUserCertificate)
// Use HTML5 for rendering
LiftRules.htmlProperties.default.set((r: Req) =>
new Html5Properties(r.userAgent))
The boot method concludes by initiating synchronization with the trust fabric (discussed in greater detail further down this page) and logging the completion of boot
CaGrid.syncWithTrustFabricAndKeepSyncing()
info("boot finished")
}
}
Synchronization with the trust fabric is initiated by a call to one of the CaGrid object's methods. The CaGrid object is the subject of the next section of this page.
Run-Time Interactions with CaGrid
We support:previously looked at the CaGridInstallation object which is used to find the caGrid installation directory. The CaGrid object has methods for getting information about locations under the caGrid installation directory and also for getting run-time information about caGrid.
package edu.emory.cci.caGrid.liftClient.model import java.io._ import net.liftweb._ import net.liftweb.common._ import net.liftweb.http._ import org.apache.axis.utils.XMLUtils import org.apache.commons.io._ import org.globus.wsrf.encoding.ObjectDeserializer import org.xml.sax.InputSource import gov.nih.nci.cagrid.syncgts.bean.SyncDescription import gov.nih.nci.cagrid.syncgts.core.SyncGTS import org.cagrid.gaards.dorian.client.DorianClient import org.cagrid.gaards.dorian.client.GridUserClient /** * An object for managing interactions with caGrid. */ object CaGrid extends Logger {
The source for the CaGrid object begins with the usual Lift-related imports. Imports needed for Globus and caGrid support follow. The CaGrid object inherits Lift's Logger train so that it can generate log messages.
The first member of the CaGrid object is a private SessionVar to contain a CredentialProxy for the current user. SessionVar is a class provided by the Lift framework to contain session-specific values.
A SessionVar object contains a value. The value that it contains will depend on the session that is in scope when its value is retrieved. The value will either be the value that the SessionVar object was previous set to when the same session was in scope or if no value have been set in the context of the current session, then its value is the default passed to its constructor.
private[this] object sessionCredential extends SessionVar[Option[CredentialProxy]](None)
The sessionCredential object is a SessionVar that initially contains None. The first time it is accessed, a CredentialProxy() object is created and the CredentialProxy object becomes the value of sessionCredential for the current session. This is accomplished by requiring clients to access the sessionCredential object through the credentials method.
def credentials:CredentialProxy = {
if (sessionCredential.isEmpty) {
sessionCredential.set(Some(new CredentialProxy()))
}
sessionCredential.get.get
}
The caGrid client defines a class named CredentialProxy for creating and managing user credentials. For each web session, the caGrid client will have a corresponding CredentialProxy object. The CredentialProxy object is accessed by calling the CaGrid object's credentials method.
The name of the currently configured target grid is the value of the CaGrid object's targetGridName value. It gets the name of the currently configured target grid as the value of the target.grid property in the properties file in the .currentgrid.properties file in the root directory of the caGrid installation.
/** * The name of the configured target grid */ lazy val targetGridName : String = { var inputStream : InputStream = null var targetGridPropertiesFile = new File(CaGridInstallation.homeDirectory, ".currentgrid.properties") info("Getting name of target grid from file: " + targetGridPropertiesFile.getAbsolutePath()) val targetGridProperties = new java.util.Properties() try { inputStream = new BufferedInputStream(new FileInputStream(targetGridPropertiesFile)) targetGridProperties.load(inputStream) } catch { case e: Exception => { val msg = "An error occured while trying to read a properties file to determine the current target grid" error(msg, e) throw new RuntimeException(msg, e) } } finally { if (inputStream != null) { IOUtils.closeQuietly(inputStream) } } val targetGridNameString = targetGridProperties.getProperty("target.grid") if (targetGridNameString == null) { val msg = "Properties file does not specify a value for \"target.grid\": " + targetGridPropertiesFile.getAbsolutePath() error(msg) throw new RuntimeException(msg) } info("Target grid name is: " + targetGridNameString) targetGridNameString }
The value of repositoryDirectory identifies the directory that is the root of the Ivy repository that contains .jar files and other kinds of files needed to build caGrid.
/** * The local repository that contains caGrid's dependencies. */ lazy val repositoryDirectory = new File(CaGridInstallation.homeDirectory, "repository")
The value of targetGridParentDirectory identifies the directory that contains configuration information for all known target grids.
/** * The directory that contains all the configured targe-grid ivy files and target-grid directories. */ lazy val targetGridParentDirectory = new File(new File(repositoryDirectory, "caGrid"), "target_grid")
The value of targetGridDirectory identifies directory that contains configuration information for the currently configured target grid.
/** * The directory that contains the configuration information for the target grid. */ lazy val targetGridDirectory = new File(targetGridParentDirectory, targetGridName)
The value of serviceUrlProperties is a java.util.Properties object that contains URLs for the currently configured target grid's core services. It gets this by reading the service_urls.properties file in the directory identified by targetGridDirectory.
/** * The properties for the url of core services for the currently configured target grid */ private lazy val serviceUrlProperties:java.util.Properties = { val propertiesFile = new File(targetGridDirectory, "service_urls.properties") val theProperties = new java.util.Properties() var propertyInput : InputStream = null try { info("Loading service url properties from " + propertiesFile.getAbsolutePath()) propertyInput = new BufferedInputStream(new FileInputStream(propertiesFile)) theProperties.load(propertyInput) // This will be the value of serviceUrlProperties theProperties } catch { case e:IOException => { val msg = "Error occurred while trying to read a properties file specifying core services URLs: " + propertiesFile.getAbsolutePath() error(msg, e) throw new RuntimeException(msg, e) } } finally { if (propertyInput != null) { IOUtils.closeQuietly(propertyInput); } } }
The value of indexUrl is either Some(String) that is the URL of the currently configured target grid's index service or None if the currently targeted grid does not have an index service. The URL string is obtained by getting the value of the cagrid.master.index.service.url property from serviceUrlProperties.
/** * The URL of the index service */ val indexUrl:Option[String] = { val indexProperty = serviceUrlProperties.getProperty("cagrid.master.index.service.url") if (indexProperty == null) { None } else { Some(indexProperty) } }
The value of cadsrUrl is the URL sring for the currently configured target grid's CADSR service.
/**
* the URL of the CADSR service
*/
val cadsrUrl = "http://cadsr-dataservice.nci.nih.gov:80/wsrf/services/cagrid/CaDSRDataService"
The value of mmsUrl is either Some(String) that is the URL of the currently configured target grid's metadata model service or None if the currently targeted grid does not have an mms service. The URL string is obtained by getting the value of the cagrid.master.mms.service.url property from serviceUrlProperties.
/** * The URL of the Metadata Model Service. */ val mmsUrl:Option[String] = { val mmsProperty = serviceUrlProperties.getProperty("cagrid.master.mms.service.url") if (mmsProperty == null) { None } else { Some(mmsProperty) } }
The value of gmeUrl is either Some(String) that is the URL of the currently configured target grid's global model exchange or None if the currently targeted grid does not have an gme service. The URL string is obtained by getting the value of the cagrid.master.gme.service.url property from serviceUrlProperties.
/** * The URL of the Global Model Exchange. */ val gmeUrl:Option[String] = { val gmeProperty = serviceUrlProperties.getProperty("cagrid.master.gme.service.url") if (gmeProperty == null) { None } else { Some(gmeProperty) } }
The value of grouperUrl is either Some(String) that is the URL of the currently configured target grid's grid grouper service or None if the currently targeted grid does not have a grid grouper service. The URL string is obtained by getting the value of the cagrid.master.gridgrouper.service.url property from serviceUrlProperties.
/** * The URL of the Grid Grouper service */ val grouperUrl:Option[String] = { val grouperProperty = serviceUrlProperties.getProperty("cagrid.master.gridgrouper.service.url") if (grouperProperty == null) { None } else { Some(grouperProperty) } }
The value of cdsUrl is either Some(String) that is the URL of the currently configured target grid's credential delegation service or None if the currently targeted grid does not have a credential delegation service. The URL string is obtained by getting the value of the cagrid.master.cds.service.url property from serviceUrlProperties.
/** * The URL of the Credential Delegation Service */ val cdsUrl:Option[String] = { val cdsProperty = serviceUrlProperties.getProperty("cagrid.master.cds.service.url") if (cdsProperty == null) { None } else { Some(cdsProperty) } }
The value of dorianUrl is the URL string of the currently configured target grid's Dorian service. The URL string is obtained by getting the value of the cagrid.master.dorian.service.url property from serviceUrlProperties.
/** * The URL of the Dorian Service */ val dorianUrl:String = { val dorianProperty = serviceUrlProperties.getProperty("cagrid.master.dorian.service.url") if (dorianProperty == null) { val msg = "Target grid configuration does not include a URL for the dorian service" error(msg) throw new RuntimeException(msg) } dorianProperty }
The value of syncDescriptionFile identifies the file that contains configuration information that is needed to synchronize with the trust fabric.
/** * The file that contains configuration information for syncGTS */ val syncDescriptionFile = new File(targetGridDirectory, "sync-description.xml");
The value of dotGlobusDirectory identifies the .globus directory that is the root for globus configuration information.
val dotGlobusDirectory = new File(scala.util.Properties.userHome, ".globus")
The syncWithTrustFabricAndKeepSyncing synchronously performs an immediate synchronization with the trust fabric and then causes asynchronous synchronizations with the the trust fabric to be performed periodically. The synchronization operations are performed using the configuration information in the file identified by syncDescriptionFile and manage trust information in the certificates directory under the directory identified by dotGlobusDirectory.
/**
* Perform initial synchronization with the trust fabric and then start a
* background thread to perform additional synchronizations.
*/
def syncWithTrustFabricAndKeepSyncing() {
info("Starting Synchronization with trust fabric.")
try {
val syncDescriptionString = FileUtils.readFileToString(syncDescriptionFile)
val certificatesDirectory = new File(dotGlobusDirectory, "certificates")
if (certificatesDirectory.mkdirs()) {
info("Created " + certificatesDirectory.getAbsolutePath())
} else {
FileUtils.cleanDirectory(certificatesDirectory)
}
val targetCertificatesDirectory = new File(targetGridDirectory, "certificates")
FileUtils.copyDirectory(targetCertificatesDirectory, certificatesDirectory, true)
val doc = XMLUtils.newDocument(new InputSource(new StringReader(syncDescriptionString)))
val description = ObjectDeserializer.toObject(doc.getDocumentElement(), classOf[SyncDescription]).asInstanceOf[SyncDescription]
val syncGts = SyncGTS.getInstance()
syncGts.syncOnce(description) // sync now
info("Initial synchronization with trust fabric complete. Initiating thread to perform future re-synchronization.")
syncGts.syncAndResyncInBackground(description, true) // keep sync'ing in the background
} catch {
case e: Exception => {
val msg = "Synchronization with trust fabric failed!"
error(msg, e)
throw new RuntimeException(msg, e)
}
}
info("Finished initial synchronization with trust fabric.")
}
The registerLocalUser method takes information about a user that has been collected into an org.cagrid.gaards.dorian.idp.Application object and tries to register the user with the Dorian service.
/** * Request registration of a user described by the given Application object * * @return A string describing the result of the request. */ def registerLocalUser(userApplication:org.cagrid.gaards.dorian.idp.Application):String = { val dorianClient = new DorianClient(dorianUrl) if ( dorianClient.doesLocalUserExist( userApplication.getUserId()) ) { "User already registered: " + userApplication.getUserId() } else { dorianClient.registerLocalUser(userApplication) } } }
Uses for the above infrastructure are described under the following section headings.
Authenticate users against Dorian
The infrastructure we have discussed under the preceding section headings is not directly visible to the end user. It gets used before the user gets to this login screen:
The configuration information that creates this screen and integrates it with the infrastructure we discussed previously is discussed under the following sub-headings.
Implicit Navigation to the First Page
You may have noticed that we did nothing during the caGrid client's initialization to tell it what page to present initially. When the Lift framework has not been told where to start, it defaults to the first page in the current SiteMap.
Recall that in the Boot class's boot method support:we created a SiteMap object and made it the current SiteMap by passing it to LiftRules.setSiteMap. For your convenience, here is the first page description from the SiteMap:
Menu("Login") / "index" >> If(() => !CaGrid.credentials.hasValidUserCertificate, "Logging in is for people who have not yet logged in.")
This tells the Lift framework that the page is named "index". In absence of any other configuration, when this becomes the current page Lift will look for a file named index.html in the directory src/main/webapp. Here is the content of the index.html file:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<title>Login</title>
</head>
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
<span class="lift:SiteInfo">
<h2>CaGrid Web Client for <span id="targetGridName">the name of the target grid goes here.</span></h2>
<div id="time">Time goes here</div>
</span>
<p>
<form class="lift:Helloworld.login?form=POST">
<table >
<tbody>
<tr>
<td style="background-color: white;">User Name</td>
<td style="background-color: white;"><input name="user" maxlength="60" /></td>
</tr>
<tr>
<td style="background-color: white;">Password</td>
<td style="background-color: white;"><input name="password" type="password" /></td>
</tr>
<tr>
<td style="background-color: white;">Lifetime</td>
<td style="background-color: white;">
<input name="lifehours" /> hours <input name="lifeminutes" /> minutes
</td>
</tr>
<tr>
<td style="background-color: white;" colspan="2" align="center">
<input type="submit" value="Login"></input>
</td>
</tr>
</tbody>
</table>
</form>
</p>
</div>
</body>
</html>
There are a two things you should notice about this raw HTML:
- This is valid HTML that can be rendered by a browser.
- Some of the HTML elements contain a class attribute whose value begins with "lift:…". These class attributes tell lift what to do with the HTML. This way of telling Lift what to do has the advantage of not getting in the way of conventional HTML authoring tools.
The Lift term for an HTML file that contains these special class attributes is "template file". We will discuss how the Lift framework processes template files later on. For now, let us look at how a browser renders this HTML:

Two more things to notice about the HTML are:
- it contains placeholder text that is replaced with live data when this HTML is processed by the Lift framework: "the name of the target grid goes here." is replaced with the actual name of the target grid; "Time goes here" is replaced with the current time.
- The heading from the top of the live page and the menu from the left side of the page are missing.
In the next section of this tutorial we will discuss how the heading and menu are added to the live version of this page (and other pages too). We will discuss how the Lift framework replaces placeholders with actual content later in this tutorial.
Common Page Layout
Since most web sites are organized to have common element in their pages such as heading, menus and footers, the Lift framework provides a way for a page's HTML to indicate that a part of it should be embedded in the content of another HTML file.
<div id="main" class="lift:surround?with=default;at=content">
The above div element from the index.html file has a class attribute whose value is a direction to the Lift framework. It can be recognized as a Lift framework directive because it begins with "lift:". What follows the colon (:) is an identifier that tells Lift what to do.
The identifier in this case is surround, which tells lift to embed the div element in another HTML file.
What follows the question mark (?) are parameter-name/value pairs. The value of the with parameter is the name of the file in which to embed this div element. The value of the at parameter is the id of the element in the enclosing HTML file that that will be replaced by this div element.
The effect of this class attribute value is to tell the Lift framework to render this HTML file by finding a file named default.html and replacing the element in default.html whose id is
"content" with this div element. The portion of the index.html file outside of the div element is not used to render the page.
The lift framework finds the default.html file in the src/main/webapp/templates-hidden directory. When searching for template files, Lift normally ignores the templates-hidden directory and any other directory whose name ends with -hidden. However, when looking for a template in which to embed content, Lift does look in directories whose name ends with -hidden.
The default.html file is provided as port of the Lift framework. I made some small edits to customize the page layout for caGrid. Below are some relevant excepts from the default.html file. In parts of the file that I have edited, the original content appears as a comment below my edit.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:lift="http://liftweb.net/"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="description" content="" /> <meta name="keywords" content="" /> <title class="lift:Menu.title">+CAGrid Web Client+-App: -</title> <style class="lift:CSS.blueprint"></style> <style class="lift:CSS.fancyType"></style> <script id="jquery" src="/classpath/jquery.js" type="text/javascript"></script> <script id="json" src="/classpath/json.js" type="text/javascript"></script> <style type="text/css"> /* <![CDATA[ */ .edit_error_class { display: block; color: red; } #lift__noticesContainer___notice { background-color: lime; } #lift__noticesContainer___warning { background-color: yellow; } #lift__noticesContainer___error { background-color: red; } <!-- I added the above to control the background color for each type of status message that can be displayed. --> ? </style> </head> <body> <div class="container"> <div class="column span-12 last" style="text-align: right"> <h1 class="alt">CaGrid Web Client<img alt="" id="ajax-loader" style="display:none; margin-bottom: 0px; margin-left: 5px" src="/images/ajax-loader.gif"></h1> <!-- <h1 class="alt">app<img alt="" id="ajax-loader" style="display:none; margin-bottom: 0px; margin-left: 5px" src="/images/ajax-loader.gif"></h1> --> </div> <hr> <div class="column span-6 colborder sidebar"> <hr class="space" > <span class="lift:Menu.builder"></span> <div class="lift:Msgs?showAll=true"></div> <hr class="space" /> </div> <div class="column span-17 last"> <div id="content">The main content will get bound here</div> </div> <hr /> <!-- <div class="column span-23 last" style="text-align: center"> <h4 class="alt"> <a href="http://www.liftweb.net"><i>Lift</i></a> is Copyright 2007-2011 WorldWide Conferencing, LLC. Distributed under an Apache 2.0 License.</h4> </div> --> </div> </body> </html>
DIV and SPAN HTML Elements
You will notice that most Lift directives that appear as class attributes are in div or span elements. These are popular because you can add them to existing html without messing up the formatting. The span element has no affect on how HTML is formatted. The div element implies a line break.
There is no reason that a Lift directive cannot be added to other kinds of HTML elements. Putting them in div or span elements is often just more convenient.
Replacing Placeholders with Snippets
Earlier, we pointed out the that the index.html file contains some placeholder text. The following paragraphs explain how these placeholders are replaced with actual content.
The following code listing shows the span element from the index.html file that contains the placeholder text.
<span class="lift:SiteInfo"> <h2>CaGrid Web Client for <span id="targetGridName">the name of the target grid goes here.</span></h2> <div id="time">Time goes here</div> </span>
Up to this point, we have referred to class attribute values that look like lift:SiteInfo as lift directives. This is a simplification we have use so to that we could put off explaining that this is a way of calling a type of Scala method called a snippet. A snippet is a Scala method that Lift calls to transform an input XML element into an output XML element.
Snippets are a very flexible mechanism and very central to the design of Lift. Here is what you need to know about snippets for now:
The word that appears after lift: in the value of a class attribute is treated as a class name. The class attribute value lift:SiteInfo tells lift to call the SiteInfo class's render method. If there is a snippet method to be called whose name is not render, then the method name can be specified like this: lift:Abc.doit which tells Lift to call the doit method of the abc class.
If the purpose of a snippet method is just replace the HTML element that calls it, the snippet method just needs to return a NodeSeq object to replace the HTML element. If the method needs to look at the element it is replacing it can take a NodeSeq as an argument; otherwise it can take no argument.
Snippets can be used to replace selected portions of the HTML contained by the element that calls the snippet. This is the nature of the snippet method invoked by the above example:
package edu.emory.cci.caGrid.liftClient.snippet import scala.xml.{NodeSeq, Text} import net.liftweb.util._ import net.liftweb.common._ import net.liftweb.http._ import java.util.Date import code.lib._ import Helpers._ import edu.emory.cci.caGrid.liftClient.model.CaGrid class SiteInfo extends Logger { def render = "#time" #> (new Date()).toString & "#targetGridName" #> CaGrid.targetGridName }
The above render method is an example of a snippet method that replaces selected nodes of the HTML tree under the element that causes the method to be called. It returns an object that tells Lift what changes to make to the HTML tree. The object returned by the above render method tells the Lift framework to replace the element whose id is time with text that consists of the string that contains the current time; it tells the Lift framework to replace the element whose id is targetGridName with text that consists of the taget grid name provided by the CaGrid object we discussed previously.
The #> operator constructs a description of a replacement to be made in HTML. The left argument is a string that identifies an element to be replaced. The right argument is a value that is used to replace the element. The & operator is used to assemble multiple replacements into a single object.
The string that specifies what elements are to be replaced can follow one of a few different patterns based on CSS selector syntax. Here are some of the possibilities:
- #zot – selects elements with a id attribute of "zot"
- @zot – selects elements with a name attribute of "zot"
- .zot – selects elements with a CSS class of "zot"
- attributeName=zot – selects elements with an attribute named "attributeName" having its value equal to "zot".
A variety of different types of value can be used as the right argument of #> to specify a replacement value. These include String, NodeSeq, NodeSeq=>NodeSeq and Seq[support:NodeSeq].
A complete discussion of snippets is beyond the scope of this tutorial. You can find more information about snippets in the on-line book Exploring Lift
.
Forms
Enabling HTML forms to work with lift is rather simple. Here is the login form from index.html:
<form class="lift:Helloworld.login?form=POST"> <table > <tbody> <tr> <td style="background-color: white;">User Name</td> <td style="background-color: white;"><input name="user" maxlength="60" /></td> </tr> <tr> <td style="background-color: white;">Password</td> <td style="background-color: white;"><input name="password" type="password" /></td> </tr> <tr> <td style="background-color: white;">Lifetime</td> <td style="background-color: white;"> <input name="lifehours" /> hours <input name="lifeminutes" /> minutes </td> </tr> <tr> <td style="background-color: white;" colspan="2" align="center"> <input type="submit" value="Login"></input> </td> </tr> </tbody> </table> </form>
All that was needed to Lift-enable this form was to specify a snippet method and to add the parameter form=POST. For lift to process a form, it must specify either form=POST or form=GET depending on the submit method to be used.
Here is the Helloworld class that contains the login snippet method.
package edu.emory.cci.caGrid.liftClient.snippet import scala.xml.{NodeSeq, Text} import net.liftweb.util._ import net.liftweb.common._ import net.liftweb.http._ import code.lib._ import Helpers._ import org.joda.time.Period import edu.emory.cci.caGrid.liftClient.model.CaGrid class Helloworld extends Logger {
The listing for the Helloworld class begins with the usual imports for a Lift snippet class. We also import the Period class form the joda-time library.
Joda-time is a library for manipulating and reasoning about time. We use its Period class to represent the length of time requested for the lifetime of the user certificate that is obtained by the login operation.
/** * This request var allows the userName to be remembered from one request to * another, so multiple login tries do not require reentering the same user * name. */ object userName extends RequestVar("") /** * Allow number of hours in lifetime to be remembered from one request to * another. */ object lifetimeHours extends RequestVar("12") /** * Allow number of minutes in lifetime to be remembered from one request to * another. */ object lifetimeMinutes extends RequestVar("0")
The HelloWorld class defines three RequestVar objects. The RequestVar class is provided by the List framework to allow values to be remembered from one request to another without navigating to another page. These are used with the login form, so that if the login fails, the user name, and lifetime hours and minutes will be remembered and not need to be re-entered.
/** * snippet for managing the login form. */ def login = { var pswd = "" def processLogin() { /* The body of this nested method is shown in a listing further down on this page. */ } "@user" #> SHtml.text(userName.is, userName(_)) & "@password" #> SHtml.password( pswd, pswd = _ ) & "@lifehours" #> SHtml.text(lifetimeHours.is, lifetimeHours(_)) & "@lifeminutes" #> SHtml.text(lifetimeMinutes.is, lifetimeMinutes(_)) & "type=submit" #> SHtml.submit("Login", processLogin) } }
The last member of the HelloWorld class is a snippet method named login. The login method contains a nested method named processLogin. The purpose of the processLogin method is to process the information provided through the login form. Notice that the body of the processLogin method has been omitted from the above listing. We will discuss the body of the processLogin method later.
The login method constructs an object that guides the Lift framework in the replacement of the form's field and submit button elements with customized HTML that is computed by Lift. The strings "@user", "@password, "@lifehours" and "@lifeminutes" match elements whose name attribute has the corresponding value. Those also happen to be the names of input elements in the HTML template that are rendered as editable fields. The string "type=submit" matches the input element that is rendered as a submit button, because it has a type attribute whose value is submit.
The HTML used to replace the matched elements is computed using methods of the List-supplied object SHtml. The SHtml object has methods used to compute, in a context-aware way, HTML appropriate to render something, add behavior or both.
The SHtml.text method returns an HTML element suitable for rendering a text field. It signature is
text (value: String, func: (String) ? Any, attrs: ElemAttr*) => scala.xml.Elem
where
- value is the initial value that will be in the rendered text field.
- func is a method to be called when the form is submitted. The value of the text field is passed to the method with the expectation that the method will put the given value some place it can be seen by another method that will be called later to process the contents of the form as a whole.
- attrs is zero or more attributes that are to be explicitly set in the returned HTML element.
The replacement specified as
"@user" #> SHtml.text(userName.is, userName(_))
specifies that the HTML element whose name is user is to be replaced with an element that will render a text field. The replacement text field will have an initial value specified by userName.is, which is the current value of the userName RequestVar. When the client posts the form, the value of the text field is to be passed to an anonymous method that passes the value to the userName object's apply method, which has the effect of setting the value of the RequestVar to the passed-in value.
The replacement specified as
"@password" #> SHtml.password( pswd, pswd = _ )
is similar to the one for user. It calls SHtml.password, which returns an HTML element suitable for rendering a text field that does not echo what you type into it. The initial value for the password text field is kept in the local variable pswd. Because the value of a local variable is not remembered from one method call to the next, if a login attempt fails then what was last typed into the password field is forgotten.
The replacement specified as
"type=submit" #> SHtml.submit("Login", processLogin)
replaces the place-holder submit button with an HTML element that is rendered as a submit button and triggers the form's post-submit processing. This calls the methods associated with each of the form's inputs, including the processLogin method that is passed to SHtml.submit.
When the processLogin method is called, the anonymous methods associated with the other form inputs have already been called and captured the values supplied in the form's fields. Here are the details of the processLogin method:
def processLogin() {
var errorCount = 0
userName(userName.is.trim)
if (userName.is.toString.length == 0) {
errorCount += 1
S.error("userName", "User name may not be blank");
}
try {
if (lifetimeHours.is.toInt <= 0) {
errorCount += 1
S.error("lifehours", "Lifetime hours must be greater than zero")
}
} catch {
case ex: NumberFormatException => {
errorCount += 1
S.error("lifehours", "Lifetime hours must be a valid number")
}
}
try {
if (lifetimeMinutes.is.toInt < 0) {
errorCount += 1
S.error("lifeminutes", "Lifetime minutes may not be less than zero")
}
if (lifetimeMinutes.is.toInt > 59) {
errorCount += 1
S.error("lifeminutes", "Lifetime minutes may not be greater than 59")
}
} catch {
case ex: NumberFormatException => {
errorCount += 1
S.error("lifeminutes", "Lifetime minutes must be a valid number")
}
}
The processLogin method begins with verifying that the supplied values are acceptable. If there are any errors to report, the name of the relevant field and the error message are passed to S.error. The messages passed to S.error are accumulated and displayed as part of the HTTP response.
| The S object The S object encapsulates the state of the current HTTP request and response. In addition to the error method which is used to display messages about serious problems in the user interface, there is also a warning method for less serious issues and an notice method for informational messages. |
After the input values have been checked, the processLogin method continues like this:
if (errorCount > 0) {
S.error(errorCount + " errors found")
} else {
If any problems were found with the inputs from the form, errorCount will be greater than zero and an additional error message will be posted announcing how many errors were found. Notice that this call to S.error does not include the name of a field.
With errorCount greater than zero, no further actions are performed by the processLogin method. This means that the login page will remain the current page and will be redisplayed by the HTTP response that is sent after processLogin returns. Here is what the left side of the login page looks like when it is redisplayed with errors.

The processLogin method continues by setting up and making a method call to get grid credentials for the user:
val lifetimePeriod = new Period()
lifetimePeriod.withHours(lifetimeHours.is.toInt)
lifetimePeriod.withMinutes(lifetimeMinutes.is.toInt)
val credentials = CaGrid.credentials.login(userName.is, pswd, lifetimePeriod)
A Period (from the joda-time library) object is created that contains the desired lifetime for the requested user credentials. The specified user name, password and lifetime are passed into the login method of the CredentialProxy object provided by the CaGrid object. We will look at the details of the CredentialProxy class later.
The return type of CredentialProxy.login is Box[support:GlobusCredential]. The GlobusCredential class encapsulates the user certificate that is returned by the login operation.
The Box class is provided and heavily used by the Life framework. It is similar in purpose to the Option class provided by the standard Scala library, but allows a richer set of possibilities to be represented by a single object.
The value of something whose type is Option[support:Any] can be an instance of Some[support:Any] to indicate the presence of an Any value or None to indicate the absence of a value. The Option class does not provide a way to indicate the absence of a value due to an error. Lift's Box class solves this problem.
The value of something whose type is Box[support:Any] can be an instance of Full[support:Any] to indicate the presence of an Any value, it can be Empty to indicate the absence of an Any value or it can be an instance of
Failure(String, Box[support:Throwable], Box[support:Failure]). If the first Box in a Failure is Full, it contains an underlying exception. If the second Box in a Failure is Full, it contains an underlying failure.
Here is the last part of processLogin:
credentials match {
case Full(_) => {
S.notice( "Login succeeded for user: " + userName.is )
S.redirectTo("/home");
}
case Failure(msg, excp, _) => S.error(msg)
}
}
}
The Failure case is handled by sending the message in the Failure object to the user. Nothing else is done, so the current page remains the login page.
For the Full case, we send a notice to the user that the login was successful. We then call S.redirectTo to navigate to the caGrid client's home page. We don't bother accessing the contents of the returned Box because we can get the GlobusCredentials object from the CaGrid.credentials later.
We complete this discussion of login with the details of the CaGrid class:
package edu.emory.cci.caGrid.liftClient.model import org.joda.time._ import gov.nih.nci.cagrid.opensaml.SAMLAssertion import net.liftweb.common._ import org.cagrid.gaards.authentication.BasicAuthentication import org.cagrid.gaards.authentication.client.AuthenticationClient import org.cagrid.gaards.dorian.client.GridUserClient import org.cagrid.gaards.dorian.federation.CertificateLifetime import org.globus.gsi.GlobusCredential /** * This class contains and is used to manipulate a user's credentials. */ class CredentialProxy extends Logger {
The imports for the CredentialProxy class include org.joda.time. to allow the requested lifetime for the user certificate to be passed in as a single object. They also include net.liftweb.common. so that CredentialProxy can work with Box and its subclasses.
The remaining imports are needed to work with caGrid.
private var myUserPassword:Option[UserPassword] = None private var myCredential:Box[GlobusCredential] = Empty private def userPassword = myUserPassword /** The GlobusCredential from the last call to login, unless logout has since been called */ def credential:Box[GlobusCredential] = myCredential
The CredentialProxy class has two instance variables:
- myUserPassword can contain an object that encapsulates the user name and password used to obtain a user certificate.
- myCredential can contain an object the encapsulates the user certificate obtained by the login method.
Both of these variables are private. Clients of the CredentialProxy class can access the values of myUserPassword and myCredential by calling the userPassword or credential methods, respectively.
The values for myUserPassword and myCredential are set indirectly by calling the login method. Here is the login method:
/** * Use the given username and password to log in the user with a credential * whose requested lifetime will be the given duration. * * @return a box the is either Full if the login failed or a Failure that * contains an explanation of the problem. */ def login(username:String, password:String, credentialLifetime:Period):Box[GlobusCredential] = { myCredential = try { myUserPassword = Some(UserPassword(username, password)) // Create credential val auth = new BasicAuthentication() auth.setUserId(username) auth.setPassword(password) //Authenticate to the IdP (DorianIdP) using credential val authClient = new AuthenticationClient(CaGrid.dorianUrl); val saml = authClient.authenticate(auth) val lifetime = new CertificateLifetime() lifetime.setHours(credentialLifetime.getHours()) lifetime.setMinutes(credentialLifetime.getMinutes()) lifetime.setSeconds(credentialLifetime.getSeconds()) val dorian = new GridUserClient(CaGrid.dorianUrl) Box( dorian.requestUserCertificate(saml, lifetime)) } catch { case e:Exception => { val msg = "Login failed" error(msg, e) Failure(msg, Full(e), Empty) } } myCredential }
The login method makes two calls to caGrid's Dorian service. First it passes the user name and password to Dorian's authenticate method. This is part of Dorian's authentication service. If the authenticate method recognizes the user name and password as being a valid combination that identifies a known user, it returns a SAML assertion. If the user name and password are invalid, the authenticate method throws an exception.
After receiving a SAML assertion, the login method passes the SAML assertion and the requested certificate lifetime to Dorian's requestUserCertificate method. The requestUserCertificate method returns a GlobusCredential object that encapsulates the user certificate issued by Dorian.
The rest of the methods in the CredentialProxy class are for accessing or manipulating the user certificate:
/** * Return true if myCredential contains a globus credential that has time * left in its lifetime. */ def hasValidUserCertificate:Boolean = { if (myCredential.isDefined) { debug("credential time left=" + myCredential.openTheBox.getTimeLeft()) } myCredential.isDefined /* && myCredential.openTheBox.getTimeLeft() > 0 */ } /** * Discard the credential */ def logout() { myCredential = Empty } def gridIdentity:Box[String] = { myCredential match { case Full(credential) => Box(credential.getIdentity()) case _ => Empty } } }
Just to for completeness, here is the UserPassword class:
package edu.emory.cci.caGrid.liftClient.model case class UserPassword (userName:String, password:String) {}
The Home Page
Once you are logged in, you will see the home page. It looks like this:

For phase one the functionality of the home page is very limited. It only allows you to log out.
The menu does include a "Service Selection" item, but that is not yet functional.
Here is the HTML that the home page is based on:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<title>Home</title>
</head>
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
<span class="lift:SiteInfo">
<h2>CaGrid Web Client for <span id="targetGridName">the name of the target grid goes here.</span></h2>
<div id="time">Time goes here</div>
</span>
<p>
<span class="lift:UserStatus">
You are logged in with grid identity: <span id="gridIdentity"/>
</span>
</p>
</div>
</body>
</html>
Like the HTML for the login page, it embeds itself in a standard page template by using lift:surround. It also uses lift:SiteInfo to display the name of the client's configured target grid and the current time.
The UserStatus.render method is a snippet method used to display the logged in grid identity. Here is a listing of UserStatus.
package edu.emory.cci.caGrid.liftClient.snippet import scala.xml.{NodeSeq, Text} import net.liftweb.util._ import net.liftweb.common._ import net.liftweb.http._ import java.util.Date import code.lib._ import Helpers._ import edu.emory.cci.caGrid.liftClient.model.CaGrid class UserStatus extends Logger { /** * Snippet for showing information about someone being logged in. */ def render = { "#gridIdentity" #> CaGrid.credentials.gridIdentity } }
All that this snippet does is to replace the element whose id is gridIdentity with the actual grid identity returned by support:CaGrid.credentials.gridIdentity. Note that the gridIdentity method returns a Box[support:String] and that the lift framework is smart enough to get the String out of the Box.
Logging Out
Clicking on the Logout link in the menu takes us to the Logout page. Here is what the Logout page looks like:
Here is the HTML for the logout page:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<title>Logout</title>
</head>
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
<span class="lift:SiteInfo">
<h2>CaGrid Web Client for <span id="targetGridName">the name of the target grid goes here.</span></h2>
<div id="time">Time goes here</div>
</span>
<p>
<form class="lift:Logout?form=POST">
<table >
<tbody>
<tr>
<td style="background-color: white;">Click on the button below to log out. Click a menu item to do something else.</td>
</tr>
<tr>
<td style="background-color: white;" colspan="2" align="center"><input type="submit" value="Log Out"></input></td>
</tr>
</tbody>
</table>
</form>
</p>
</div>
</body>
</html>
The main thing to notice about this HTML is its similarity to what we have seen before. The only thing new here is the Logout snippet class.
Here is the Logout class:
package edu.emory.cci.caGrid.liftClient.snippet import scala.xml.{NodeSeq, Text} import net.liftweb.util._ import net.liftweb.common._ import net.liftweb.http._ import java.util.Date import code.lib._ import Helpers._ import edu.emory.cci.caGrid.liftClient.model.CaGrid class Logout extends Logger { def render = { def processLogout() { CaGrid.credentials.logout() S.redirectTo("/index") } "type=submit" #> SHtml.submit("Login", processLogout) } }
The organization of this snippet method is similar to the previous snipped we saw for a form. It defines a nested method. In this case, the nested method is named processLogout. It then constructs an object that arranges for the HTML for then simple submit button to replaced with HTML for a submit button that will call the processLogout button.
Registering New Users
The one last function that is in phase one of the caGrid client is registering a new user. This is accessible from the login page. The page to request a new User id looks like this:
The form on this page has more fields than the other forms we have looked at. Here is what the underlying HTML file request_uid.html looks like:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<title>Logout</title>
</head>
<body class="lift:content_id=main">
<div id="main" class="lift:surround?with=default;at=content">
<span class="lift:SiteInfo">
<h2>Request a New User ID for <span id="targetGridName"></span></h2>
</span>
<p>
<span class="lift:RegisterUser">Generated form goes here</span>
</p>
</div>
</body>
</html>
The interesting thing about this file is that the HTML does not contain the form. Instead, it invokes a snippet that uses the Lift framework to generate the form. This particular snippet looks very different than what we have seen previously. Here is the abridged source for the RegisterUser class that contains the snippet method:
package edu.emory.cci.caGrid.liftClient.snippet import scala.xml.{NodeSeq, Text} import net.liftweb.util._ import net.liftweb.common._ import net.liftweb.http._ import net.liftweb.proto.ProtoRules import java.util.Date import code.lib._ import Helpers._ import edu.emory.cci.caGrid.liftClient.model.CaGrid import org.cagrid.gaards.dorian.idp.Application import org.cagrid.gaards.dorian.idp.CountryCode import org.cagrid.gaards.dorian.idp.StateCode /** * Snippet for generating new user registration form. */ object RegisterUser extends Logger with LiftScreen {
The first set of imports are those needed for the Lift part of the LiftScreen class. The next import is of the CaGrid class that contains the method that we will use to request the new user ID once all of the needed information has been obtained through the UI. The third set of imports is to allow us to organize the parameters for requesting a user ID into an object that the CaGrid class can work with.
The most important thing to notice is that the RegisterUser class inherits the LiftScreen trait. The LiftScreen trait provides the actual snippet method that generates the HTML for the form. The LiftScreen trait determines what to generate by introspecting on the class it is part of. It looks for public val members that are an instance of Field. It looks for a method named finish that is called to process the contents of the form after it is submitted.
/** * Function for the most common sort of field we will have. */ private def requiredStringField(name:String) = { field(name, "", trim, valMinLen(1, name + " may not be blank"), valMaxLen(255, name + " may not be longer than 255 characters")) }
Most of the fields in the form to be generated for requesting a user ID share some common attributes:
- They should initially be blank.
- Any leading or trailing blanks in their value should be discarded.
- They are required fields and so should not be blank.
- They should not have excessively long values.
The requiredStringField method is a convenient way to construct field objects that describe fields with a given name and these attributes.
private def emailPattern = ProtoRules.emailRegexPattern.vend private def validEmailAddr_?(email: String): Boolean = emailPattern.matcher(email).matches /** * Validation function for e-mail addresses. */ private def valEMail(msg: => String): String => List[FieldError] = s => s match { case str if (null ne str) && validEmailAddr_?(str) => Nil case _ => List(FieldError(currentField.box openOr new FieldIdentifier {}, Text(msg))) }
We want to validate the e-mail address that is supplied to the form, if any. We use a regular expression to determine the validity of e-mail addresses. This is an example of how to construct your own server-side field validation methods.
val userId = requiredStringField("User ID") val pswd = password("Password", "", trim, valMinLen(8, "Password is too short"), valMaxLen(30, "Password may not be longer than 30 characters")) val email = field("E-Mail", "", trim, valEMail("E-Mail address is not valid"), valMaxLen(255, "E-Mail address may not be longer than 255 characters")) val firstName = requiredStringField("First Name") val lastName = requiredStringField("Last Name") val address = requiredStringField("Address Line 1") val address2 = field("Address Line 2", "", trim, valMaxLen(255, "Address Line 2 address may not be longer than 255 characters")) val city = requiredStringField("City")
The above portion of the code shows the definition of some fields. The majority of these are constructed by a call to requiredStringField. The password method constructs an object that describes a password field.
The field method is what we use to construct most Field objects. However there are some fields that cannot be constructed this way and are instead created by defining and instantiating a subclass of Field. The definitions in the following piece of code describes a drop-down list of two-letter state code and a list of countries.
val state = new Field { private val stateList = (StateCode.Outside_US, "Outside US") :: (StateCode.AL, "AL") :: (StateCode.AK, "AK") :: ? (StateCode.WV, "WV") :: (StateCode.WI, "WI") :: (StateCode.WY, "WY") :: Nil; type ValueType = StateCode override def name = "State" override implicit def manifest = buildIt[StateCode] override def default = StateCode.CA override def toForm: Box[NodeSeq] = SHtml.selectObj(stateList, Empty, set _) } val country = new Field { private val countryNameAndCodeList = (CountryCode.AF, "AFGHANISTAN") :: (CountryCode.AL, "ALBANIA") :: (CountryCode.DZ, "ALGERIA") :: ? (CountryCode.YE, "YEMEN") :: (CountryCode.ZM, "ZAMBIA") :: (CountryCode.ZW, "ZIMBABWE") :: Nil; type ValueType = CountryCode override def name = "Country" override implicit def manifest = buildIt[CountryCode] override def default = CountryCode.US override def toForm: Box[NodeSeq] = SHtml.selectObj(countryNameAndCodeList, Empty, set _) } val zipcode = requiredStringField("Zip Code") val phoneNumber = requiredStringField("Phone Number") val organization = requiredStringField("Organization")
The final piece of code in the RegisterUser class is the finish method, which is called after the form's submit button has been pressed.
def finish() {
// Create a registration application
val userApplication = new Application()
// Get values from UI
userApplication.setUserId( userId.is )
userApplication.setPassword( pswd.is )
userApplication.setEmail( email.is )
userApplication.setFirstName( firstName.is )
userApplication.setLastName( lastName.is )
userApplication.setAddress( address.is)
userApplication.setAddress2( address2.is )
userApplication.setCity( city.is )
userApplication.setState( state.is )
userApplication.setCountry( country.is )
userApplication.setZipcode( zipcode.is )
userApplication.setPhoneNumber( phoneNumber.is )
userApplication.setOrganization( organization.is )
val msg = CaGrid.registerLocalUser(userApplication)
S.notice(msg)
}
}
The finish method creates an Application object, populates it with information from the form's fields and then passes the application object to the CaGrid.registerLocalUser method. The CaGrid.registerLocalUser method returns a string that describes the outcome of the request for registration. This string is passed to S.notice to be displayed as a status message on the current page.






