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.
This tutorial assumes that you have some experience developing web applications. Some basic knowledge of the Scala programming language is assumed.
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
In Windows, run the command
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:
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:
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!
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.
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,
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:
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 to contain information about the configuration of the installation. The location of this installation configuration directory is the value of InstallerDirectory.
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.
A Properties object that contains the property-value pairs from the file identified by PropertiesFile is the value of the InstallerProperties member:
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.
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:
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.
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:
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
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:
See http://ant.apache.org/ivy for more detailed information.
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:
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:
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
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.
The Lift framework initializes an application by instantiating a class named Boot The Boot class begins like this.
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.
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.
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.
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
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.
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.
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.
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.
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 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 value of targetGridParentDirectory identifies the directory that contains configuration information for all known target grids.
The value of targetGridDirectory identifies directory that contains configuration information for the currently configured target grid.
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 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 value of cadsrUrl is the URL sring for the currently configured target grid's CADSR service.
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 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 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 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 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 value of syncDescriptionFile identifies the file that contains configuration information that is needed to synchronize with the trust fabric.
The value of dotGlobusDirectory identifies the .globus directory that is the root for globus configuration information.
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.
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.
Uses for the above infrastructure are described under the following section headings.
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.
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:
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:
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.
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.
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.
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.
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.
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:
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.
Enabling HTML forms to work with lift is rather simple. Here is the login form from index.html:
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.
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.
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.
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
- 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
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
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
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:
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 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:
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:
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:
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.
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:
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.
Just to for completeness, here is the UserPassword class:
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:
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.
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.
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:
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:
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.
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:
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:
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.
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.
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.
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.
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.
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.