From d065598296ffe95632dd1c550ff41a9d055b8d52 Mon Sep 17 00:00:00 2001 From: David Pollak Date: Tue, 12 Feb 2013 10:16:56 -0800 Subject: [PATCH 001/115] First step to a revised 3.0 --- build.sbt | 6 +- liftsh | 2 +- .../scala/net/liftweb/jpa/RequestVarEM.scala | 65 --- persistence/ldap/.gitignore | 1 - persistence/ldap/README | 31 -- .../net/liftweb/ldap/LDAPProtoUser.scala | 203 --------- .../scala/net/liftweb/ldap/LdapVendor.scala | 408 ------------------ .../ldap/src/test/resources/ldap.properties | 5 - .../ldap/src/test/resources/logback-test.xml | 13 - .../scala/net/liftweb/ldap/LdapSpec.scala | 177 -------- project/Build.scala | 22 +- project/Dependencies.scala | 2 +- project/Developers.scala | 2 +- .../main/scala/net/liftweb/http}/Wizard.scala | 30 +- .../scala/net/liftweb/http}/WizardSpec.scala | 0 15 files changed, 24 insertions(+), 943 deletions(-) delete mode 100644 persistence/jpa/src/main/scala/net/liftweb/jpa/RequestVarEM.scala delete mode 100644 persistence/ldap/.gitignore delete mode 100644 persistence/ldap/README delete mode 100644 persistence/ldap/src/main/scala/net/liftweb/ldap/LDAPProtoUser.scala delete mode 100644 persistence/ldap/src/main/scala/net/liftweb/ldap/LdapVendor.scala delete mode 100644 persistence/ldap/src/test/resources/ldap.properties delete mode 100644 persistence/ldap/src/test/resources/logback-test.xml delete mode 100644 persistence/ldap/src/test/scala/net/liftweb/ldap/LdapSpec.scala rename web/{wizard/src/main/scala/net/liftweb/wizard => webkit/src/main/scala/net/liftweb/http}/Wizard.scala (95%) rename web/{wizard/src/test/scala/net/liftweb/wizard => webkit/src/test/scala/net/liftweb/http}/WizardSpec.scala (100%) diff --git a/build.sbt b/build.sbt index 204a396651..4af4c58ba5 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import Dependencies._ organization in ThisBuild := "net.liftweb" -version in ThisBuild := "2.5-SNAPSHOT" +version in ThisBuild := "3.0-SNAPSHOT" homepage in ThisBuild := Some(url("http://www.liftweb.net")) @@ -12,7 +12,9 @@ startYear in ThisBuild := Some(2006) organizationName in ThisBuild := "WorldWide Conferencing, LLC" -crossScalaVersions in ThisBuild := Seq("2.10.0", "2.9.2", "2.9.1-1", "2.9.1") +scalaVersion := "2.10.0" + +crossScalaVersions in ThisBuild := Seq("2.10.0") libraryDependencies in ThisBuild <++= scalaVersion {sv => Seq(specs2(sv), scalacheck) } diff --git a/liftsh b/liftsh index 0db10227b4..91995c5fcc 100755 --- a/liftsh +++ b/liftsh @@ -6,7 +6,7 @@ if test -f ~/.liftsh.config; then fi # Internal options, always specified -INTERNAL_OPTS="-Dfile.encoding=UTF-8 -Xmx768m -noverify -XX:ReservedCodeCacheSize=96m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=512m" +INTERNAL_OPTS="-Dfile.encoding=UTF-8 -Xmx1768m -noverify -XX:ReservedCodeCacheSize=96m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=512m" # Add 64bit specific option exec java -version 2>&1 | grep -q "64-Bit" && INTERNAL_OPTS="${INTERNAL_OPTS} -XX:+UseCompressedOops -XX:ReservedCodeCacheSize=128m" diff --git a/persistence/jpa/src/main/scala/net/liftweb/jpa/RequestVarEM.scala b/persistence/jpa/src/main/scala/net/liftweb/jpa/RequestVarEM.scala deleted file mode 100644 index cdd71f719e..0000000000 --- a/persistence/jpa/src/main/scala/net/liftweb/jpa/RequestVarEM.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2006-2011 WorldWide Conferencing, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.liftweb -package jpa - -import javax.persistence.EntityManager - -import net.liftweb.http.TransientRequestVar - -import org.scala_libs.jpa.{ScalaEMFactory, ScalaEntityManager} - -/** - * This trait provides specific functionality for the Lift web framework - * by using a Lift RequestVar to hold the underlying EM. This - * allows you to use a singleton for EM access. You must mix in some - * other class to provide the actual ScalaEMFactory functionality. - * Example usage would be: - * - *

- * - * object Model extends LocalEMF("test") with RequestVarEM - * - *

- * - * @author Derek Chen-Becker - */ -trait RequestVarEM extends ScalaEntityManager with ScalaEMFactory { - /** - * Provides the request var that holds the underlying EntityManager - * for each request. - */ - object emVar extends TransientRequestVar[EntityManager](openEM()) { - this.registerGlobalCleanupFunc(ignore => closeEM(this.is)) - - override def __nameSalt = net.liftweb.util.Helpers.randomString(10) - } - - // Must be provided to properly implement ScalaEntityManager - protected def em = emVar.is - val factory = this - - /** - * Returns the current underlying EntityManager. Generally - * you shouldn't need to do this unless you're using some very - * advanced or propietary functionality on the EM. - * - * @return The underlying EM - */ - def getUnderlying : EntityManager = em -} - diff --git a/persistence/ldap/.gitignore b/persistence/ldap/.gitignore deleted file mode 100644 index f78c92ab69..0000000000 --- a/persistence/ldap/.gitignore +++ /dev/null @@ -1 +0,0 @@ -server-work/ diff --git a/persistence/ldap/README b/persistence/ldap/README deleted file mode 100644 index d1efa240c5..0000000000 --- a/persistence/ldap/README +++ /dev/null @@ -1,31 +0,0 @@ -This module provides a LDAPVendor class to perform search and bind operations against a LDAP Server, -and a base class to authentificate LDAP users (using the LDAPVendor) - -1: SimpleLDAPVendor -SimpleLDAPVendor extends LDAPVendor class and provides a simple and functional LDAPVendor, -that only needs to provide a parameters var to define the LDAP Server properties . - -SimpleLDAPVendor.parameters = () => Map("ldap.url" -> "ldap://localhost", - "ldap.base" -> "dc=company,dc=com", - "ldap.userName" -> "cn=query,dc=company,dc=com", - "ldap.password" -> "password") - -or - -SimpleLDAPVendor.parameters = () => SimpleLDAPVendor.parametersFromFile("/some/directory/ldap.properties") - -2: LDAPProtoUser -Base class of LDAP users - - We can define : - - - loginErrorMessage = Message displayed when user auth failed, default = "Unable to login with : %s" - - ldapUserSearch = LDAP search sentence to search user object using login and password, default = (uid=%s) - - rolesSearchFilter = LDAP search filter to get the user roles, default value = (&(objectClass=groupOfNames)(member=%s)) - - rolesNameRegex = Regular expression to get the role name from his dn (maybe we should get object cn attribute or something ?) - - - - We can override setRoles function if we want to define roles search manually - - - diff --git a/persistence/ldap/src/main/scala/net/liftweb/ldap/LDAPProtoUser.scala b/persistence/ldap/src/main/scala/net/liftweb/ldap/LDAPProtoUser.scala deleted file mode 100644 index dbb9a5f3e1..0000000000 --- a/persistence/ldap/src/main/scala/net/liftweb/ldap/LDAPProtoUser.scala +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2010-2011 WorldWide Conferencing, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.liftweb -package ldap - -import javax.naming.directory.{Attributes} -import scala.util.matching.{Regex} -import scala.xml.{Elem, NodeSeq} -import net.liftweb.http.{LiftResponse, RedirectResponse, S, SessionVar} -import net.liftweb.http.js.{JsCmds} -import net.liftweb.mapper.{BaseOwnedMappedField, - MappedString, - MetaMegaProtoUser, - MegaProtoUser} -import net.liftweb.sitemap.{Menu} -import net.liftweb.util.{Helpers} -import net.liftweb.common.{Box, Empty, Full} - -import Helpers._ - -import scala.util.matching.{Regex} -import scala.xml.{Elem, NodeSeq} - -trait MetaLDAPProtoUser[ModelType <: LDAPProtoUser[ModelType]] extends MetaMegaProtoUser[ModelType] { - self: ModelType => - - override def signupFields: List[FieldPointerType] = uid :: - cn :: dn :: Nil - - override def fieldOrder: List[FieldPointerType] = uid :: - cn :: dn :: Nil - - /** - * The menu item for creating the user/sign up (make this "Empty" to disable) - */ - override def createUserMenuLoc: Box[Menu] = Empty - - /** - * The menu item for lost password (make this "Empty" to disable) - */ - override def lostPasswordMenuLoc: Box[Menu] = Empty - - /** - * The menu item for resetting the password (make this "Empty" to disable) - */ - override def resetPasswordMenuLoc: Box[Menu] = Empty - - /** - * The menu item for changing password (make this "Empty" to disable) - */ - override def changePasswordMenuLoc: Box[Menu] = Empty - - /** - * The menu item for validating a user (make this "Empty" to disable) - */ - override def validateUserMenuLoc: Box[Menu] = Empty - - override def editUserMenuLoc: Box[Menu] = Empty - - /** - * User search sentence - */ - def ldapUserSearch: String = "(uid=%s)" - - /** - * Error messages - */ - def loginErrorMessage: String = "Unable to login with : %s" - - def commonNameAttributeName = "cn" - def uidAttributeName = "uid" - - override def loginXhtml : Elem = { -
- - - - - - - - - - - - - -
{S.?("log.in")}
Username
Password
 
-
- } - - def ldapVendor: LDAPVendor = new LDAPVendor - - override def login : NodeSeq = { - if (S.post_?) { - if (!ldapLogin(S.param("username").openOr(""), - S.param("password").openOr(""))) - S.error(loginErrorMessage.format(S.param("username").openOr(""))) - } - - Helpers.bind("user", loginXhtml, - "name" -> (JsCmds.FocusOnLoad()), - "password" -> (), - "submit" -> ()) - } - - def ldapLogin(username: String, password: String): Boolean = { - def _getUserAttributes(dn: String) = ldapVendor.attributesFromDn(dn) - - val users = ldapVendor.search(ldapUserSearch.format(username)) - - if (users.size >= 1) { - val userDn = users(0) - if (ldapVendor.bindUser(userDn, password)) { - val completeDn = userDn + "," + ldapVendor.ldapBaseDn.vend //configure().get("ldap.base").getOrElse("") - logUserIn(this) - - bindAttributes(_getUserAttributes(completeDn)) - - setRoles(completeDn, ldapVendor) - S.redirectTo(homePage) - } - else return false - } - else return false - - return true - } - - def bindAttributes(attrs: Attributes) = { - for { - theCn <- Box !! attrs.get(commonNameAttributeName).get - theUid <- Box !! attrs.get(uidAttributeName).get - } - { - cn(theCn.toString) - uid(theUid.toString) - } - } -} - -trait LDAPProtoUser[T <: LDAPProtoUser[T]] extends MegaProtoUser[T] { - self: T => - /** - * User Roles LDAP search filter - */ - def rolesSearchFilter: String = "(&(objectclass=groupofnames)(member=%s))" - - /** - * Regular expression to get user roles names - */ - def rolesNameRegex = ".*cn=(.[^,]*),ou=.*" - - object ldapRoles extends SessionVar[List[String]](List()) - - override def getSingleton: MetaLDAPProtoUser[T] - - object uid extends MappedString(this, 64) { - override def dbIndexed_? = true - } - - object dn extends MappedString(this, 64) { - override def dbIndexed_? = true - } - - object cn extends MappedString(this, 64) { - override def dbIndexed_? = true - } - - def getRoles: List[String] = { - return ldapRoles.get - } - - def setRoles(userDn: String, ldapVendor: LDAPVendor) { - def getGroupNameFromDn(dn: String): String = { - val regex = new Regex(rolesNameRegex) - - val regex(groupName) = dn - return groupName - } - - // Search for user roles - val filter = rolesSearchFilter.format(userDn) - - val groups = ldapVendor.search(filter) - groups foreach { g => ldapRoles.set(ldapRoles.get :+ getGroupNameFromDn(g)) } - } -} - diff --git a/persistence/ldap/src/main/scala/net/liftweb/ldap/LdapVendor.scala b/persistence/ldap/src/main/scala/net/liftweb/ldap/LdapVendor.scala deleted file mode 100644 index b205d90a4b..0000000000 --- a/persistence/ldap/src/main/scala/net/liftweb/ldap/LdapVendor.scala +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright 2010-2011 WorldWide Conferencing, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.liftweb -package ldap - -import java.io.{InputStream, FileInputStream} -import java.util.{Hashtable, Properties} - -import javax.naming.{AuthenticationException,CommunicationException,Context,NamingException} -import javax.naming.directory.{Attributes, BasicAttributes, SearchControls} -import javax.naming.ldap.{InitialLdapContext,LdapName} - -import scala.collection.mutable.ListBuffer -import scala.collection.JavaConversions._ - -import util.{ControlHelpers,Props,SimpleInjector,ThreadGlobal} -import common._ - -/** - * This class provides functionality to allow us to search and - * bind (authenticate) a username from a ldap server. - * - * To configure the LDAP Vendor parameters, use one of the configure - * methods to provide a Map of string parameters. - * - * The primary parameters (with defaults) are: - * - * - * Optionally, you can set the following parameters to control context testing - * and reconnect attempts: - * - * - * - * In addition to configuration via a Map or Properties file, fine-grained control - * over behaviors can be specified via Inject values corresponding to each - * of the properties. - * - * To use LDAPVendor, you can simply create an object extending it - * and configure: - * - *
- * object myLdap extends LDAPVendor
- * myLdap.configure()
- * 
- * - */ -class LDAPVendor extends Loggable with SimpleInjector { - // =========== Constants =============== - final val KEY_URL = "ldap.url" - final val KEY_BASE_DN = "ldap.base" - final val KEY_USER = "ldap.userName" - final val KEY_PASSWORD = "ldap.password" - final val KEY_AUTHTYPE = "ldap.authType" - final val KEY_FACTORY = "ldap.initial_context_factory" - final val KEY_LOOKUP = "lift-ldap.testLookup" - final val KEY_RETRY_INTERVAL = "lift-ldap.retryInterval" - final val KEY_MAX_RETRIES = "lift-ldap.maxRetries" - - final val DEFAULT_URL = "ldap://localhost" - final val DEFAULT_BASE_DN = "" - final val DEFAULT_USER = "" - final val DEFAULT_PASSWORD = "" - final val DEFAULT_AUTHTYPE = "simple" - final val DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory" - final val DEFAULT_LOOKUP = Empty - final val DEFAULT_RETRY_INTERVAL = 5000 - final val DEFAULT_MAX_RETRIES = 6 - - - - /** - * Configure straight from the Props object. This allows - * you to use Lift's run modes for different LDAP configuration. - */ - def configure() { - configure(Props.props) - } - - /** - * Configure from the given file. The file is expected - * to be in a format parseable by java.util.Properties - */ - def configure(filename : String) { - val stream = new FileInputStream(filename) - configure(stream) - stream.close() - } - - /** - * Configure from the given input stream. The stream is expected - * to be in a format parseable by java.util.Properties - */ - def configure(stream : InputStream) { - val p = new Properties() - p.load(stream) - - configure(propertiesToMap(p)) - } - - /** - * Configure from the given Map[String,String] - */ - def configure(props : Map[String,String]) { - internal_config = processConfig(props) - } - - /** - * This controls the URL used to connect to the LDAP - * server - */ - val ldapUrl = new Inject[String](DEFAULT_URL){} - - /** - * This controls the base DN used for searcheds - */ - val ldapBaseDn = new Inject[String](DEFAULT_BASE_DN){} - - /** - * This controls the username used to bind for - * searches (not authentication) - */ - val ldapUser = new Inject[String](DEFAULT_USER){} - - /** - * This controls the password used to bind for - * searches (not authentication) - */ - val ldapPassword = new Inject[String](DEFAULT_PASSWORD){} - - /** - * This controls the type of authentication to - * use. - */ - val ldapAuthType = new Inject[String](DEFAULT_AUTHTYPE){} - - /** - * This controls the factory used to obtain an - * InitialContext - */ - val ldapFactory = new Inject[String](DEFAULT_FACTORY){} - - /** - * This can be set to test the InitialContext on each LDAP - * operation. It should be set to a search DN. - */ - val testLookup = new Inject[Box[String]](Empty){} - - /** - * This sets the interval between connection attempts - * on the InitialContext. The default is 5 seconds - */ - val retryInterval = new Inject[Long](5000){} - - /** - * This sets the maximum number of connection - * attempts before giving up. The default is 6 - */ - val retryMaxCount = new Inject[Int](6){} - - /** - * This sets the Directory SearchControls instance - * that is used to refine searches on the provider. - */ - val searchControls = new Inject[SearchControls](defaultSearchControls){} - - /** - * The default SearchControls to use: search the - * base DN with a sub-tree scope, and return the - * "cn" attribute. - */ - def defaultSearchControls() : SearchControls = { - val constraints = new SearchControls() - constraints.setSearchScope(SearchControls.SUBTREE_SCOPE) - constraints.setReturningAttributes(Array("cn")) - return constraints - } - - /** - * The configuration to use for connecting to the - * provider. It should be set via the configure methods - */ - private var internal_config : Map[String,String] = Map.empty - - /** - * The configuration to use for connecting to the - * provider. It should be set via the configure methods - */ - def configuration = internal_config - - /** - * This method checks the configuration and sets defaults for any - * properties that are required. It also processes any of the - * optional configuration propertes related to context testing - * and retries. - * - * This method is intended to be called during update of the default - * configuration, not during granular override of the config. - */ - def processConfig(input : Map[String,String]) : Map[String,String] = { - var currentConfig = input - - def setIfEmpty(name : String, newVal : String) = - if (currentConfig.get(name).isEmpty) { - currentConfig += (name -> newVal) - } - - // Verify the minimum config - setIfEmpty(KEY_URL, DEFAULT_URL) - setIfEmpty(KEY_BASE_DN, DEFAULT_BASE_DN) - setIfEmpty(KEY_USER, DEFAULT_USER) - setIfEmpty(KEY_PASSWORD, DEFAULT_PASSWORD) - setIfEmpty(KEY_AUTHTYPE, DEFAULT_AUTHTYPE) - setIfEmpty(KEY_FACTORY, DEFAULT_FACTORY) - - // Set individual properties - ldapUrl.default.set(currentConfig(KEY_URL)) - ldapBaseDn.default.set(currentConfig(KEY_BASE_DN)) - ldapUser.default.set(currentConfig(KEY_USER)) - ldapPassword.default.set(currentConfig(KEY_PASSWORD)) - ldapAuthType.default.set(currentConfig(KEY_AUTHTYPE)) - ldapFactory.default.set(currentConfig(KEY_FACTORY)) - - // Process the optional configuration properties - currentConfig.get(KEY_LOOKUP).foreach{ - prop => testLookup.default.set(Full(prop)) - } - - ControlHelpers.tryo { - currentConfig.get(KEY_RETRY_INTERVAL).foreach{ - prop => retryInterval.default.set(prop.toLong) - } - } - - ControlHelpers.tryo { - currentConfig.get(KEY_MAX_RETRIES).foreach{ - prop => retryMaxCount.default.set(prop.toInt) - } - } - - currentConfig - } - - protected def propertiesToMap(props: Properties) : Map[String,String] = { - Map.empty ++ props - } - - // =========== Code ==================== - - /** - * Obtains a (possibly cached) InitialContext - * instance based on the currently set parameters. - */ - def initialContext = getInitialContext() - - def attributesFromDn(dn: String): Attributes = - initialContext.getAttributes(dn) - - /** - * Searches the base DN for entities matching the given filter. - */ - def search(filter: String): List[String] = { - logger.debug("Searching for '%s'".format(filter)) - - val resultList = new ListBuffer[String]() - - val searchResults = initialContext.search(ldapBaseDn.vend, - filter, - searchControls.vend) - - while(searchResults.hasMore()) { - resultList += searchResults.next().getName - } - - return resultList.reverse.toList - } - - /** - * Attempts to authenticate the given DN against the configured - * LDAP provider. - */ - def bindUser(dn: String, password: String) : Boolean = { - logger.debug("Attempting to bind user '%s'".format(dn)) - - try { - val username = dn + "," + ldapBaseDn.vend - - ldapUser.doWith(username) { - ldapPassword.doWith(password) { - val ctx = openInitialContext() - ctx.close() - } - } - - logger.info("Successfully authenticated " + dn) - true - } catch { - case ae : AuthenticationException => { - logger.warn("Authentication failed for '%s' : %s".format(dn, ae.getMessage)) - false - } - } - } - - // This caches the context for the current thread - private[this] final val currentInitialContext = new ThreadGlobal[InitialLdapContext]() - - /** - * This method attempts to fetch the cached InitialLdapContext for the - * current thread. If there isn't a current context, open a new one. If a - * test DN is configured, the connection (cached or new) will be validated - * by performing a lookup on the test DN. - */ - protected def getInitialContext() : InitialLdapContext = { - val maxAttempts = retryMaxCount.vend - var attempts = 0 - - var context : Box[InitialLdapContext] = Empty - - while (context.isEmpty && attempts < maxAttempts) { - try { - context = (currentInitialContext.box, testLookup.vend) match { - // If we don't want to test an existing context, just return it - case (Full(ctxt), Empty) => Full(ctxt) - case (Full(ctxt), Failure(_,_,_)) => Full(ctxt) - case (Full(ctxt), Full(test)) => { - logger.debug("Testing InitialContext prior to returning") - ctxt.lookup(test) - Full(ctxt) - } - case (Empty | Failure(_,_,_) ,_) => { - // We'll just allocate a new InitialContext to the thread - currentInitialContext(openInitialContext()) - - // Setting context to Empty here forces one more iteration in case a test - // DN has been configured - Empty - } - } - } catch { - case commE : CommunicationException => { - logger.error(("Communications failure on attempt %d while " + - "verifying InitialContext: %s").format(attempts + 1, commE.getMessage)) - - // The current context failed, so clear it - currentInitialContext(null) - - // We sleep before retrying - Thread.sleep(retryInterval.vend) - attempts += 1 - } - } - } - - // We have a final check on the context before returning - context match { - case Full(ctxt) => ctxt - case Empty | Failure(_,_,_) => throw new CommunicationException("Failed to connect to '%s' after %d attempts". - format(ldapUrl.vend, attempts)) - } - } - - /** - * This method does the actual work of setting up the environment and constructing - * the InitialLdapContext. - */ - protected def openInitialContext () : InitialLdapContext = { - logger.debug("Obtaining an initial context from '%s'".format(ldapUrl.vend)) - - var env = new Hashtable[String, String]() - env.put(Context.PROVIDER_URL, ldapUrl.vend) - env.put(Context.SECURITY_AUTHENTICATION, ldapAuthType.vend) - env.put(Context.SECURITY_PRINCIPAL, ldapUser.vend) - env.put(Context.SECURITY_CREDENTIALS, ldapPassword.vend) - env.put(Context.INITIAL_CONTEXT_FACTORY, ldapFactory.vend) - new InitialLdapContext(env, null) - } -} - diff --git a/persistence/ldap/src/test/resources/ldap.properties b/persistence/ldap/src/test/resources/ldap.properties deleted file mode 100644 index 1eae51d5e4..0000000000 --- a/persistence/ldap/src/test/resources/ldap.properties +++ /dev/null @@ -1,5 +0,0 @@ -# LDAP -ldap.url = ldap://localhost -ldap.base = dc=sample_company,dc=com -ldap.userName = cn=admin,dc=sample_company,dc=com -ldap.password = password diff --git a/persistence/ldap/src/test/resources/logback-test.xml b/persistence/ldap/src/test/resources/logback-test.xml deleted file mode 100644 index bfc78d1d7a..0000000000 --- a/persistence/ldap/src/test/resources/logback-test.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - %d [%thread] %level %logger - %m%n - - - - - - - - - diff --git a/persistence/ldap/src/test/scala/net/liftweb/ldap/LdapSpec.scala b/persistence/ldap/src/test/scala/net/liftweb/ldap/LdapSpec.scala deleted file mode 100644 index 8933bb741a..0000000000 --- a/persistence/ldap/src/test/scala/net/liftweb/ldap/LdapSpec.scala +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2010-2011 WorldWide Conferencing, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.liftweb -package ldap - -import java.io.File - -import javax.naming.CommunicationException - -import org.apache.mina.util.AvailablePortFinder -import org.apache.directory.server.core.DefaultDirectoryService -import org.apache.directory.server.core.partition.impl.btree.jdbm.{JdbmIndex,JdbmPartition} -import org.apache.directory.server.ldap.LdapServer -import org.apache.directory.server.protocol.shared.transport.TcpTransport -import org.apache.directory.server.xdbm.Index -import org.apache.directory.server.core.entry.ServerEntry -import org.apache.directory.shared.ldap.name.LdapDN - -import org.specs2.mutable.Specification -import org.specs2.specification.{ AfterExample, BeforeExample } - -import common._ -import util.Helpers.tryo - - -/** - * Systems under specification for Ldap. - */ -object LdapSpec extends Specification with AfterExample with BeforeExample { - "LDAP Specification".title - sequential - - val ROOT_DN = "dc=ldap,dc=liftweb,dc=net" - - // Thanks to Francois Armand for pointing this utility out! - val service_port = AvailablePortFinder.getNextAvailable(40000) - val service = new DefaultDirectoryService - val ldap = new LdapServer - - lazy val workingDir = Box.legacyNullTest(System.getProperty("apacheds.working.dir")) - - /* - * The following is taken from: - * http://directory.apache.org/apacheds/1.5/41-embedding-apacheds-into-an-application.html - * http://stackoverflow.com/questions/1560230/running-apache-ds-embedded-in-my-application - */ - def before = { - (try { - // Disable changelog - service.getChangeLog.setEnabled(false) - - // Configure a working directory if we have one set, otherwise fail - // because we don't want it using current directory under SBT - workingDir match { - case Full(d) => - val dir = new java.io.File(d) - dir.mkdirs - service.setWorkingDirectory(dir) - case _ => failure("No working dir set for ApacheDS!") - } - - // Set up a partition - val partition = new JdbmPartition - partition.setId("lift-ldap") - partition.setSuffix(ROOT_DN) - service.addPartition(partition) - - // Index attributes (gnarly type due to poor type inferencing) - val indices : java.util.Set[Index[_,ServerEntry]] = new java.util.HashSet() - - List("objectClass", "ou", "uid", "sn").foreach { - attr : String => indices.add(new JdbmIndex(attr)) - } - - partition.setIndexedAttributes(indices) - - // Set up the transport to use our "available" port - ldap.setTransports(new TcpTransport(service_port)) - ldap.setDirectoryService(service) - - service.startup() - - // Inject the root entry if it does not already exist - if ( !service.getAdminSession().exists(partition.getSuffixDn)) { - val rootEntry = service.newEntry(new LdapDN(ROOT_DN)) - rootEntry.add( "objectClass", "top", "domain", "extensibleObject" ); - rootEntry.add( "dc", "ldap" ); - service.getAdminSession().add( rootEntry ); - } - - addTestData() - - ldap.start() - - }) must not(throwAn[Exception]).orSkip - } - - "LDAPVendor" should { - object myLdap extends LDAPVendor - - myLdap.configure(Map("ldap.url" -> "ldap://localhost:%d/".format(service_port), - "ldap.base" -> "dc=ldap,dc=liftweb,dc=net")) - - "handle simple lookups" in { - myLdap.search("objectClass=person") must_== List("cn=Test User") - } - - "handle simple authentication" in { - myLdap.bindUser("cn=Test User", "letmein") must_== true - } - - "attempt reconnects" in { - object badLdap extends LDAPVendor - badLdap.configure() - - // Make sure that we use a port where LDAP won't live - badLdap.ldapUrl.doWith("ldap://localhost:2") { - // Let's not make this spec *too* slow - badLdap.retryInterval.doWith(1000) { - badLdap.search("objectClass=person") must throwA[CommunicationException] - } - } - } - } - - - def after = { - ldap.stop() - service.shutdown() - - // Clean up the working directory - def deleteTree(f : File) { - // First, delete any children if this is a directory - if (f.isDirectory) { - f.listFiles.foreach(deleteTree) - } - f.delete() - } - - tryo { - workingDir.foreach { dir => - deleteTree(new File(dir)) - } - } - } - - def addTestData() { - val username = new LdapDN("cn=Test User," + ROOT_DN) - if (! service.getAdminSession().exists(username)) { - // Add a test user. This will be used for searching and binding - val entry = service.newEntry(username) - entry.add("objectClass", "person", "organizationalPerson") - entry.add("cn", "Test User") - entry.add("sn", "User") - /* LDAP Schema for userPassword is octet string, so we - * need to use getBytes. If you just pass in a straight String, - * ApacheDS sets userPassword to null :( */ - entry.add("userPassword", "letmein".getBytes()) - service.getAdminSession.add(entry) - } - } -} - diff --git a/project/Build.scala b/project/Build.scala index efcd53cc92..5a9789b8f6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,5 +1,5 @@ /* - * Copyright 2012 WorldWide Conferencing, LLC + * Copyright 2012-2013 WorldWide Conferencing, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,7 +76,7 @@ object BuildDef extends Build { // Web Projects // ------------ lazy val web: Seq[ProjectReference] = - Seq(testkit, webkit, wizard) + Seq(testkit, webkit) lazy val testkit = webProject("testkit") @@ -98,16 +98,12 @@ object BuildDef extends Build { System.setProperty("net.liftweb.webapptest.src.test.webapp", (src / "webapp").absString) }) - lazy val wizard = - webProject("wizard") - .dependsOn(webkit, db) - .settings(description := "Wizard Library") // Persistence Projects // -------------------- lazy val persistence: Seq[ProjectReference] = - Seq(db, proto, jpa, mapper, record, squeryl_record, mongodb, mongodb_record, ldap) + Seq(db, proto, mapper, record, squeryl_record, mongodb, mongodb_record) lazy val db = persistenceProject("db") @@ -118,11 +114,6 @@ object BuildDef extends Build { persistenceProject("proto") .dependsOn(webkit) - lazy val jpa = - persistenceProject("jpa") - .dependsOn(webkit) - .settings(libraryDependencies ++= Seq(scalajpa, persistence_api)) - lazy val mapper = persistenceProject("mapper") .dependsOn(db, proto) @@ -156,13 +147,6 @@ object BuildDef extends Build { .dependsOn(record, mongodb) .settings(parallelExecution in Test := false) - lazy val ldap = - persistenceProject("ldap") - .dependsOn(mapper) - .settings(libraryDependencies += apacheds, - initialize in Test <<= (crossTarget in Test) { ct => - System.setProperty("apacheds.working.dir", (ct / "apacheds").absolutePath) - }) def coreProject = liftProject("core") _ def webProject = liftProject("web") _ diff --git a/project/Dependencies.scala b/project/Dependencies.scala index caf678f420..5a5f912212 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,5 +1,5 @@ /* - * Copyright 2011 WorldWide Conferencing, LLC + * Copyright 2011-2013 WorldWide Conferencing, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/project/Developers.scala b/project/Developers.scala index 1290378802..7c0146d683 100644 --- a/project/Developers.scala +++ b/project/Developers.scala @@ -1,5 +1,5 @@ /* - * Copyright 2012 WorldWide Conferencing, LLC + * Copyright 2013 WorldWide Conferencing, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/web/wizard/src/main/scala/net/liftweb/wizard/Wizard.scala b/web/webkit/src/main/scala/net/liftweb/http/Wizard.scala similarity index 95% rename from web/wizard/src/main/scala/net/liftweb/wizard/Wizard.scala rename to web/webkit/src/main/scala/net/liftweb/http/Wizard.scala index 3e49a801ed..38d1c9ba65 100644 --- a/web/wizard/src/main/scala/net/liftweb/wizard/Wizard.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/Wizard.scala @@ -1,5 +1,5 @@ /* - * Copyright 2009-2011 WorldWide Conferencing, LLC + * Copyright 2009-2013 WorldWide Conferencing, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ package net.liftweb -package wizard +package http import net.liftweb._ import http._ @@ -24,14 +24,13 @@ import JsCmds._ import common._ import util._ -import db._ import Helpers._ import scala.xml._ import scala.reflect.Manifest object WizardRules extends Factory with FormVendor { - val dbConnectionsForTransaction: FactoryMaker[List[ConnectionIdentifier]] = - new FactoryMaker[List[ConnectionIdentifier]](() => Nil) {} + val dbConnectionsForTransaction: FactoryMaker[List[LoanWrapper]] = + new FactoryMaker[List[LoanWrapper]](() => Nil) {} private def m[T](implicit man: Manifest[T]): Manifest[T] = man @@ -39,7 +38,7 @@ object WizardRules extends Factory with FormVendor { private object currentWizards extends SessionVar[Set[String]](Set()) - private[wizard] def registerWizardSession(): String = { + private[http] def registerWizardSession(): String = { S.synchronizeForSession { val ret = Helpers.nextFuncName currentWizards.set(currentWizards.is + ret) @@ -47,12 +46,12 @@ object WizardRules extends Factory with FormVendor { } } - private[wizard] def isValidWizardSession(id: String): Boolean = + private[http] def isValidWizardSession(id: String): Boolean = S.synchronizeForSession { currentWizards.is.contains(id) } - private[wizard] def deregisterWizardSession(id: String) { + private[http] def deregisterWizardSession(id: String) { S.synchronizeForSession { currentWizards.set(currentWizards.is - id) } @@ -295,9 +294,9 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { } } - class WizardSnapshot(private[wizard] val screenVars: Map[String, (NonCleanAnyVar[_], Any)], + class WizardSnapshot(private[http] val screenVars: Map[String, (NonCleanAnyVar[_], Any)], val currentScreen: Box[Screen], - private[wizard] val snapshot: Box[WizardSnapshot], + private[http] val snapshot: Box[WizardSnapshot], private val firstScreen: Boolean) extends Snapshot { def restore() { registerThisSnippet(); @@ -322,7 +321,7 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { _screenList = _screenList ::: List(screen) } - def dbConnections: List[ConnectionIdentifier] = WizardRules.dbConnectionsForTransaction.vend + def dbConnections: List[LoanWrapper] = WizardRules.dbConnectionsForTransaction.vend /** * The ordered list of Screens @@ -394,7 +393,7 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { nextScreen match { case Empty => - def useAndFinish(in: List[ConnectionIdentifier]) { + def useAndFinish(in: List[LoanWrapper]) { in match { case Nil => { WizardRules.deregisterWizardSession(CurrentSession.is) @@ -407,8 +406,7 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { } } - case x :: xs => DB.use(x) { - conn => + case x :: xs => x.apply { useAndFinish(xs) } } @@ -546,7 +544,7 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { def postFinish() { } - private[wizard] def enterScreen() { + private[http] def enterScreen() { if (!_touched) { _touched.set(true) localSetup() @@ -599,7 +597,7 @@ trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered { } - private[wizard] object WizardVarHandler { + private[http] object WizardVarHandler { def get[T](name: String): Box[T] = ScreenVars.is.get(name).map(_._2.asInstanceOf[T]) diff --git a/web/wizard/src/test/scala/net/liftweb/wizard/WizardSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/WizardSpec.scala similarity index 100% rename from web/wizard/src/test/scala/net/liftweb/wizard/WizardSpec.scala rename to web/webkit/src/test/scala/net/liftweb/http/WizardSpec.scala From 7964a6fa37c784f90511be32d1e3b3ab4eed35ed Mon Sep 17 00:00:00 2001 From: David Pollak Date: Tue, 12 Feb 2013 18:32:44 -0800 Subject: [PATCH 002/115] First bits of Lift 3.0 --- .../scala/net/liftweb/actor/LAFuture.scala | 19 ++ .../net/liftweb/builtin/snippet/Comet.scala | 2 +- .../scala/net/liftweb/http/CometActor.scala | 12 +- .../scala/net/liftweb/http/LiftMerge.scala | 22 +- .../scala/net/liftweb/http/LiftRules.scala | 57 +++- .../scala/net/liftweb/http/LiftServlet.scala | 6 +- .../scala/net/liftweb/http/LiftSession.scala | 313 +++++++++++++++++- .../src/main/scala/net/liftweb/http/S.scala | 9 +- .../http/auth/HttpAuthentication.scala | 2 +- .../net/liftweb/http/js/JsCommands.scala | 8 +- .../liftweb/http/provider/HTTPProvider.scala | 15 +- .../servlet/HTTPResponseServlet.scala | 2 +- .../containers/Jetty6AsyncProvider.scala | 2 +- .../containers/Jetty7AsyncProvider.scala | 4 +- .../containers/Servlet30AsyncProvider.scala | 2 +- .../net/liftweb/http/rest/RestHelper.scala | 32 ++ 16 files changed, 471 insertions(+), 36 deletions(-) diff --git a/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala b/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala index 1599280b63..7f074b7c03 100644 --- a/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala +++ b/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala @@ -128,6 +128,25 @@ class LAFuture[T] /*extends Future[T]*/ { final class AbortedFutureException() extends Exception("Aborted Future") object LAFuture { + /** + * Create an LAFuture from a function that + * will be applied on a separate thread. The LAFuture + * is returned immediately and the value may be obtained + * by calling `get` + * + * @param f the function that computes the value of the future + * @tparam T the type + * @return an LAFuture that will yield its value when the value has been computed + */ + def apply[T](f: () => T): LAFuture[T] = { + val ret = new LAFuture[T] + LAScheduler.execute(() => { + ret.satisfy(f()) + }) + ret + } + + /** * Collect all the future values into the aggregate future * The returned future will be satisfied when all the diff --git a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Comet.scala b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Comet.scala index c846cbcedc..ef17401185 100644 --- a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Comet.scala +++ b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Comet.scala @@ -60,7 +60,7 @@ object Comet extends DispatchSnippet with LazyLoggable { private def buildSpan(timeb: Box[Long], xml: NodeSeq, cometActor: LiftCometActor, spanId: String): NodeSeq = Elem(cometActor.parentTag.prefix, cometActor.parentTag.label, cometActor.parentTag.attributes, - cometActor.parentTag.scope, Group(xml)) % + cometActor.parentTag.scope, cometActor.parentTag.minimizeEmpty, xml :_*) % (new UnprefixedAttribute("id", Text(spanId), Null)) % (timeb.filter(_ > 0L).map(time => (new PrefixedAttribute("lift", "when", Text(time.toString), Null))) openOr Null) diff --git a/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala b/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala index 6b6c88a3f2..cee889fbda 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala @@ -526,7 +526,7 @@ trait CometActor extends LiftActor with LiftCometActor with BindHelpers { private val logger = Logger(classOf[CometActor]) val uniqueId = Helpers.nextFuncName private var spanId = uniqueId - private var lastRenderTime = Helpers.nextNum + @volatile private var lastRenderTime = Helpers.nextNum /** * If we're going to cache the last rendering, here's the @@ -534,6 +534,12 @@ trait CometActor extends LiftActor with LiftCometActor with BindHelpers { */ private[this] var _realLastRendering: RenderOut = _ + /** + * Get the current render clock for the CometActor + * @return + */ + def renderClock: Long = lastRenderTime + /** * The last rendering (cached or not) */ @@ -725,9 +731,9 @@ trait CometActor extends LiftActor with LiftCometActor with BindHelpers { /** * Creates the span element acting as the real estate for comet rendering. */ - def buildSpan(time: Long, xml: NodeSeq): NodeSeq = { + def buildSpan(time: Long, xml: NodeSeq): Elem = { Elem(parentTag.prefix, parentTag.label, parentTag.attributes, - parentTag.scope, Group(xml)) % + parentTag.scope, parentTag.minimizeEmpty, xml :_*) % new UnprefixedAttribute("id", Text(spanId), if (time > 0L) { diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala index 7b8b6fdae6..445e2d9f86 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala @@ -144,11 +144,11 @@ private[http] trait LiftMerge { node <- _fixHtml(nodes, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy) } yield node - case e: Elem if e.label == "form" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "action", v.attributes, true), v.scope, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) - case e: Elem if e.label == "script" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, false), v.scope, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) - case e: Elem if e.label == "a" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, true), v.scope, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) - case e: Elem if e.label == "link" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, false), v.scope, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) - case e: Elem => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, true), v.scope, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) + case e: Elem if e.label == "form" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "action", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) + case e: Elem if e.label == "script" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, false), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) + case e: Elem if e.label == "a" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) + case e: Elem if e.label == "link" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, false), v.scope, e.minimizeEmpty,_fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) + case e: Elem => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*) case c: Comment if stripComments => NodeSeq.Empty case _ => v } @@ -183,6 +183,12 @@ private[http] trait LiftMerge { headChildren += nl } + for { + e <- S.cometAtEnd() + } { + _fixHtml(e, true, false, false, true, false, false, true, false) + } + // Appends ajax stript to body if (LiftRules.autoIncludeAjaxCalc.vend().apply(this)) { bodyChildren += @@ -233,12 +239,12 @@ private[http] trait LiftMerge { } htmlKids += nl - htmlKids += Elem(headTag.prefix, headTag.label, headTag.attributes, headTag.scope, headChildren.toList: _*) + htmlKids += Elem(headTag.prefix, headTag.label, headTag.attributes, headTag.scope, headTag.minimizeEmpty, headChildren.toList: _*) htmlKids += nl - htmlKids += Elem(bodyTag.prefix, bodyTag.label, bodyTag.attributes, bodyTag.scope, bodyChildren.toList: _*) + htmlKids += Elem(bodyTag.prefix, bodyTag.label, bodyTag.attributes, bodyTag.scope, bodyTag.minimizeEmpty, bodyChildren.toList: _*) htmlKids += nl - val tmpRet = Elem(htmlTag.prefix, htmlTag.label, htmlTag.attributes, htmlTag.scope, htmlKids.toList: _*) + val tmpRet = Elem(htmlTag.prefix, htmlTag.label, htmlTag.attributes, htmlTag.scope, htmlTag.minimizeEmpty, htmlKids.toList: _*) val ret: Node = if (Props.devMode) { LiftRules.xhtmlValidator.toList.flatMap(_(tmpRet)) match { diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala index 2957892575..fb8c35d1cb 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala @@ -36,6 +36,7 @@ import java.util.concurrent.{ConcurrentHashMap => CHash} import scala.reflect.Manifest import java.util.concurrent.atomic.AtomicInteger +import actor.LAFuture class LiftRulesJBridge { def liftRules: LiftRules = LiftRules @@ -70,6 +71,42 @@ object LiftRulesMocker { */ final case class StatelessReqTest(path: List[String], httpReq: HTTPRequest) +/** + * Sometimes we're going to have to surface more data from one of these requests + * than we might like (for example, extra info about continuing the computation on + * a different thread), so we'll start off right by having an Answer trait + * that will have some subclasses and implicit conversions + */ +sealed trait DataAttributeProcessorAnswer + +/** + * The companion object that has the implicit conversions + */ +object DataAttributeProcessorAnswer { + implicit def nodesToAnswer(in: NodeSeq): DataAttributeProcessorAnswer = DataAttributeProcessorAnswerNodes(in) + implicit def nodeFuncToAnswer(in: () => NodeSeq): DataAttributeProcessorAnswer = DataAttributeProcessorAnswerFork(in) + implicit def nodeFutureToAnswer(in: LAFuture[NodeSeq]): DataAttributeProcessorAnswer = DataAttributeProcessorAnswerFuture(in) + implicit def setNodeToAnswer(in: Seq[Node]): DataAttributeProcessorAnswer = DataAttributeProcessorAnswerNodes(in) +} + +/** + * Yep... just a bunch of nodes. + * @param nodes + */ +final case class DataAttributeProcessorAnswerNodes(nodes: NodeSeq) extends DataAttributeProcessorAnswer + +/** + * A function that returns a bunch of nodes... run it on a different thread + * @param nodeFunc + */ +final case class DataAttributeProcessorAnswerFork(nodeFunc: () => NodeSeq) extends DataAttributeProcessorAnswer + +/** + * A future that returns nodes... run them on a different thread + * @param nodeFuture the future of the NodeSeq + */ +final case class DataAttributeProcessorAnswerFuture(nodeFuture: LAFuture[NodeSeq]) extends DataAttributeProcessorAnswer + /** * The Lift configuration singleton */ @@ -87,6 +124,12 @@ object LiftRules extends LiftRulesMocker { type DispatchPF = PartialFunction[Req, () => Box[LiftResponse]]; + /** + * A partial function that allows processing of any attribute on an Elem + * if the attribute begins with "data-" + */ + type DataAttributeProcessor = PartialFunction[(String, String, Elem), DataAttributeProcessorAnswer] + /** * The test between the path of a request and whether that path * should result in stateless servicing of that path @@ -423,7 +466,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable { val ret: Box[String] = for{ url <- Box !! LiftRules.getClass.getResource("/" + cn + ".class") - val newUrl = new java.net.URL(url.toExternalForm.split("!")(0) + "!" + "/META-INF/MANIFEST.MF") + newUrl = new java.net.URL(url.toExternalForm.split("!")(0) + "!" + "/META-INF/MANIFEST.MF") str <- tryo(new String(readWholeStream(newUrl.openConnection.getInputStream), "UTF-8")) ma <- """Implementation-Version: (.*)""".r.findFirstMatchIn(str) } yield ma.group(1) @@ -436,7 +479,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable { val ret: Box[Date] = for{ url <- Box !! LiftRules.getClass.getResource("/" + cn + ".class") - val newUrl = new java.net.URL(url.toExternalForm.split("!")(0) + "!" + "/META-INF/MANIFEST.MF") + newUrl = new java.net.URL(url.toExternalForm.split("!")(0) + "!" + "/META-INF/MANIFEST.MF") str <- tryo(new String(readWholeStream(newUrl.openConnection.getInputStream), "UTF-8")) ma <- """Built-Time: (.*)""".r.findFirstMatchIn(str) asLong <- asLong(ma.group(1)) @@ -836,6 +879,14 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable { new FactoryMaker(() => () => DefaultRoutines.resourceForCurrentReq()) {} + /** + * Ever wanted to add custom attribute processing to Lift? Here's your chance. + * Every attribute with the data- prefix will be tested against this + * RulesSeq and if there's a match, then use the rule process. Simple, easy, cool. + */ + val dataAttributeProcessor: RulesSeq[DataAttributeProcessor] = new RulesSeq() + + /** * There may be times when you want to entirely control the templating process. You can insert * a function to this factory that will do your custom template resolution. If the PartialFunction @@ -925,7 +976,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable { val sm = smf() _sitemap = Full(sm) for (menu <- sm.menus; - val loc = menu.loc; + loc = menu.loc; rewrite <- loc.rewritePF) LiftRules.statefulRewrite.append(PerRequestPF(rewrite)) _sitemap diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala index 4806b49181..58d1f96625 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala @@ -81,7 +81,7 @@ class LiftServlet extends Loggable { logger.debug("Destroyed Lift handler.") // super.destroy } catch { - case e => logger.error("Destruction failure", e) + case e: Exception => logger.error("Destruction failure", e) } } @@ -162,7 +162,7 @@ class LiftServlet extends Loggable { handleGenericContinuation(theReq, resp, sesBox, func); true // we have to return true to hold onto the request case e if e.getClass.getName.endsWith("RetryRequest") => throw e - case e => logger.info("Request for " + req.request.uri + " failed " + e.getMessage, e); throw e + case e: Throwable => logger.info("Request for " + req.request.uri + " failed " + e.getMessage, e); throw e } } @@ -531,7 +531,7 @@ class LiftServlet extends Loggable { } } catch { case foc: LiftFlowOfControlException => throw foc - case e => S.runExceptionHandlers(requestState, e) + case e: Exception => S.runExceptionHandlers(requestState, e) } } diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala index d1bb00662a..dc858fdaaa 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala @@ -20,6 +20,7 @@ package http import java.lang.reflect.{Method} import collection.mutable.{HashMap, ListBuffer} +import js.JE.{JsRaw, AnonFunc} import xml._ import common._ @@ -31,6 +32,7 @@ import http.js.{JsCmd, AjaxInfo} import builtin.snippet._ import js._ import provider._ +import json.JsonAST object LiftSession { @@ -669,7 +671,7 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, else soFar } catch { - case exception => + case exception: Exception => (valid, pair :: invalid) } } @@ -1228,7 +1230,7 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case e: LiftFlowOfControlException => throw e - case e => S.runExceptionHandlers(request, e) + case e: Exception => S.runExceptionHandlers(request, e) } @@ -1994,6 +1996,192 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, private object _lastFoundSnippet extends ThreadGlobal[String] + private object DataAttrNode { + def unapply(in: Node): Option[DataAttributeProcessorAnswer] = { + in match { + case e: Elem => + + val attrs = e.attributes + + val rules = LiftRules.dataAttributeProcessor.toList + + e.attributes.toStream.flatMap { + case UnprefixedAttribute(key, value, _) if key.toLowerCase().startsWith("data-") => + val nk = key.substring(5).toLowerCase() + val vs = value.text + val a2 = attrs.filter{ + case UnprefixedAttribute(k2, _, _) => k2 != key + case _ => true + } + + NamedPF.applyBox((nk, vs, new Elem(e.prefix, e.label, a2, e.scope, e.minimizeEmpty, e.child :_*)), rules) + case _ => None + }.headOption + + case _ => None + } + } + } + + /** + * Pass in a LiftActor and get a JavaScript expression (function(x) {...}) that + * represents an asynchronous Actor message from the client to the server. + * + * The Actor should respond to a message in the form of a JsonAST.JValue. + * + * This method requires the session be stateful + * + * In general, your Actor should be a subclass of ScopedLiftActor because + * that way you'll have the scope of the current session. + * + * @param in the Actor to send messages to. + * + * @return a JsExp that contains a function that can be called with a parameter + * and when the function is called, the parameter is JSON serialized and sent to + * the server + */ + def clientActorFor(in: LiftActor): JsExp = { + testStatefulFeature{ + AnonFunc("x", + SHtml.jsonCall(JsRaw("x"), (p: JsonAST.JValue) => { + in ! p + JsCmds.Noop + }).cmd) + + } + } + + /** + * Pass in a LiftActor and get a JavaScript expression (function(x) {...}) that + * represents an asynchronous Actor message from the client to the server. + * + * The Actor should respond to a message in the form of a JsonAST.JValue. + * + * This method requires the session be stateful + * + * In general, your Actor should be a subclass of ScopedLiftActor because + * that way you'll have the scope of the current session. + * + * @param in the Actor to send messages to. + * @param xlate a function that will take the JsonAST.JValue and convert it + * into a representation that can be sent to the Actor (probably + * deserialize it into a case class.) If the translation succeeds, + * the translated message will be sent to the actor. If the + * translation fails, an error will be logged and the raw + * JsonAST.JValue will be sent to the actor + * + * + * @return a JsExp that contains a function that can be called with a parameter + * and when the function is called, the parameter is JSON serialized and sent to + * the server + */ + def clientActorFor(in: LiftActor, xlate: JsonAST.JValue => Box[Any]): JsExp = { + testStatefulFeature{ + AnonFunc("x", + SHtml.jsonCall(JsRaw("x"), (p: JsonAST.JValue) => { + in.!(xlate(p) match { + case Full(v) => v + case Empty => logger.error("Failed to deserialize JSON message "+p); p + case Failure(msg, _, _) => logger.error("Failed to deserialize JSON message "+p+". Error "+msg); p + }) + JsCmds.Noop + }).cmd) + + } + } + + + /** + * Create a Actor that will take messages on the server and then send them to the client. So, from the + * server perspective, it's just an Async message send. From the client perspective, they get a function + * called each time the message is sent to the server. + * + * If the message sent to the LiftActor is a JsCmd or JsExp, then the code is sent directly to the + * client and executed on the client. + * + * If the message is a JsonAST.JValue, it's turned into a JSON string, sent to the client and + * the client calls the function named in the `toCall` parameter with the value. + * + * If the message is anything else, we attempt to JSON serialize the message and if it + * can be JSON serialized, it's sent over the wire and passed to the `toCall` function on the server + * @return + */ + def serverActorForClient(toCall: String): LiftActor = { + testStatefulFeature{ + val ca = new CometActor { + /** + * It's the main method to override, to define what is rendered by the CometActor + * + * There are implicit conversions for a bunch of stuff to + * RenderOut (including NodeSeq). Thus, if you don't declare the return + * turn to be something other than RenderOut and return something that's + * coercible into RenderOut, the compiler "does the right thing"(tm) for you. + *
+ * There are implicit conversions for NodeSeq, so you can return a pile of + * XML right here. There's an implicit conversion for NodeSeq => NodeSeq, + * so you can return a function (e.g., a CssBindFunc) that will convert + * the defaultHtml to the correct output. There's an implicit conversion + * from JsCmd, so you can return a pile of JavaScript that'll be shipped + * to the browser.
+ * Note that the render method will be called each time a new browser tab + * is opened to the comet component or the comet component is otherwise + * accessed during a full page load (this is true if a partialUpdate + * has occurred.) You may want to look at the fixedRender method which is + * only called once and sets up a stable rendering state. + */ + def render: RenderOut = NodeSeq.Empty + + + + override def lifespan = Full(120) + + override def hasOuter = false + + override def parentTag =
+ + override def lowPriority: PartialFunction[Any, Unit] = { + case jsCmd: JsCmd => partialUpdate(JsCmds.JsTry(jsCmd, false)) + case jsExp: JsExp => partialUpdate(JsCmds.JsTry(jsExp.cmd, false)) + case jv: JsonAST.JValue => { + val s: String = json.pretty(json.render(jv)) + partialUpdate(JsCmds.JsTry(JsRaw(toCall+"("+s+")").cmd, false)) + } + case x: AnyRef => { + println("Hey we got the message "+x) + import json._ + implicit val formats = Serialization.formats(NoTypeHints) + + val ser: Box[String] = Helpers.tryo(Serialization.write(x)) + + ser.foreach(s => partialUpdate(JsCmds.JsTry(JsRaw(toCall+"("+s+")").cmd, false))) + + } + + case _ => // this will never happen because the message is boxed + + } + } + + synchronized { + asyncComponents(ca.theType -> ca.name) = ca + asyncById(ca.uniqueId) = ca + } + + ca.callInitCometActor(this, Full(Helpers.nextFuncName), Full(Helpers.nextFuncName), NodeSeq.Empty, Map.empty) + + + + ca ! PerformSetupComet2(Empty) + + val node: Elem = ca.buildSpan(ca.renderClock, NodeSeq.Empty) + + S.addCometAtEnd(node) + + ca + } + } + + /** * Processes the surround tag and other lift tags * @@ -2006,6 +2194,18 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case Group(nodes) => Group(processSurroundAndInclude(page, nodes)) + + case elem@DataAttrNode(toDo) => toDo match { + case DataAttributeProcessorAnswerNodes(nodes) => + processSurroundAndInclude(page, nodes) + + case DataAttributeProcessorAnswerFork(nodeFunc) => + processOrDefer(true)(processSurroundAndInclude(page, nodeFunc())) + case DataAttributeProcessorAnswerFuture(nodeFuture) => + processOrDefer(true)(processSurroundAndInclude(page, + nodeFuture.get(15000).openOr(NodeSeq.Empty))) + } + case elem @ SnippetNode(element, kids, isLazy, attrs, snippetName) if snippetName != _lastFoundSnippet.value => processOrDefer(isLazy) { S.withCurrentSnippetNodeSeq(elem) { @@ -2024,7 +2224,7 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case v: Elem => Elem(v.prefix, v.label, processAttributes(v.attributes, this.allowAttributeProcessing.is), - v.scope, processSurroundAndInclude(page, v.child): _*) + v.scope, v.minimizeEmpty, processSurroundAndInclude(page, v.child): _*) case pcd: scala.xml.PCData => pcd case text: Text => text @@ -2425,7 +2625,7 @@ private object SnippetNode { } yield { val (par, nonLift) = liftAttrsAndParallel(elm.attributes) val newElm = new Elem(elm.prefix, elm.label, - nonLift, elm.scope, elm.child: _*) + nonLift, elm.scope, elm.minimizeEmpty, elm.child: _*) (newElm, newElm, par || (lift.find { case up: UnprefixedAttribute if up.key == "parallel" => true @@ -2442,3 +2642,108 @@ private object SnippetNode { } } } + +/** + * A LiftActor that runs in the scope of the current Session, repleat with SessionVars, etc. + * In general, you'll want to use a ScopedLiftActor when you do stuff with clientActorFor, etc. + * so that you have the session scope + * + */ +trait ScopedLiftActor extends LiftActor with LazyLoggable { + /** + * The session captured when the instance is created. It should be correct if the instance is created + * in the scope of a request + */ + protected val _session: LiftSession = S.session openOr new LiftSession("", Helpers.nextFuncName, Empty) + + /** + * The render version of the page that this was created in the scope of + */ + protected val _uniqueId: String = RenderVersion.get + + /** + * The session associated with this actor. By default it's captured at the time of instantiation, but + * that doesn't always work, so you might have to override this method + * @return + */ + def session: LiftSession = _session + + + /** + * The unique page ID of the page that this Actor was created in the scope of + * @return + */ + def uniqueId: String = _uniqueId + + /** + * Compose the Message Handler function. By default, + * composes highPriority orElse mediumPriority orElse internalHandler orElse + * lowPriority orElse internalHandler. But you can change how + * the handler works if doing stuff in highPriority, mediumPriority and + * lowPriority is not enough. + */ + protected def composeFunction: PartialFunction[Any, Unit] = composeFunction_i + + private def composeFunction_i: PartialFunction[Any, Unit] = { + // if we're no longer running don't pass messages to the other handlers + // just pass them to our handlers + highPriority orElse mediumPriority orElse + lowPriority + } + + /** + * Handle messages sent to this Actor before the + */ + def highPriority: PartialFunction[Any, Unit] = Map.empty + + def lowPriority: PartialFunction[Any, Unit] = Map.empty + + def mediumPriority: PartialFunction[Any, Unit] = Map.empty + + protected override def messageHandler = { + val what = composeFunction + val myPf: PartialFunction[Any, Unit] = new PartialFunction[Any, Unit] { + def apply(in: Any): Unit = + S.initIfUninitted(session) { + RenderVersion.doWith(uniqueId) { + S.functionLifespan(true) { + try { + what.apply(in) + } catch { + case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e) + case e: Exception => reportError("Message dispatch for " + in, e) + } + if (S.functionMap.size > 0) { + session.updateFunctionMap(S.functionMap, uniqueId, millis) + S.clearFunctionMap + } + } + } + } + + def isDefinedAt(in: Any): Boolean = + S.initIfUninitted(session) { + RenderVersion.doWith(uniqueId) { + S.functionLifespan(true) { + try { + what.isDefinedAt(in) + } catch { + case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e); false + case e: Exception => reportError("Message test for " + in, e); false + } + } + } + } + } + + myPf + } + + /** + * How to report an error that occurs during message dispatch + */ + protected def reportError(msg: String, exception: Exception) { + logger.error(msg, exception) + } + +} diff --git a/web/webkit/src/main/scala/net/liftweb/http/S.scala b/web/webkit/src/main/scala/net/liftweb/http/S.scala index 9092ca08d1..e235b86abd 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/S.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/S.scala @@ -362,6 +362,8 @@ trait S extends HasParams with Loggable { */ private object _tailTags extends TransientRequestVar(new ListBuffer[Elem]) + private object _cometTags extends TransientRequestVar(new ListBuffer[Elem]) + private object p_queryLog extends TransientRequestVar(new ListBuffer[(String, Long)]) private object p_notice extends TransientRequestVar(new ListBuffer[(NoticeType.Value, NodeSeq, Box[String])]) @@ -541,7 +543,7 @@ trait S extends HasParams with Loggable { // TODO: Is this used anywhere? - DCB def templateFromTemplateAttr: Box[NodeSeq] = for (templateName <- attr("template") ?~ "Template Attribute missing"; - val tmplList = templateName.roboSplit("/"); + tmplList = templateName.roboSplit("/"); template <- Templates(tmplList) ?~ "couldn't find template") yield template @@ -788,6 +790,11 @@ trait S extends HasParams with Loggable { */ def atEndOfBody(): List[Elem] = _tailTags.is.toList + + def addCometAtEnd(elem: Elem): Unit = _cometTags.is += elem + + def cometAtEnd(): List[Elem] = _cometTags.is.toList + /** * Sometimes it's helpful to accumute JavaScript as part of servicing * a request. For example, you may want to accumulate the JavaScript diff --git a/web/webkit/src/main/scala/net/liftweb/http/auth/HttpAuthentication.scala b/web/webkit/src/main/scala/net/liftweb/http/auth/HttpAuthentication.scala index f6c7bcb673..1f5c448f30 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/auth/HttpAuthentication.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/auth/HttpAuthentication.scala @@ -112,7 +112,7 @@ case class HttpDigestAuthentication(realmName: String)(func: PartialFunction[(St try { Schedule.schedule (this, CheckAndPurge, 5 seconds) } catch { - case e => logger.error("Couldn't start NonceWatcher ping", e) + case e: Exception => logger.error("Couldn't start NonceWatcher ping", e) } } diff --git a/web/webkit/src/main/scala/net/liftweb/http/js/JsCommands.scala b/web/webkit/src/main/scala/net/liftweb/http/js/JsCommands.scala index 924d26945b..46cbc6ebbd 100755 --- a/web/webkit/src/main/scala/net/liftweb/http/js/JsCommands.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/js/JsCommands.scala @@ -261,9 +261,9 @@ object JE { /** * gets the element by ID */ - case class ElemById(id: String, then: String*) extends JsExp { + case class ElemById(id: String, thenStr: String*) extends JsExp { override def toJsCmd = "document.getElementById(" + id.encJs + ")" + ( - if (then.isEmpty) "" else then.mkString(".", ".", "") + if (thenStr.isEmpty) "" else thenStr.mkString(".", ".", "") ) } @@ -781,9 +781,9 @@ object JsCmds { * Assigns the value of 'right' to the members of the element * having this 'id', chained by 'then' sequences */ - case class SetElemById(id: String, right: JsExp, then: String*) extends JsCmd { + case class SetElemById(id: String, right: JsExp, thenStr: String*) extends JsCmd { def toJsCmd = "if (document.getElementById(" + id.encJs + ")) {document.getElementById(" + id.encJs + ")" + ( - if (then.isEmpty) "" else then.mkString(".", ".", "") + if (thenStr.isEmpty) "" else thenStr.mkString(".", ".", "") ) + " = " + right.toJsCmd + ";};" } diff --git a/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala b/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala index 189f868428..abf75f7289 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/provider/HTTPProvider.scala @@ -87,8 +87,17 @@ trait HTTPProvider { preBoot b.boot } catch { - case e => - logger.error("Failed to Boot! Your application may not run properly", e); + case e: Exception => + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("********** Failed to Boot! Your application may not run properly", e); + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") + logger.error("------------------------------------------------------------------") } finally { postBoot @@ -114,7 +123,7 @@ trait HTTPProvider { LiftRules.templateCache = Full(InMemoryCache(500)) } } catch { - case _ => logger.error("LiftWeb core resource bundle for locale " + Locale.getDefault() + ", was not found ! ") + case _: Exception => logger.error("LiftWeb core resource bundle for locale " + Locale.getDefault() + ", was not found ! ") } finally { LiftRules.bootFinished() } diff --git a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/HTTPResponseServlet.scala b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/HTTPResponseServlet.scala index cd079ce590..c466d5221f 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/HTTPResponseServlet.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/HTTPResponseServlet.scala @@ -43,7 +43,7 @@ class HTTPResponseServlet(resp: HttpServletResponse) extends HTTPResponse { val cook30 = cookie.asInstanceOf[{def setHttpOnly(b: Boolean): Unit}] cook30.setHttpOnly(bv) } catch { - case e => // swallow.. the exception will be thrown for Servlet 2.5 containers but work for servlet + case e: Exception => // swallow.. the exception will be thrown for Servlet 2.5 containers but work for servlet // 3.0 containers } } diff --git a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty6AsyncProvider.scala b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty6AsyncProvider.scala index b5684bcd6f..b5a9cf855b 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty6AsyncProvider.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty6AsyncProvider.scala @@ -50,7 +50,7 @@ object Jetty6AsyncProvider extends AsyncProviderMeta { val isPending = cci.getMethod("isPending") (true, (cc), (meth), (getObj), (setObj), (suspend), resume, isPending) } catch { - case e => (false, null, null, null, null, null, null, null) + case e: Exception => (false, null, null, null, null, null, null, null) } } diff --git a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty7AsyncProvider.scala b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty7AsyncProvider.scala index ab553de8a1..8ccff77793 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty7AsyncProvider.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Jetty7AsyncProvider.scala @@ -54,7 +54,7 @@ object Jetty7AsyncProvider extends AsyncProviderMeta { val isResumed = cci.getMethod("isResumed") (true, (cc), (meth), (getAttribute), (setAttribute), (suspend), setTimeout, resume, isExpired, isResumed) } catch { - case e => + case e: Exception => (false, null, null, null, null, null, null, null, null, null) } } @@ -128,7 +128,7 @@ class Jetty7AsyncProvider(req: HTTPRequest) extends ServletAsyncProvider { resumeMeth.invoke(cont) true } catch { - case e => setAttribute.invoke(cont, "__liftCometState", null) + case e: Exception => setAttribute.invoke(cont, "__liftCometState", null) false } } diff --git a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Servlet30AsyncProvider.scala b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Servlet30AsyncProvider.scala index 878c4e3f1f..f3a24579af 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Servlet30AsyncProvider.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/provider/servlet/containers/Servlet30AsyncProvider.scala @@ -48,7 +48,7 @@ object Servlet30AsyncProvider extends AsyncProviderMeta { val isSupported = cc.getMethod("isAsyncSupported") (true, cc, asyncClass, startAsync, getResponse, complete, isSupported) } catch { - case e => + case e: Exception => (false, null, null, diff --git a/web/webkit/src/main/scala/net/liftweb/http/rest/RestHelper.scala b/web/webkit/src/main/scala/net/liftweb/http/rest/RestHelper.scala index ff7c638c7a..eb8448b6d5 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/rest/RestHelper.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/rest/RestHelper.scala @@ -20,6 +20,7 @@ package rest import net.liftweb._ +import actor.LAFuture import json._ import common._ import util._ @@ -520,6 +521,37 @@ trait RestHelper extends LiftRules.DispatchPF { protected implicit def thingToResp[T](in: T)(implicit c: T => LiftResponse): () => Box[LiftResponse] = () => Full(c(in)) + + /** + * If we're returning a future, then automatically turn the request into an Async request + * @param in the LAFuture of the response type + * @param c the implicit conversion from T to LiftResponse + * @tparam T the type + * @return Nothing + */ + protected implicit def futureToResponse[T](in: LAFuture[T])(implicit c: T => LiftResponse): + () => Box[LiftResponse] = () => { + RestContinuation.async(reply => { + in.foreach(t => reply.apply(c(t))) + }) + } + + /** + * If we're returning a future, then automatically turn the request into an Async request + * @param in the LAFuture of the response type + * @param c the implicit conversion from T to LiftResponse + * @tparam T the type + * @return Nothing + */ + protected implicit def futureBoxToResponse[T](in: LAFuture[Box[T]])(implicit c: T => LiftResponse): + () => Box[LiftResponse] = () => { + RestContinuation.async(reply => { + in.foreach(t => reply.apply{ + boxToResp(t).apply() openOr NotFoundResponse() + }) + }) + } + /** * Turn a Box[T] into the return type expected by * DispatchPF. Note that this method will return From 9b77ed65f0826e4b0264e20bdbcbb1f14b9c088d Mon Sep 17 00:00:00 2001 From: David Pollak Date: Wed, 13 Feb 2013 13:56:25 -0800 Subject: [PATCH 003/115] Added Tag Processing --- .../scala/net/liftweb/http/LiftRules.scala | 29 ++++++++++++++++++- .../scala/net/liftweb/http/LiftSession.scala | 29 +++++++++++++++---- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala index fb8c35d1cb..aaf7289f22 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala @@ -128,7 +128,12 @@ object LiftRules extends LiftRulesMocker { * A partial function that allows processing of any attribute on an Elem * if the attribute begins with "data-" */ - type DataAttributeProcessor = PartialFunction[(String, String, Elem), DataAttributeProcessorAnswer] + type DataAttributeProcessor = PartialFunction[(String, String, Elem, LiftSession), DataAttributeProcessorAnswer] + + /** + * The pattern/PartialFunction for matching tags in Lift + */ + type TagProcessor = PartialFunction[(String, Elem, LiftSession), DataAttributeProcessorAnswer] /** * The test between the path of a request and whether that path @@ -886,6 +891,28 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable { */ val dataAttributeProcessor: RulesSeq[DataAttributeProcessor] = new RulesSeq() + /** + * Ever wanted to match on *any* arbitrary tag in your HTML and process it + * any way you wanted? Well, here's your chance, dude. You can capture any + * tag and do anything you want with it. + * + * Note that this set of PartialFunctions is run for **EVERY** node + * in the DOM so make sure it runs *FAST*. + * + * Also, no subsequent processing of the returned NodeSeq is done (no + * LiftSession.processSurroundAndInclude()) so evaluate everything + * you want to. + * + * But do avoid infinite loops, so make sure the PartialFunction actually + * returns true *only* when you're going to return a modified node. + * + * An example might be: + * + * + * case ("script", e, session) if e.getAttribute("data-serverscript").isDefined => ... + */ + val tagProcessor: RulesSeq[TagProcessor] = new RulesSeq() + /** * There may be times when you want to entirely control the templating process. You can insert diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala index dc858fdaaa..819a92a787 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala @@ -1997,14 +1997,14 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, private object _lastFoundSnippet extends ThreadGlobal[String] private object DataAttrNode { + val rules = LiftRules.dataAttributeProcessor.toList + def unapply(in: Node): Option[DataAttributeProcessorAnswer] = { in match { - case e: Elem => + case e: Elem if !rules.isEmpty => val attrs = e.attributes - val rules = LiftRules.dataAttributeProcessor.toList - e.attributes.toStream.flatMap { case UnprefixedAttribute(key, value, _) if key.toLowerCase().startsWith("data-") => val nk = key.substring(5).toLowerCase() @@ -2014,8 +2014,8 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case _ => true } - NamedPF.applyBox((nk, vs, new Elem(e.prefix, e.label, a2, e.scope, e.minimizeEmpty, e.child :_*)), rules) - case _ => None + NamedPF.applyBox((nk, vs, new Elem(e.prefix, e.label, a2, e.scope, e.minimizeEmpty, e.child :_*), LiftSession.this), rules) + case _ => Empty }.headOption case _ => None @@ -2023,6 +2023,18 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, } } + private object TagProcessingNode { + val rules = LiftRules.tagProcessor.toList + + def unapply(in: Node): Option[DataAttributeProcessorAnswer] = { + in match { + case e: Elem if !rules.isEmpty => + NamedPF.applyBox((e.label, e, LiftSession.this), rules) + case _ => None + } + } + } + /** * Pass in a LiftActor and get a JavaScript expression (function(x) {...}) that * represents an asynchronous Actor message from the client to the server. @@ -2194,6 +2206,13 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case Group(nodes) => Group(processSurroundAndInclude(page, nodes)) + case elem@TagProcessingNode(toDo) => toDo match { + case DataAttributeProcessorAnswerNodes(nodes) => nodes + case DataAttributeProcessorAnswerFork(nodeFunc) => + processOrDefer(true)(nodeFunc()) + case DataAttributeProcessorAnswerFuture(nodeFuture) => + processOrDefer(true)(nodeFuture.get(15000).openOr(NodeSeq.Empty)) + } case elem@DataAttrNode(toDo) => toDo match { case DataAttributeProcessorAnswerNodes(nodes) => From f14302a7e585d8b7a95ab7d7e797e668a7aef6f1 Mon Sep 17 00:00:00 2001 From: David Pollak Date: Wed, 13 Feb 2013 15:28:27 -0800 Subject: [PATCH 004/115] Snippets processed before Tags --- .../scala/net/liftweb/http/LiftSession.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala index 819a92a787..87fb37fea1 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala @@ -2206,14 +2206,6 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, case Group(nodes) => Group(processSurroundAndInclude(page, nodes)) - case elem@TagProcessingNode(toDo) => toDo match { - case DataAttributeProcessorAnswerNodes(nodes) => nodes - case DataAttributeProcessorAnswerFork(nodeFunc) => - processOrDefer(true)(nodeFunc()) - case DataAttributeProcessorAnswerFuture(nodeFuture) => - processOrDefer(true)(nodeFuture.get(15000).openOr(NodeSeq.Empty)) - } - case elem@DataAttrNode(toDo) => toDo match { case DataAttributeProcessorAnswerNodes(nodes) => processSurroundAndInclude(page, nodes) @@ -2241,6 +2233,14 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, } } + case elem@TagProcessingNode(toDo) => toDo match { + case DataAttributeProcessorAnswerNodes(nodes) => nodes + case DataAttributeProcessorAnswerFork(nodeFunc) => + processOrDefer(true)(nodeFunc()) + case DataAttributeProcessorAnswerFuture(nodeFuture) => + processOrDefer(true)(nodeFuture.get(15000).openOr(NodeSeq.Empty)) + } + case v: Elem => Elem(v.prefix, v.label, processAttributes(v.attributes, this.allowAttributeProcessing.is), v.scope, v.minimizeEmpty, processSurroundAndInclude(page, v.child): _*) From 5943e5d9bb2655ede9a1d25e4539fbdf31f33239 Mon Sep 17 00:00:00 2001 From: David Pollak Date: Wed, 13 Feb 2013 16:03:38 -0800 Subject: [PATCH 005/115] A helper to remove an attribute from an Elem --- .../scala/net/liftweb/util/BindHelpers.scala | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala b/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala index b902197f35..a2e4349bc0 100644 --- a/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala @@ -147,6 +147,23 @@ trait BindHelpers { case _ => elem } + + /** + * Remove an attribute from the element + * + * @param name the name of the attribute to remove + * @param elem the element + * @return the element sans the named attribute + */ + def removeAttribute(name: String, elem: Elem): Elem = { + val a = elem.attributes.filter{ + case up: UnprefixedAttribute => up.key != name + case _ => true + } + + elem.copy(attributes = a) + } + /** * Adds a css class to the existing class tag of an Elem or create * the class attribute @@ -1255,7 +1272,7 @@ trait BindHelpers { new Elem(e.prefix, e.label, new UnprefixedAttribute("id", id, meta), - e.scope, e.child :_*) + e.scope, e.minimizeEmpty, e.child :_*) } case x => x From fbdfaf177eea9d0d07a1809b45e2fea851ac8483 Mon Sep 17 00:00:00 2001 From: David Pollak Date: Tue, 19 Feb 2013 12:13:36 -0800 Subject: [PATCH 006/115] Fixed some bugs in the Html5 writing routines --- .../scala/net/liftweb/util/HtmlParser.scala | 166 +++++++++--------- 1 file changed, 86 insertions(+), 80 deletions(-) diff --git a/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala b/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala index 1d5f759f4b..ff6eabb6c9 100644 --- a/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala +++ b/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala @@ -37,7 +37,7 @@ trait Html5Writer { */ protected def writeAttributes(m: MetaData, writer: Writer) { m match { - case null => + case null => case Null => case md if (null eq md.value) => writeAttributes(md.next, writer) case up: UnprefixedAttribute => { @@ -54,21 +54,21 @@ trait Html5Writer { case '<' => writer.append("<") case c if c >= ' ' && c.toInt <= 127 => writer.append(c) case c if c == '\u0085' => - case c => { - val str = Integer.toHexString(c) - writer.append("&#x") - writer.append("0000".substring(str.length)) - writer.append(str) - writer.append(';') - } + case c => { + val str = Integer.toHexString(c) + writer.append("&#x") + writer.append("0000".substring(str.length)) + writer.append(str) + writer.append(';') + } } - + pos += 1 } - - writer.append('"') - writeAttributes(up.next, writer) + writer.append('"') + + writeAttributes(up.next, writer) } case pa: PrefixedAttribute => { @@ -100,12 +100,12 @@ trait Html5Writer { pos += 1 } - writer.append('"') + writer.append('"') } - writeAttributes(pa.next, writer) + writeAttributes(pa.next, writer) } - + case x => writeAttributes(x.next, writer) } } @@ -127,7 +127,7 @@ trait Html5Writer { case '\n' => sb.append('\n') case '\r' => sb.append('\r') case '\t' => sb.append('\t') - case c => + case c => if (reverse) { HtmlEntities.revMap.get(c) match { case Some(str) => { @@ -135,15 +135,15 @@ trait Html5Writer { sb.append(str) sb.append(';') } - case _ => - if (c >= ' ' && - c != '\u0085' && - !(c >= '\u007f' && c <= '\u0095')) sb.append(c) + case _ => + if (c >= ' ' && + c != '\u0085' && + !(c >= '\u007f' && c <= '\u0095')) sb.append(c) } } else { - if (c >= ' ' && - c != '\u0085' && - !(c >= '\u007f' && c <= '\u0095')) sb.append(c) + if (c >= ' ' && + c != '\u0085' && + !(c >= '\u007f' && c <= '\u0095')) sb.append(c) } } @@ -187,14 +187,14 @@ trait Html5Writer { case a: Atom[_] if a.getClass eq classOf[Atom[_]] => escape(a.data.toString, writer, !convertAmp) - + case Comment(comment) if !stripComment => { writer.append("") } - - case er: EntityRef => + + case er: EntityRef if convertAmp => HtmlEntities.entMap.get(er.entityName) match { case Some(chr) if chr.toInt >= 128 => writer.append(chr) case _ => { @@ -203,25 +203,32 @@ trait Html5Writer { writer.append(sb) } } - + + + case er: EntityRef => + val sb = new StringBuilder() + er.buildString(sb) + writer.append(sb) + + case x: SpecialNode => { val sb = new StringBuilder() x.buildString(sb) writer.append(sb) } - + case g: Group => for (c <- g.nodes) write(c, writer, stripComment, convertAmp) - - case e: Elem if (null eq e.prefix) && - Html5Constants.nonReplaceable_?(e.label) => { + + case e: Elem if (null eq e.prefix) && + Html5Constants.nonReplaceable_?(e.label) => { writer.append('<') writer.append(e.label) writeAttributes(e.attributes, writer) writer.append(">") e.child match { - case null => + case null => case seq => seq.foreach { case Text(str) => writer.append(str) case pc: PCData => { @@ -245,15 +252,15 @@ trait Html5Writer { writer.append(e.label) writer.append('>') } - - case e: Elem if (null eq e.prefix) && - Html5Constants.voidTag_?(e.label) => { + + case e: Elem if (null eq e.prefix) && + Html5Constants.voidTag_?(e.label) => { writer.append('<') writer.append(e.label) writeAttributes(e.attributes, writer) writer.append(">") } - + /* case e: Elem if ((e.child eq null) || e.child.isEmpty) => { @@ -266,7 +273,7 @@ trait Html5Writer { writeAttributes(e.attributes, writer) writer.append(" />") }*/ - + case e: Elem => { writer.append('<') if (null ne e.prefix) { @@ -285,7 +292,7 @@ trait Html5Writer { writer.append(e.label) writer.append('>') } - + case _ => // dunno what it is, but ignore it } } @@ -293,20 +300,20 @@ trait Html5Writer { object Html5Constants { val voidTags: Set[String] = Set("area", - "base", - "br", - "col", - "command", - "embed", - "hr", - "img", - "input", - "keygen", - "link", - "meta", - "param", - "source", - "wbr") + "base", + "br", + "col", + "command", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "wbr") /** * Is the tag a void tag? @@ -318,14 +325,14 @@ object Html5Constants { */ def nonReplaceable_?(t: String): Boolean = (t equalsIgnoreCase "script") || - (t equalsIgnoreCase "style") + (t equalsIgnoreCase "style") } /** * A utility that supports parsing of HTML5 file. * The Parser hooks up nu.validator.htmlparser - * to + * to */ trait Html5Parser { /** @@ -344,7 +351,7 @@ trait Html5Parser { /* override def createNode (pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: List[Node]) : Elem = { if (pre == "lift" && label == "head") { - super.createNode(null, label, attrs, scope, children) + super.createNode(null, label, attrs, scope, children) } else { super.createNode(pre, label, attrs, scope, children) } @@ -356,9 +363,9 @@ trait Html5Parser { if (text.length() > 0) { hStack.push(createText(text)) } - } - buffer.setLength(0) - } + } + buffer.setLength(0) + } } saxer.scopeStack.push(TopScope) @@ -368,11 +375,11 @@ trait Html5Parser { hp.parse(is) saxer.scopeStack.pop - + in.close() saxer.rootElem match { case null => Empty - case e: Elem => + case e: Elem => AutoInsertedBody.unapply(e) match { case Some(x) => Full(x) case _ => Full(e) @@ -383,40 +390,40 @@ trait Html5Parser { } private object AutoInsertedBody { - def checkHead(n: Node): Boolean = + def checkHead(n: Node): Boolean = n match { case e: Elem => { e.label == "head" && e.prefix == null && - e.attributes == Null && - e.child.length == 0 + e.attributes == Null && + e.child.length == 0 } case _ => false } - - def checkBody(n: Node): Boolean = + + def checkBody(n: Node): Boolean = n match { case e: Elem => { e.label == "body" && e.prefix == null && - e.attributes == Null && - e.child.length >= 1 && - e.child(0).isInstanceOf[Elem] + e.attributes == Null && + e.child.length >= 1 && + e.child(0).isInstanceOf[Elem] } case _ => false } - + def unapply(n: Node): Option[Elem] = n match { case e: Elem => { if (e.label == "html" && e.prefix == null && - e.attributes == Null && - e.child.length == 2 && - checkHead(e.child(0)) && - checkBody(e.child(1))) { - Some(e.child(1).asInstanceOf[Elem].child(0).asInstanceOf[Elem]) - } else { - None - } + e.attributes == Null && + e.child.length == 2 && + checkHead(e.child(0)) && + checkBody(e.child(1))) { + Some(e.child(1).asInstanceOf[Elem].child(0).asInstanceOf[Elem]) + } else { + None + } } - + case _ => None } } @@ -426,7 +433,6 @@ trait Html5Parser { * will be returned on successful parsing, otherwise * a Failure. */ - def parse(str: String): Box[Elem] = + def parse(str: String): Box[Elem] = parse(new ByteArrayInputStream(str.getBytes("UTF-8"))) } - From b49f5399d3fbed93032d4e01cfd1b748ed5a59cf Mon Sep 17 00:00:00 2001 From: David Pollak Date: Tue, 19 Feb 2013 12:38:33 -0800 Subject: [PATCH 007/115] Simply Lift --- docs/simply_lift.md | 547 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 docs/simply_lift.md diff --git a/docs/simply_lift.md b/docs/simply_lift.md new file mode 100644 index 0000000000..844a3f9488 --- /dev/null +++ b/docs/simply_lift.md @@ -0,0 +1,547 @@ +# Simply Lift + +> David Pollak +> February 19, 2013 +>
+> Copyright © 2010-2013 by David Pollak +> Creative Commons License +>
+> Simply Lift by http://simply.liftweb.net is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
Based on a work at https://github.com/lift/framework. + + +## The Lift Web Framework + +### Introduction + +The Lift Web Framework provides web application developers tools to make writing security, interacting, scalable web applications easier than with any other web framework. After reading Part I of this book, you should understand Lift's core concepts and be able to write Lift applications. But with anything, practice is important. I have been writing Lift and Scala for 6 years, and even I learn new things about the language and the framework on a weekly basis. Please consider Lift an path and an exploration, rather than an end point. + +“Yo, David, stop yer yappin'. I'm coming from Rails|Spring|Struts|Django and I want to get started super fast with Lift.” See From MVC. + +Lift is built on top of the [Scala](http://scala-lang.org) programming language. Scala runs on the [Java Virtual Machine](http://www.oracle.com/technetwork/java/index.html). Lift applications are typically packaged as [http://en.wikipedia.org/wiki/WAR_(Sun_file_format)||WAR] files and run as a [http://www.oracle.com/technetwork/java/index-jsp-135475.html||J/EE Servlets] or Servlet Filters. This book will provide you with the core concepts you need to successfully write Lift web applications. The book assumes knowledge of Servlets and Servlet containers, the Scala Language (Chapters 1-6 of [http://apress.com/book/view/9781430219897||Beginning Scala] gives you a good grounding in the language), build tools, program editors, web development including HTML and JavaScript, etc. Further, this book will not explore persistence. Lift has additional modules for persisting to relational and non-relational data stores. Lift doesn't distinguish as to how an object is materialized into the address space... Lift can treat any object any old way you want. There are many resources (including [http://exploring.liftweb.net/||Exploring Lift]) that cover ways to persist data from a JVM. + +Lift is different from most web frameworks and it's likely that Lift's differences will present a challenge and a friction if you are familiar with the MVCMVC school of web frameworksThis includes Ruby on Rails, Struts, Java Server Faces, Django, TurboGears, etc.. But Lift is different and Lift's differences give you more power to create interactive applications. Lift's differences lead to more concise web applications. Lift's differences result in more secure and scalable applications. Lift's differences let you be more productive and make maintaining applications easier for the future you or whoever is writing your applications. Please relax and work to understand Lift's differences... and see how you can make best use of Lift's features to build your web applications. + +Lift creates abstractions that allow easier expression of business logic and then maps those abstractions to HTTP and HTML. This approach differs from traditional web frameworks which build abstractions on top of HTTP and HTML and require the developer to bridge between common business logic patterns and the underlying protocol. The difference means that you spend more time thinking about your application and less time thinking about the plumbing. + +I am a “concept learner.” I learn concepts and then apply them over and over again as situations come up. This book focuses a lot on the concepts. If you're a concept learner and like my stream on conciousness style, this book will likely suit you well. On the other hand, it may not. + +Up to date versions of this book are available in PDF form at http://simply.liftweb.net/Simply_Lift.pdf. The source code for this book is available at [https://github.com/dpp/simply_lift||https://github.com/dpp/simply_lift]. + +If you've got questions, feedback, or improvements to this document, please join the conversation on the [http://groups.google.com/group/liftweb||Lift Google Group]. + +I'm a “roll up your sleaves and get your hands dirty with code” kinda guy... so let's build a simple Chat application in Lift. This application will allow us to demonstrate some of Lift's core features as well as giving a “smack in the face” demonstration of how Lift is different. + +### The ubiquitous Chat app + +Writing a multi-user chat application in Lift is super-simple and illustrates many of Lift's core concepts. + +The Source Code can be found at [https://github.com/dpp/simply_lift/tree/master/chat]. + +#### The View + +When writing a Lift app, it's often best to start off with the user interface... build what the user will see and then add behavior to the HTML page. So, let's look at the Lift template that will make up our chat application. + + + +It's a valid HTML page, but there are some hinky looking class attributes. The first one is . The class in this case says “the actual page content is contained by the element with id='main'.” This allows you to have valid HTML pages for each of your templates, but dynamically add “chrome” around the content based on one or more chrome templates. + +Let's look at the
. It's got a funky class as well: lift:surround?with=default;at=content. This class invokes a snippet which surrounds the
with the default template and inserts the
and its children at the element with id “content” in the default template. Or, it wraps the default chrome around the
. For more on snippets, see [sec:Snippets]. + +Next, we define how we associate dynamic behavior with the list of chat elements:
. The “comet” snippet looks for a class named Chat that extends CometActor and enables the mechanics of pushing content from the CometActor to the browser when the state of the CometActor changes. + +#### The Chat Comet component + +The [http://en.wikipedia.org/wiki/Actor_model||Actor Model] provides state in functional languages include Erlang. Lift has an Actor library and LiftActors (see [sec:LiftActor]) provides a powerful state and concurrency model. This may all seem abstract, so let's look at the Chat class. + + + +The Chat component has private state, registers with the ChatServer, handles incoming messages and can render itself. Let's look at each of those pieces. + +The private state, like any private state in prototypical object oriented code, is the state that defines the object's behavior. + +registerWith is a method that defines what component to register the Chat component with. Registration is a part of the Listener (or [http://en.wikipedia.org/wiki/Observer_pattern||Observer]) pattern. We'll look at the definition of the ChatServer in a minute. + +The lowPriority method defines how to process incoming messages. In this case, we're Pattern Matching (see [sec:Pattern-Matching]) the incoming message and if it's a Vector[String], then we perform the action of setting our local state to the Vector and re-rendering the component. The re-rendering will force the changes out to any browser that is displaying the component. + +We define how to render the component by defining the CSS to match and the replacement (See [sec:CSS-Selector-Transforms]). We match all the
  • tags of the template and for each message, create an
  • tag with the child nodes set to the message. Additionally, we clear all the elements that have the clearable in the class attribute. + +That's it for the Chat CometActor component. + +#### The ChatServer + +The ChatServer code is: + + + +The ChatServer is defined as an object rather than a class. This makes it a singleton which can be referenced by the name ChatServer anywhere in the application. Scala's singletons differ from Java's static in that the singleton is an instance of an object and that instance can be passed around like any other instance. This is why we can return the ChatServer instance from the registerWith method in that Chat component. + +The ChatServer has private state, a Vector[String] representing the list of chat messages. Note that Scala's type inferencer infers the type of msgs so you do not have to explicitly define it. + +The createUpdate method generates an update to send to listeners. This update is sent when a listener registers with the ChatServer or when the updateListeners() method is invoked. + +Finally, the lowPriority method defines the messages that this component can handle. If the ChatServer receives a String as a message, it appends the String to the Vector of messages and updates listeners. + +#### User Input + +Let's go back to the view and see how the behavior is defined for adding lines to the chat. + +
    defines an input form and the form.ajax snippet turns a form into an Ajax (see [sec:Ajax]) form that will be submitted back to the server without causing a full page load. + +Next, we define the input form element: . It's a plain old input form, but we've told Lift to modify the 's behavior by calling the ChatIn snippet. + +#### Chat In + +The ChatIn snippet (See [sec:Snippets]) is defined as: + + + +The code is very simple. The snippet is defined as a method that associates a function with form element submission, onSubmit. When the element is submitted, be that normal form submission, Ajax, or whatever, the function is applied to the value of the form. In English, when the user submits the form, the function is called with the user's input. + +The function sends the input as a message to the ChatServer and returns JavaScript that sets the value of the input box to a blank string. + +#### Running it + +Running the application is easy. Make sure you've got Java 1.6 or better installed on your machine. Change directories into the chat directory and type sbt update ~jetty-run. The Simple Build Tool will download all necessary dependencies, compile the program and run it. + +You can point a couple of browsers to http://localhost:8080 and start chatting. + +Oh, and for fun, try entering From 9285b3553fcdcea64bf6015ab49a59c9d90526bb Mon Sep 17 00:00:00 2001 From: "Diego Medina (fmpwizard)" Date: Mon, 29 Jul 2013 01:38:01 -0400 Subject: [PATCH 112/115] Fixed the ToHeadUsages tests related to using the new html5 parser --- .../net/liftweb/webapptest/ToHeadUsages.scala | 7 ++++--- web/webkit/src/test/webapp/basicDiv.html | 8 ++++---- web/webkit/src/test/webapp/deferred.html | 10 +++++----- .../src/test/webapp/htmlFragmentWithHead.html | 11 +++++------ .../src/test/webapp/htmlSnippetWithHead.html | 2 +- .../test/webapp/templates-hidden/default.html | 16 +++++++++------- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/web/webkit/src/test/scala/net/liftweb/webapptest/ToHeadUsages.scala b/web/webkit/src/test/scala/net/liftweb/webapptest/ToHeadUsages.scala index 10fa9b0fe3..ec7a0370ba 100644 --- a/web/webkit/src/test/scala/net/liftweb/webapptest/ToHeadUsages.scala +++ b/web/webkit/src/test/scala/net/liftweb/webapptest/ToHeadUsages.scala @@ -55,7 +55,8 @@ object ToHeadUsages extends Specification { "merge from html fragment" in { jetty.browse( "/htmlFragmentWithHead", html => - html.getElementByXPath("/html/head/script[@id='fromFrag']") must not(beNull when jetty.running)) + html.getElementByXPath("/html/head/script[@id='fromFrag']") must not(beNull when jetty.running) + ) } "merge from html fragment does not include head element in body" in { @@ -136,8 +137,8 @@ object ToHeadUsages extends Specification { (idx >= 0) must_== true } ) - } - */ + }*/ + } "deferred snippets" should { diff --git a/web/webkit/src/test/webapp/basicDiv.html b/web/webkit/src/test/webapp/basicDiv.html index d0bd06eb84..29ac577050 100644 --- a/web/webkit/src/test/webapp/basicDiv.html +++ b/web/webkit/src/test/webapp/basicDiv.html @@ -1,13 +1,13 @@ - +
    bat
    -
    +
    -
    +
    - +
    diff --git a/web/webkit/src/test/webapp/deferred.html b/web/webkit/src/test/webapp/deferred.html index 51703ddf60..57c4cd617b 100644 --- a/web/webkit/src/test/webapp/deferred.html +++ b/web/webkit/src/test/webapp/deferred.html @@ -1,10 +1,10 @@ - - - +
    +
    +
    - +
    - +
    diff --git a/web/webkit/src/test/webapp/htmlFragmentWithHead.html b/web/webkit/src/test/webapp/htmlFragmentWithHead.html index ec053346db..3423b1a65e 100644 --- a/web/webkit/src/test/webapp/htmlFragmentWithHead.html +++ b/web/webkit/src/test/webapp/htmlFragmentWithHead.html @@ -1,7 +1,6 @@ - - - - +

    Welcome to your project!

    - - + + + +
    diff --git a/web/webkit/src/test/webapp/htmlSnippetWithHead.html b/web/webkit/src/test/webapp/htmlSnippetWithHead.html index 71577c1c25..4e5944771f 100644 --- a/web/webkit/src/test/webapp/htmlSnippetWithHead.html +++ b/web/webkit/src/test/webapp/htmlSnippetWithHead.html @@ -1,5 +1,5 @@

    Welcome to your project!

    -

    +

    diff --git a/web/webkit/src/test/webapp/templates-hidden/default.html b/web/webkit/src/test/webapp/templates-hidden/default.html index 532a640123..00f3c5f922 100644 --- a/web/webkit/src/test/webapp/templates-hidden/default.html +++ b/web/webkit/src/test/webapp/templates-hidden/default.html @@ -1,15 +1,17 @@ + - - - - + + + + Lift webapptest - - - + + +
    The main content will get bound here
    + From 58247b87ff3066e6fc5958f8c8b3f83d3451cb22 Mon Sep 17 00:00:00 2001 From: David Pollak Date: Mon, 9 Jan 2012 13:56:37 -0800 Subject: [PATCH 113/115] Closes #1126. Swallows an inner for Menu.item --- .../src/main/scala/net/liftweb/builtin/snippet/Menu.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala index 001bea2e6c..ebcb48ce85 100644 --- a/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala +++ b/web/webkit/src/main/scala/net/liftweb/builtin/snippet/Menu.scala @@ -396,15 +396,21 @@ object Menu extends DispatchSnippet { * set the "donthide" attribute on the tag to force it to show text only (same text as normal, * but not in an anchor tag)

    * + * *

    Alternatively, you can set the "linkToSelf" attribute to "true" to force a link. You * can specify your own link text with the tag's contents. Note that case is significant, so * make sure you specify "linkToSelf" and not "linktoself".

    * */ - def item(text: NodeSeq): NodeSeq = { + def item(_text: NodeSeq): NodeSeq = { val donthide = S.attr("donthide").map(Helpers.toBoolean) openOr false val linkToSelf = (S.attr("linkToSelf") or S.attr("linktoself")).map(Helpers.toBoolean) openOr false + val text = ("a" #> ((n: NodeSeq) => n match { + case e: Elem => e.child + case xs => xs + })).apply(_text) + for { name <- S.attr("name").toList } yield { From d8d6201247ca44eeeafdc9ed515f109756a58bc9 Mon Sep 17 00:00:00 2001 From: "Diego Medina (fmpwizard)" Date: Thu, 8 Aug 2013 00:02:47 -0400 Subject: [PATCH 114/115] We don't want to use eventually here, as it will keep sending an `Add(44)` message to the actor --- core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala b/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala index 4957e4aa49..a0ca287c84 100644 --- a/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala +++ b/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala @@ -39,6 +39,7 @@ class ActorSpec extends Specification { } private def commonFeatures(actor: LiftActor) = { + sequential "allow setting and getting of a value" in { val a = actor @@ -57,7 +58,7 @@ class ActorSpec extends Specification { "allow adding of a value" in { val a = actor a ! Set(33) - (a !< Add(44)).get(50) must be_==(Full(Answer(77))).eventually(900, 100.milliseconds) + (a !< Add(44)).get(500) must be_==(Full(Answer(77))) } "allow subtracting of a value" in { From ce58aff36a4284d6fc07339bc6b83014751f3d36 Mon Sep 17 00:00:00 2001 From: "Diego Medina (fmpwizard)" Date: Thu, 8 Aug 2013 22:11:01 -0400 Subject: [PATCH 115/115] Fixed #1484 - Replace console.log for lift_defaultLogError - Lift 3.0 --- .../scala/net/liftweb/http/js/ScriptRenderer.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala b/web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala index 644ce81505..dbe3515ec9 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala @@ -91,15 +91,15 @@ object ScriptRenderer { '_failed': false, processMsg: function(evt) {if (this._done || this._failed) return; this._events.push(evt); - for (var v in this._eventFuncs) {try {this._eventFuncs[v](evt);} catch (e) {console.log(e);}}; + for (var v in this._eventFuncs) {try {this._eventFuncs[v](evt);} catch (e) {""" + (LiftRules.jsLogFunc.map(_(JsVar("e")).toJsCmd) openOr "") + """}}; if (evt.done) {this.doneMsg();} else if (evt.success) {this.successMsg(evt.success);} else if (evt.failure) {this.failMsg(evt.failure);}}, successMsg: function(value) {if (this._done || this._failed) return; this._values.push(value); for (var f in this._valueFuncs) {this._valueFuncs[f](value);}}, failMsg: function(msg) {if (this._done || this._failed) return; liftAjax._removeIt(this.guid); this._failed = true; this._failMsg = msg; for (var f in this._failureFuncs) {this._failureFuncs[f](msg);}}, doneMsg: function() {if (this._done || this._failed) return; liftAjax._removeIt(this.guid); this._done = true; for (var f in this._doneFuncs) {this._doneFuncs[f]();}}, - then: function(f) {this._valueFuncs.push(f); for (var v in this._values) {try {f(this._values[v]);} catch (e) {console.log(e);}} return this;}, - fail: function(f) {this._failureFuncs.push(f); if (this._failed) {try {f(this._failMsg);} catch (e) {console.log(e);}}; return this;}, - done: function(f) {this._doneFuncs.push(f); if (this._done) {try {f();} catch (e) {console.log(e);}} return this;}, - onEvent: function(f) {this._eventFuncs.push(f); for (var v in this._events) {try {f(this._events[v]);} catch (e) {console.log(e);}}; return this;}, + then: function(f) {this._valueFuncs.push(f); for (var v in this._values) {try {f(this._values[v]);} catch (e) {""" + (LiftRules.jsLogFunc.map(_(JsVar("e")).toJsCmd) openOr "") + """;}} return this;}, + fail: function(f) {this._failureFuncs.push(f); if (this._failed) {try {f(this._failMsg);} catch (e) {""" + (LiftRules.jsLogFunc.map(_(JsVar("e")).toJsCmd) openOr "") + """;}}; return this;}, + done: function(f) {this._doneFuncs.push(f); if (this._done) {try {f();} catch (e) {""" + (LiftRules.jsLogFunc.map(_(JsVar("e")).toJsCmd) openOr "") + """;}} return this;}, + onEvent: function(f) {this._eventFuncs.push(f); for (var v in this._events) {try {f(this._events[v]);} catch (e) {""" + (LiftRules.jsLogFunc.map(_(JsVar("e")).toJsCmd) openOr "") + """;}}; return this;}, map: function(f) {var ret = new liftAjax.Promise(); this.done(function() {ret.doneMsg();}); this.fail(function (m) {ret.failMsg(m);}); this.then(function (v) {ret.successMsg(f(v));}); return ret;} }; },