diff --git a/.gitignore b/.gitignore index 569cd5f51b..e1b478d038 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ project/plugins/project # Pax Runner (for easy OSGi launching) runner +# ignore the sbt launcher +project/sbt-launch* diff --git a/build.sbt b/build.sbt index d19cca3be5..a91f67ed23 100644 --- a/build.sbt +++ b/build.sbt @@ -14,9 +14,9 @@ organizationName in ThisBuild := "WorldWide Conferencing, LLC" scalaVersion in ThisBuild := "2.10.4" -crossScalaVersions in ThisBuild := Seq("2.10.0") +crossScalaVersions in ThisBuild := Seq("2.10.4") -libraryDependencies in ThisBuild <++= scalaVersion {sv => Seq(specs2(sv), scalacheck) } +libraryDependencies in ThisBuild <++= scalaVersion {sv => Seq(specs2(sv), scalacheck, scalatest(sv)) } // Settings for Sonatype compliance pomIncludeRepository in ThisBuild := { _ => false } @@ -25,8 +25,13 @@ publishTo in ThisBuild <<= isSnapshot(if (_) Some(Opts.resolver.sonat scmInfo in ThisBuild := Some(ScmInfo(url("https://github.com/lift/framework"), "scm:git:https://github.com/lift/framework.git")) -pomExtra in ThisBuild ~= (_ ++ {Developers.toXml}) +pomExtra in ThisBuild := Developers.toXml credentials in ThisBuild <+= state map { s => Credentials(BuildPaths.getGlobalSettingsDirectory(s, BuildPaths.getGlobalBase(s)) / ".credentials") } initialize <<= (name, version, scalaVersion) apply printLogo + +resolvers in ThisBuild ++= Seq( + "snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", + "releases" at "https://oss.sonatype.org/content/repositories/releases" +) diff --git a/contributors.md b/contributors.md index bf743bd7f3..0760d36338 100644 --- a/contributors.md +++ b/contributors.md @@ -209,3 +209,15 @@ Robert Freytag ### Email: ### robertfreytag+lift at gmail .. com + +### Name: ### +Mikhail Limansky + +### Email: ### +mike.limansky at gmail dot com + +### Name: ### +Aleksey Izmailov + +### Email: ### +izmailoff at gmail dot com diff --git a/core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala b/core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala index 5e505e2856..e6e2604071 100644 --- a/core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala +++ b/core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala @@ -19,13 +19,14 @@ package common import xml.{NodeSeq, Text} +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification /** * System under specification for Conversions. */ -class ConversionsSpec extends Specification { +class ConversionsSpec extends Specification with XmlMatchers { "A StringOrNodeSeq" should { diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala index 751d0fb5a8..bb7d486e04 100644 --- a/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala +++ b/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala @@ -28,23 +28,25 @@ import org.scalatest.junit.JUnitRunner * Tests the Line Tokenizer that prepares input for parsing. */ @RunWith(classOf[JUnitRunner]) -class LineTokenizerTest extends LineTokenizer with FlatSpec with ShouldMatchers { +class LineTokenizerTest extends FlatSpec with ShouldMatchers { + + val tokenizer = new LineTokenizer "The LineTokenizer" should "split input lines correctly" in { - splitLines("line1\nline2\n") should equal (List("line1", "line2")) - splitLines("line1\nline2 no nl") should equal (List("line1", "line2 no nl")) - splitLines("test1\n\ntest2\n") should equal (List("test1", "", "test2")) - splitLines("test1\n\ntest2\n\n") should equal (List("test1", "", "test2")) - splitLines("\n\n") should equal (Nil) - splitLines("\n") should equal (Nil) - splitLines("") should equal (List("")) + tokenizer.splitLines("line1\nline2\n") should equal (List("line1", "line2")) + tokenizer.splitLines("line1\nline2 no nl") should equal (List("line1", "line2 no nl")) + tokenizer.splitLines("test1\n\ntest2\n") should equal (List("test1", "", "test2")) + tokenizer.splitLines("test1\n\ntest2\n\n") should equal (List("test1", "", "test2")) + tokenizer.splitLines("\n\n") should equal (Nil) + tokenizer.splitLines("\n") should equal (Nil) + tokenizer.splitLines("") should equal (List("")) } it should "preprocess the input correctly" in { - tokenize("[foo]: http://example.com/ \"Optional Title Here\"") should equal( + tokenizer.tokenize("[foo]: http://example.com/ \"Optional Title Here\"") should equal( (new MarkdownLineReader(List(), Map( "foo"->new LinkDefinition("foo", "http://example.com/", Some("Optional Title Here")) )) ) ) - tokenize( + tokenizer.tokenize( """[Baz]: http://foo.bar 'Title next line' some text @@ -68,8 +70,8 @@ new OtherLine("more text") it should "parse different line types" in { def p(line:String) = { - lineToken(new LineReader(Seq(line))) match { - case Success(result, _) => result + tokenizer.lineToken(new LineReader(Seq(line))) match { + case tokenizer.Success(result, _) => result case _ => fail("Line tokenization failed.") } } diff --git a/core/util/src/main/scala/net/liftweb/util/Mailer.scala b/core/util/src/main/scala/net/liftweb/util/Mailer.scala index e17742a199..9fa18d0e69 100644 --- a/core/util/src/main/scala/net/liftweb/util/Mailer.scala +++ b/core/util/src/main/scala/net/liftweb/util/Mailer.scala @@ -286,38 +286,69 @@ trait Mailer extends SimpleInjector { */ protected def buildMailBody(tab: MailBodyType): BodyPart = { val bp = new MimeBodyPart - tab match { - case PlainMailBodyType(txt) => bp.setText(txt, "UTF-8") - case PlainPlusBodyType(txt, charset) => bp.setText(txt, charset) - case XHTMLMailBodyType(html) => bp.setContent(encodeHtmlBodyPart(html), "text/html; charset=" + charSet) - - case XHTMLPlusImages(html, img@_*) => - val html_mp = new MimeMultipart("related") - val bp2 = new MimeBodyPart - bp2.setContent(encodeHtmlBodyPart(html), "text/html; charset=" + charSet) - html_mp.addBodyPart(bp2) - img.foreach { - i => - val rel_bpi = new MimeBodyPart - rel_bpi.setFileName(i.name) - rel_bpi.setContentID(i.name) - rel_bpi.setDisposition(if (!i.attachment) "inline" else "attachment") - rel_bpi.setDataHandler(new javax.activation.DataHandler(new javax.activation.DataSource { - def getContentType = i.mimeType - - def getInputStream = new java.io.ByteArrayInputStream(i.bytes) - - def getName = i.name - - def getOutputStream = throw new java.io.IOException("Unable to write to item") - })) - html_mp.addBodyPart(rel_bpi) - } - bp.setContent(html_mp) + + tab match { + case PlainMailBodyType(txt) => + bp.setText(txt, "UTF-8") + + case PlainPlusBodyType(txt, charset) => + bp.setText(txt, charset) + + case XHTMLMailBodyType(html) => + bp.setContent(encodeHtmlBodyPart(html), "text/html; charset=" + charSet) + + case XHTMLPlusImages(html, img@_*) => + val (attachments, images) = img.partition(_.attachment) + val relatedMultipart = new MimeMultipart("related") + + val htmlBodyPart = new MimeBodyPart + htmlBodyPart.setContent(encodeHtmlBodyPart(html), "text/html; charset=" + charSet) + relatedMultipart.addBodyPart(htmlBodyPart) + + images.foreach { image => + relatedMultipart.addBodyPart(buildAttachment(image)) + } + + if (attachments.isEmpty) { + bp.setContent(relatedMultipart) + } else { + // Some old versions of Exchange server will not behave correclty without + // a mixed multipart wrapping file attachments. This appears to be linked to + // specific versions of Exchange and Outlook. See the discussion at + // https://github.com/lift/framework/pull/1569 for more details. + val mixedMultipart = new MimeMultipart("mixed") + + val relatedMultipartBodypart = new MimeBodyPart + relatedMultipartBodypart.setContent(relatedMultipart) + mixedMultipart.addBodyPart(relatedMultipartBodypart) + + attachments.foreach { attachment => + mixedMultipart.addBodyPart(buildAttachment(attachment)) } + + bp.setContent(mixedMultipart) + } + } + bp } + private def buildAttachment(holder: PlusImageHolder) = { + val part = new MimeBodyPart + + part.setFileName(holder.name) + part.setContentID(holder.name) + part.setDisposition(if (holder.attachment) Part.ATTACHMENT else Part.INLINE) + part.setDataHandler(new javax.activation.DataHandler(new javax.activation.DataSource { + def getContentType = holder.mimeType + def getInputStream = new java.io.ByteArrayInputStream(holder.bytes) + def getName = holder.name + def getOutputStream = throw new java.io.IOException("Unable to write to item") + })) + + part + } + /** * Asynchronously send an email. diff --git a/core/util/src/main/scala/net/liftweb/util/Maker.scala b/core/util/src/main/scala/net/liftweb/util/Maker.scala index 37dc05be1e..ce5c2b0eb1 100644 --- a/core/util/src/main/scala/net/liftweb/util/Maker.scala +++ b/core/util/src/main/scala/net/liftweb/util/Maker.scala @@ -131,12 +131,21 @@ trait StackableMaker[T] extends Maker[T] { case x => x } + /** + * Changes to the stack of Makers made by this method are thread-local! + */ def doWith[F](value: T)(f: => F): F = doWith(PValueHolder(Maker(value)))(f) + /** + * Changes to the stack of Makers made by this method are thread-local! + */ def doWith[F](vFunc: () => T)(f: => F): F = doWith(PValueHolder(Maker(vFunc)))(f) + /** + * Changes to the stack of Makers made by this method are thread-local! + */ def doWith[F](addl: PValueHolder[Maker[T]])(f: => F): F = { val old = _stack.get() _stack.set(addl :: stack) diff --git a/core/util/src/main/scala/net/liftweb/util/Position.scala b/core/util/src/main/scala/net/liftweb/util/Position.scala new file mode 100644 index 0000000000..f3ac82021f --- /dev/null +++ b/core/util/src/main/scala/net/liftweb/util/Position.scala @@ -0,0 +1,85 @@ +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala +package io + +/** The object Position provides convenience methods to encode + * line and column number in one single integer. The encoded line + * (column) numbers range from 0 to `LINE_MASK` (`COLUMN_MASK`), + * where `0` indicates that the line (column) is undefined and + * `1` represents the first line (column). + * + * Line (Column) numbers greater than `LINE_MASK` (`COLUMN_MASK`) are + * replaced by `LINE_MASK` (`COLUMN_MASK`). Furthermore, if the encoded + * line number is `LINE_MASK`, the column number is always set to 0. + * + * The following properties hold: + * + * the undefined position is 0: `encode(0,0) == 0` + * encodings are non-negative : `encode(line,column) >= 0` + * position order is preserved: + * {{{ + * (line1 <= line2) || (line1 == line2 && column1 <= column2) + * }}} + * implies + * {{{ + * encode(line1,column1) <= encode(line2,column2) + * }}} + * @author Burak Emir (translated from work by Matthias Zenger and others) + */ + +/** + * This was made private in scala 2.11.0 but there is no alternative for us to use, so here, copy/paste for now. + */ + +@deprecated("This class will be removed.", "2.10.0") +abstract class Position { + /** Definable behavior for overflow conditions. + */ + def checkInput(line: Int, column: Int): Unit + + /** Number of bits used to encode the line number */ + final val LINE_BITS = 20 + /** Number of bits used to encode the column number */ + final val COLUMN_BITS = 31 - LINE_BITS // no negatives => 31 + /** Mask to decode the line number */ + final val LINE_MASK = (1 << LINE_BITS) - 1 + /** Mask to decode the column number */ + final val COLUMN_MASK = (1 << COLUMN_BITS) - 1 + + /** Encodes a position into a single integer. */ + final def encode(line: Int, column: Int): Int = { + checkInput(line, column) + + if (line >= LINE_MASK) + LINE_MASK << COLUMN_BITS + else + (line << COLUMN_BITS) | scala.math.min(COLUMN_MASK, column) + } + + /** Returns the line number of the encoded position. */ + final def line(pos: Int): Int = (pos >> COLUMN_BITS) & LINE_MASK + + /** Returns the column number of the encoded position. */ + final def column(pos: Int): Int = pos & COLUMN_MASK + + /** Returns a string representation of the encoded position. */ + def toString(pos: Int): String = line(pos) + ":" + column(pos) +} + +object Position extends Position { + def checkInput(line: Int, column: Int) { + if (line < 0) + throw new IllegalArgumentException(line + " < 0") + if ((line == 0) && (column != 0)) + throw new IllegalArgumentException(line + "," + column + " not allowed") + if (column < 0) + throw new IllegalArgumentException(line + "," + column + " not allowed") + } +} \ No newline at end of file diff --git a/core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala b/core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala index 2b8aed6bd5..04ab8f8466 100644 --- a/core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala @@ -165,7 +165,7 @@ object BasicTypesHelpersSpec extends Specification with DataTables { "put a guard around a partial function" in { val pf1: PartialFunction[String, Unit] = { - case s if s.startsWith("s") => true + case s if s.startsWith("s") => } val pf2: PartialFunction[String, Boolean] = { diff --git a/core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala b/core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala index 530a3e12eb..6248f33833 100644 --- a/core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala @@ -21,13 +21,14 @@ import java.util.Locale import xml.NodeSeq +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification /** * Systems under specification for BundleBuilder. */ -object BundleBuilderSpec extends Specification { +object BundleBuilderSpec extends Specification with XmlMatchers { "BundleBuilder Specification".title "BundleBuilder" should { diff --git a/core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala b/core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala index d468ab0535..bf52c5f48b 100644 --- a/core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala @@ -106,6 +106,8 @@ object CombParserHelpersSpec extends Specification with ScalaCheck { result.get.toString must_== "()" result.next.atEnd must beTrue } + + success } val parserA = elem("a", (c: Char) => c == 'a') val parserB = elem("b", (c: Char) => c == 'b') diff --git a/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala b/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala index 3fb2e0519e..e6c1bd8e2c 100644 --- a/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala @@ -17,6 +17,7 @@ package net.liftweb package util +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ @@ -27,7 +28,7 @@ import Helpers._ /** * Systems under specification for CSS Selector. */ -object CssSelectorSpec extends Specification { +object CssSelectorSpec extends Specification with XmlMatchers { "CSS Selector Specification".title "CssSelector" should { @@ -227,7 +228,7 @@ object CssSelectorSpec extends Specification { } -object CssBindHelpersSpec extends Specification { +object CssBindHelpersSpec extends Specification with XmlMatchers { "css bind helpers" should { "clear clearable" in { diff --git a/core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala b/core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala index 4a8f28d04d..661b3e90b9 100644 --- a/core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala @@ -65,10 +65,7 @@ object Html5ParserSpec extends Specification with PendingUntilFixed with Html5Pa "fail to parse invalid page type3" in { val parsed = parse(page3) - parsed match { - case _: Failure => true must_== true - case _ => failure("succeeded parsing invalid page") - } + parsed must beAnInstanceOf[Failure] }.pendingUntilFixed case _ => diff --git a/core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala b/core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala index 7416aa1968..1affe9b24c 100644 --- a/core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala @@ -17,6 +17,7 @@ package net.liftweb package util +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import HttpHelpers._ @@ -25,7 +26,7 @@ import HttpHelpers._ /** * Systems under specification for HttpHelpers. */ -object HttpHelpersSpec extends Specification with HttpHelpers with ListHelpers with StringHelpers { +object HttpHelpersSpec extends Specification with HttpHelpers with ListHelpers with StringHelpers with XmlMatchers { "HttpHelpers Specification".title "Http helpers" should { diff --git a/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala b/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala index b0525230d0..7766f80032 100644 --- a/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala @@ -23,7 +23,9 @@ import org.specs2.mutable.Specification import common._ -import Mailer._ +import Mailer.{From, To, Subject, PlainMailBodyType, XHTMLMailBodyType, XHTMLPlusImages, PlusImageHolder} + +import scala.io.Source trait MailerForTesting { def lastMessage_=(message: Box[MimeMessage]): Unit @@ -72,10 +74,7 @@ object MailerSpec extends Specification { ) } - msg.getContent match { - case s: String => true must_== true - case x => failure("The simple message has content type of " + x.getClass.getName) - } + msg.getContent must beAnInstanceOf[String] } "deliver multipart messages as multipart" in { @@ -89,10 +88,7 @@ object MailerSpec extends Specification { ) } - msg.getContent match { - case mp: MimeMultipart => true must_== true - case x => failure("The complex message has content type of " + x.getClass.getName) - } + msg.getContent must beAnInstanceOf[MimeMultipart] } "deliver rich messages as multipart" in { @@ -105,9 +101,33 @@ object MailerSpec extends Specification { ) } - msg.getContent match { - case mp: MimeMultipart => true must_== true - case x => failure("The complex message has content type of " + x.getClass.getName) + msg.getContent must beAnInstanceOf[MimeMultipart] + } + + "deliver emails with attachments as mixed multipart" in { + val attachmentBytes = Source.fromInputStream( + getClass.getClassLoader.getResourceAsStream("net/liftweb/util/Html5ParserSpec.page1.html") + ).map(_.toByte).toArray + val msg = doNewMessage { + sendMail( + From("sender@nowhere.com"), + Subject("This is a mixed email"), + To("recipient@nowhere.com"), + XHTMLPlusImages( + Here is some rich text , + PlusImageHolder("awesome.pdf", "text/html", attachmentBytes, true) + ) + ) + } + + msg.getContent must beLike { + case mp: MimeMultipart => + mp.getContentType.substring(0, 21) must_== "multipart/alternative" + + mp.getBodyPart(0).getContent must beLike { + case mp2: MimeMultipart => + mp2.getContentType.substring(0, 15) must_== "multipart/mixed" + } } } } diff --git a/core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala b/core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala index b3aa3168d7..2a40bd3cc7 100644 --- a/core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala @@ -17,13 +17,14 @@ package net.liftweb package util +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification /** * Systems under specification for PCDataXmlParser. */ -object PCDataXmlParserSpec extends Specification { +object PCDataXmlParserSpec extends Specification with XmlMatchers { "PCDataXmlParser Specification".title val data1 = """ diff --git a/core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala b/core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala index c15a7a16d3..b9640dc669 100644 --- a/core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala @@ -19,6 +19,7 @@ package util import xml.XML._ +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ @@ -29,7 +30,7 @@ import HeadHelper._ /** * Systems under specification for ToHead. */ -object ToHeadSpec extends Specification { +object ToHeadSpec extends Specification with XmlMatchers { "ToHead Specification".title "lift merger" should { @@ -39,14 +40,12 @@ object ToHeadSpec extends Specification { exp <- tryo(getClass.getResource("ToHeadSpec.expected1.html")).filter(_ ne null) } yield (act, exp) - susfiles match { + susfiles must beLike { case Full(sus) => val actual = load(sus._1) val expected = load(sus._2) mergeToHtmlHead(actual).toString.replaceAll("\\s", "") must_== (expected.toString.replaceAll("\\s", "")) - case _ => - failure("Failed loading test files") // TODO: Improve error message } } @@ -56,13 +55,11 @@ object ToHeadSpec extends Specification { exp <- tryo(getClass.getResource("ToHeadSpec.expected2.html")).filter(_ ne null) } yield (act, exp) - susfiles match { + susfiles must beLike { case Full(sus) => val actual = load(sus._1) val expected = load(sus._2) mergeToHtmlHead(actual) must ==/(expected) - case _ => - failure("Failed loading test files") // TODO: Improve error message } } @@ -72,14 +69,12 @@ object ToHeadSpec extends Specification { exp <- tryo(getClass.getResource("ToHeadSpec.expected3.html")).filter(_ ne null) } yield (act, exp) - susfiles match { + susfiles must beLike { case Full(sus) => val actual = load(sus._1) val expected = load(sus._2) mergeToHtmlHead(actual).toString.replaceAll("\\s", "") must_== (expected.toString.replaceAll("\\s", "")) - case _ => - failure("Failed loading test files") // TODO: Improve error message } } } diff --git a/core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala b/core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala index af86009105..f8b49dccac 100644 --- a/core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala @@ -17,13 +17,14 @@ package net.liftweb package util +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification /** * Systems under specification for VCardParser. */ -object VCardParserSpec extends Specification { +object VCardParserSpec extends Specification with XmlMatchers { "VCardParser Specification".title "VCard" should { @@ -41,7 +42,7 @@ object VCardParserSpec extends Specification { |END:VCARD""".stripMargin val list = VCardParser.parse(vcard) - list match { + list must beLike { case Left(l) => { import VCardParser._ l must_== @@ -56,7 +57,6 @@ object VCardParserSpec extends Specification { VCardEntry(VCardKey("TEL", List(("HOME", ""), ("VOICE", ""))), List("(404) 555-1212")), VCardEntry(VCardKey("END", List()), List("VCARD"))) } - case Right(r) => failure(r.toString) } } diff --git a/core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala b/core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala index 8597ae9bf2..2fa7b25ec7 100644 --- a/core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala @@ -21,13 +21,14 @@ import java.io.ByteArrayInputStream import xml.{Text, Unparsed} +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification /** * Systems under specification for XmlParser, specifically PCDataMarkupParser. */ -object XmlParserSpec extends Specification { +object XmlParserSpec extends Specification with XmlMatchers { "Xml Parser Specification".title "Multiple attributes with same name, but different namespace" should { diff --git a/liftsh b/liftsh index 5df3775650..ee36e3352f 100755 --- a/liftsh +++ b/liftsh @@ -1,5 +1,16 @@ #!/bin/sh +# Make sure to change the name of the launcher jar and the source when bumping sbt version +# so that the existence test below fails and we download the new jar. +SBT_LAUNCHER_PATH="project/sbt-launch-0.13.5.jar" +SBT_LAUNCHER_SOURCE="http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.5/sbt-launch.jar" + +# Download the sbt launcher on-the-fly if it's not already in the repository. +if test ! -f $SBT_LAUNCHER_PATH; then + echo "Downloading sbt launcher..." + curl -o ${SBT_LAUNCHER_PATH} ${SBT_LAUNCHER_SOURCE} +fi + # Load custom liftsh config if test -f ~/.liftsh.config; then . ~/.liftsh.config @@ -17,4 +28,4 @@ DEFAULT_OPTS="" cd `dirname $0` # Call with INTERNAL_OPTS followed by LIFTSH_OPTS (or DEFAULT_OPTS). java always takes the last option when duplicate. -exec java ${INTERNAL_OPTS} ${LIFTSH_OPTS:-${DEFAULT_OPTS}} -jar project/sbt-launch-0.12.1.jar "$@" +exec java ${INTERNAL_OPTS} ${LIFTSH_OPTS:-${DEFAULT_OPTS}} -jar ${SBT_LAUNCHER_PATH} "$@" diff --git a/liftsh.cmd b/liftsh.cmd index f80a55296f..c294d5005b 100644 --- a/liftsh.cmd +++ b/liftsh.cmd @@ -1,5 +1,10 @@ @echo off +set SBT_LAUNCHER_PATH="project\sbt-launch-0.13.5.jar" +set SBT_LAUNCHER_SOURCE="http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.5/sbt-launch.jar" + +if not exist %SBT_LAUNCHER_PATH% powershell -Command "(New-Object Net.WebClient).DownloadFile('%SBT_LAUNCHER_SOURCE%', '%SBT_LAUNCHER_PATH%')" + @REM Internal options, always specified set INTERNAL_OPTS=-Dfile.encoding=UTF-8 -Xmx768m -noverify -XX:ReservedCodeCacheSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=512m @@ -17,4 +22,4 @@ if "%LIFTSH_OPTS%"=="" ( ) @REM Call with INTERNAL_OPTS followed by LIFTSH_OPTS (or DEFAULT_OPTS). java always takes the last option when duplicate. -java %INTERNAL_OPTS% %LIFTSH_OPTS% -jar "%~dp0\project\sbt-launch-0.12.1.jar" %* +java %INTERNAL_OPTS% %LIFTSH_OPTS% -jar "%~dp0\%SBT_LAUNCHER_PATH%" %* diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/HasManyThrough.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/HasManyThrough.scala index d0d21aedc4..e9d0f9ea19 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/HasManyThrough.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/HasManyThrough.scala @@ -17,7 +17,7 @@ package net.liftweb package mapper -import collection.mutable.HashSet +import scala.collection.mutable.HashSet import util.FatLazy import common._ diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/ManyToMany.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/ManyToMany.scala index d249cede25..feff467a1d 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/ManyToMany.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/ManyToMany.scala @@ -29,8 +29,8 @@ import net.liftweb.common._ trait ManyToMany extends BaseKeyedMapper { this: KeyedMapper[_, _] => - type K = TheKeyType - type T = KeyedMapperType + private[this] type K = TheKeyType + private[this] type T = KeyedMapperType private var manyToManyFields: List[MappedManyToMany[_,_,_]] = Nil diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala index 38262493f8..290c70cd27 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala @@ -23,8 +23,8 @@ import java.util.{Date, Locale} import scala.language.existentials -import collection.mutable.{ListBuffer, HashMap} -import collection.immutable.{SortedMap, TreeMap} +import scala.collection.mutable.{ListBuffer, HashMap} +import scala.collection.immutable.{SortedMap, TreeMap} import xml._ import common._ diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/Schemifier.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/Schemifier.scala index 0ace118798..e4b4928be1 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/Schemifier.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/Schemifier.scala @@ -19,7 +19,7 @@ package mapper import java.sql.{Connection, ResultSet, DatabaseMetaData} -import collection.mutable.{HashMap, ListBuffer} +import scala.collection.mutable.{HashMap, ListBuffer} import common.{Full, Box, Loggable} import util.Helpers diff --git a/persistence/mapper/src/test/scala/net/liftweb/mapper/MapperSpec.scala b/persistence/mapper/src/test/scala/net/liftweb/mapper/MapperSpec.scala index 09599d8d35..5263576efd 100644 --- a/persistence/mapper/src/test/scala/net/liftweb/mapper/MapperSpec.scala +++ b/persistence/mapper/src/test/scala/net/liftweb/mapper/MapperSpec.scala @@ -169,8 +169,6 @@ class MapperSpec extends Specification with BeforeExample { val mm = SampleTag.findAll(Like(SampleTag.tag, "M%")) - (mm.length > 0) must beTrue - for (t <- mm) (t.tag.get.startsWith("M")) must beTrue @@ -179,6 +177,8 @@ class MapperSpec extends Specification with BeforeExample { t.model.obj t.model.cached_? must beTrue } + + (mm.length > 0) must beTrue } "Nullable Long works" in { @@ -209,8 +209,9 @@ class MapperSpec extends Specification with BeforeExample { "Precache works" in { val oo = SampleTag.findAll(By(SampleTag.tag, "Meow"), PreCache(SampleTag.model)) - (oo.length > 0) must beTrue for (t <- oo) yield t.model.cached_? must beTrue + + (oo.length > 0) must beTrue } "Precache works with OrderBy" in { @@ -232,16 +233,18 @@ class MapperSpec extends Specification with BeforeExample { val dogs = Dog.findAll(By(Dog.name, "fido"), PreCache(Dog.owner, false)) val oo = SampleTag.findAll(By(SampleTag.tag, "Meow"), PreCache(SampleTag.model, false)) - (oo.length > 0) must beTrue for (t <- oo) yield t.model.cached_? must beTrue + + (oo.length > 0) must beTrue } "Non-deterministic Precache works with OrderBy" in { val dogs = Dog.findAll(By(Dog.name, "fido"), OrderBy(Dog.name, Ascending), PreCache(Dog.owner, false)) val oo = SampleTag.findAll(OrderBy(SampleTag.tag, Ascending), MaxRows(2), PreCache(SampleTag.model, false)) - (oo.length > 0) must beTrue for (t <- oo) yield t.model.cached_? must beTrue + + (oo.length > 0) must beTrue } "work with Mixed case" in { @@ -324,8 +327,9 @@ class MapperSpec extends Specification with BeforeExample { val dogs = Dog2.findAll(By(Dog2.name, "fido"), PreCache(Dog2.owner, false)) val oo = SampleTag.findAll(By(SampleTag.tag, "Meow"), PreCache(SampleTag.model, false)) - (oo.length > 0) must beTrue for (t <- oo) yield t.model.cached_? must beTrue + + (oo.length > 0) must beTrue } @@ -354,8 +358,9 @@ class MapperSpec extends Specification with BeforeExample { val oo = SampleTag.findAll(OrderBy(SampleTag.tag, Ascending), MaxRows(2), PreCache(SampleTag.model, false)) - (oo.length > 0) must beTrue for (t <- oo) yield t.model.cached_? must beTrue + + (oo.length > 0) must beTrue } "Save flag results in update rather than insert" in { diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/AsMongoRecord.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/AsMongoRecord.scala new file mode 100644 index 0000000000..9fea1550e0 --- /dev/null +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/AsMongoRecord.scala @@ -0,0 +1,36 @@ +/** + * Copyright 2014 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 mongodb +package record + +import org.bson.types.ObjectId + +/** + * Extend this to create extractors for your MongoRecords. + * + * Example: + * object AsUser extends AsMongoRecord(User) + */ +class AsMongoRecord[A <: MongoRecord[A]](meta: MongoMetaRecord[A]) { + + def unapply(in: String): Option[A] = asMongoRecord(in) + + def asMongoRecord(in: String): Option[A] = + if (ObjectId.isValid(in)) meta.find(new ObjectId(in)) + else None +} diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/MongoMetaRecord.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/MongoMetaRecord.scala index 94e20c831a..7278c9e394 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/MongoMetaRecord.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/MongoMetaRecord.scala @@ -31,6 +31,7 @@ import net.liftweb.mongodb.record.field._ import net.liftweb.record.{MandatoryTypedField, MetaRecord, Record} import net.liftweb.record.FieldHelpers.expectedA import net.liftweb.record.field._ +import net.liftweb.util.ConnectionIdentifier import com.mongodb._ import com.mongodb.util.JSON @@ -141,15 +142,14 @@ trait MongoMetaRecord[BaseRecord <: MongoRecord[BaseRecord]] def find(k: String, o: Any): Box[BaseRecord] = find(new BasicDBObject(k, o)) /** - * Find all rows in this collection - */ - def findAll: List[BaseRecord] = { - /* - * The call to toArray retrieves all documents and puts them in memory. + * Find all rows in this collection. + * Retrieves all documents and puts them in memory. */ - useColl( coll => { - coll.find.toArray.map(dbo => fromDBObject(dbo)).toList - }) + def findAll: List[BaseRecord] = useColl { coll => + /** Mongo Cursors are both Iterable and Iterator, + * so we need to reduce ambiguity for implicits + */ + (coll.find: Iterator[DBObject]).map(fromDBObject).toList } /** @@ -169,16 +169,16 @@ trait MongoMetaRecord[BaseRecord <: MongoRecord[BaseRecord]] protected def findAll(sort: Option[DBObject], opts: FindOption*)(f: (DBCollection) => DBCursor): List[BaseRecord] = { val findOpts = opts.toList - useColl( coll => { + useColl { coll => val cur = f(coll).limit( - findOpts.find(_.isInstanceOf[Limit]).map(x => x.value).getOrElse(0) + findOpts.find(_.isInstanceOf[Limit]).map(_.value).getOrElse(0) ).skip( - findOpts.find(_.isInstanceOf[Skip]).map(x => x.value).getOrElse(0) + findOpts.find(_.isInstanceOf[Skip]).map(_.value).getOrElse(0) ) - sort.foreach( s => cur.sort(s)) - // The call to toArray retrieves all documents and puts them in memory. - cur.toArray.map(dbo => fromDBObject(dbo)).toList - }) + sort.foreach(s => cur.sort(s)) + // This retrieves all documents and puts them in memory. + (cur: Iterator[DBObject]).map(fromDBObject).toList + } } /** diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/DateField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/DateField.scala index 34d01c5de6..144db8a787 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/DateField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/DateField.scala @@ -1,5 +1,5 @@ /* -* Copyright 2010-2013 WorldWide Conferencing, LLC +* Copyright 2010-2014 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 @@ trait DateTypedField extends TypedField[Date] { case jv => JsRaw(Printer.compact(render(jv))) } - def asJValue: JValue = valueBox.map(v => mongodb.Meta.dateAsJValue(v, formats)) openOr (JNothing: JValue) + def asJValue: JValue = valueBox.map(v => JsonDate(v)(formats)) openOr (JNothing: JValue) } class DateField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala index ef14159042..0dd4d5110b 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/MongoListField.scala @@ -1,5 +1,5 @@ /* - * Copyright 2010-2013 WorldWide Conferencing, LLC + * Copyright 2010-2014 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. @@ -19,25 +19,32 @@ package mongodb package record package field -import java.util.Date - import scala.collection.JavaConversions._ import scala.xml.NodeSeq import common.{Box, Empty, Failure, Full} -import json.JsonAST._ -import json.JsonParser import http.SHtml import http.js.JE.{JsNull, JsRaw} +import json._ import net.liftweb.record.{Field, FieldHelpers, MandatoryTypedField, Record} import util.Helpers._ import com.mongodb._ import org.bson.types.ObjectId +import org.joda.time.DateTime /** - * List field. Compatible with most object types, - * including Pattern, ObjectId, Date, and UUID. + * List field. + * + * Supported types: + * primitives - String, Int, Long, Double, Float, Byte, BigInt, + * Boolean (and their Java equivalents) + * date types - java.util.Date, org.joda.time.DateTime + * mongo types - ObjectId, Pattern, UUID + * + * If you need to support other types, you will need to override the + * asDBObject and setFromDBObject functions accordingly. And the + * asJValue and setFromJValue functions if you will be using them. * * Note: setting optional_? = false will result in incorrect equals behavior when using setFromJValue */ @@ -46,7 +53,7 @@ class MongoListField[OwnerType <: BsonRecord[OwnerType], ListType: Manifest](rec with MandatoryTypedField[List[ListType]] with MongoFieldFlavor[List[ListType]] { - import Meta.Reflection._ + import mongodb.Meta.Reflection._ lazy val mf = manifest[ListType] @@ -56,6 +63,8 @@ class MongoListField[OwnerType <: BsonRecord[OwnerType], ListType: Manifest](rec def defaultValue = List[ListType]() + implicit def formats = owner.meta.formats + def setFromAny(in: Any): Box[MyType] = { in match { case dbo: DBObject => setFromDBObject(dbo) @@ -71,9 +80,16 @@ class MongoListField[OwnerType <: BsonRecord[OwnerType], ListType: Manifest](rec } } - def setFromJValue(jvalue: JValue) = jvalue match { + def setFromJValue(jvalue: JValue): Box[MyType] = jvalue match { case JNothing|JNull if optional_? => setBox(Empty) - case JArray(arr) => setBox(Full(arr.map(_.values.asInstanceOf[ListType]))) + case JArray(array) => setBox(Full((array.map { + case JsonObjectId(objectId) => objectId + case JsonRegex(regex) => regex + case JsonUUID(uuid) => uuid + case JsonDateTime(dt) if (mf.toString == "org.joda.time.DateTime") => dt + case JsonDate(date) => date + case other => other.values + }).asInstanceOf[MyType])) case other => setBox(FieldHelpers.expectedA("JArray", other)) } @@ -87,20 +103,22 @@ class MongoListField[OwnerType <: BsonRecord[OwnerType], ListType: Manifest](rec /** Options for select list **/ def options: List[(ListType, String)] = Nil - private def elem = SHtml.multiSelectObj[ListType]( - options, - value, - set(_) - ) % ("tabindex" -> tabIndex.toString) + private def elem = { + def elem0 = SHtml.multiSelectObj[ListType]( + options, + value, + set(_) + ) % ("tabindex" -> tabIndex.toString) + + SHtml.hidden(() => set(Nil)) ++ (uniqueFieldId match { + case Full(id) => (elem0 % ("id" -> id)) + case _ => elem0 + }) + } def toForm: Box[NodeSeq] = - if (options.length > 0) - uniqueFieldId match { - case Full(id) => Full(elem % ("id" -> id)) - case _ => Full(elem) - } - else - Empty + if (options.length > 0) Full(elem) + else Empty def asJValue = JArray(value.map(li => li.asInstanceOf[AnyRef] match { case x if primitive_?(x.getClass) => primitive2jvalue(x) diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/ObjectIdField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/ObjectIdField.scala index 1d99c5bf50..f0a59741c7 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/ObjectIdField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/ObjectIdField.scala @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 WorldWide Conferencing, LLC + * Copyright 2010-2014 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. @@ -90,8 +90,8 @@ class ObjectIdField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) case jv => JsRaw(Printer.compact(render(jv))) } - def asJValue: JValue = valueBox.map(v => Meta.objectIdAsJValue(v, owner.meta.formats)) openOr (JNothing: JValue) + def asJValue: JValue = valueBox.map(v => JsonObjectId.asJValue(v, owner.meta.formats)) openOr (JNothing: JValue) - def createdAt: Date = new Date(this.get.getTime) + def createdAt: Date = this.get.getDate } diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/PatternField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/PatternField.scala index d356769c81..a4a7fd2da9 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/PatternField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/PatternField.scala @@ -1,5 +1,5 @@ /* -* Copyright 2010-2011 WorldWide Conferencing, LLC +* Copyright 2010-2014 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. @@ -70,6 +70,6 @@ class PatternField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) case jv => Str(Printer.compact(render(jv))) } - def asJValue: JValue = valueBox.map(v => Meta.patternAsJValue(v)) openOr (JNothing: JValue) + def asJValue: JValue = valueBox.map(v => JsonRegex(v)) openOr (JNothing: JValue) } diff --git a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/UUIDField.scala b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/UUIDField.scala index 12570c04bd..b0487c215c 100644 --- a/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/UUIDField.scala +++ b/persistence/mongodb-record/src/main/scala/net/liftweb/mongodb/record/field/UUIDField.scala @@ -1,5 +1,5 @@ /* -* Copyright 2010-2011 WorldWide Conferencing, LLC +* Copyright 2010-2014 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. @@ -11,10 +11,10 @@ * limitations under the License. */ -package net.liftweb -package mongodb -package record -package field +package net.liftweb +package mongodb +package record +package field import java.util.UUID @@ -82,7 +82,7 @@ class UUIDField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) case jv => JsRaw(Printer.compact(render(jv))) } - def asJValue: JValue = valueBox.map(v => Meta.uuidAsJValue(v)) openOr (JNothing: JValue) + def asJValue: JValue = valueBox.map(v => JsonUUID(v)) openOr (JNothing: JValue) } diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala index 96ed4f4326..8d38e1940e 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/Fixtures.scala @@ -1,5 +1,5 @@ /* - * Copyright 2010-2013 WorldWide Conferencing, LLC + * Copyright 2010-2014 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. @@ -28,6 +28,8 @@ import http.SHtml import util.{FieldError, Helpers} import java.math.MathContext +import java.util.{Date, UUID} +import java.util.regex.Pattern import scala.xml.Text import net.liftweb.record._ @@ -35,6 +37,7 @@ import net.liftweb.record.field._ import net.liftweb.record.field.joda._ import org.bson.types.ObjectId +import org.joda.time.DateTime object MyTestEnum extends Enumeration { val ONE = Value("ONE") @@ -262,18 +265,38 @@ class ListTestRecord private () extends MongoRecord[ListTestRecord] with UUIDPk[ def meta = ListTestRecord object mandatoryStringListField extends MongoListField[ListTestRecord, String](this) + object mandatoryMongoRefListField extends ObjectIdRefListField(this, FieldTypeTestRecord) object mandatoryIntListField extends MongoListField[ListTestRecord, Int](this) object mandatoryMongoJsonObjectListField extends MongoJsonObjectListField(this, TypeTestJsonObject) object mongoCaseClassListField extends MongoCaseClassListField[ListTestRecord, MongoCaseClassTestObject](this) { override def formats = owner.meta.formats } - - // TODO: More List types } object ListTestRecord extends ListTestRecord with MongoMetaRecord[ListTestRecord] { override def formats = allFormats + new EnumSerializer(MyTestEnum) } +class MongoListTestRecord private () extends MongoRecord[MongoListTestRecord] with UUIDPk[MongoListTestRecord] { + def meta = MongoListTestRecord + + object objectIdRefListField extends ObjectIdRefListField(this, FieldTypeTestRecord) + object patternListField extends MongoListField[MongoListTestRecord, Pattern](this) + object dateListField extends MongoListField[MongoListTestRecord, Date](this) + object uuidListField extends MongoListField[MongoListTestRecord, UUID](this) +} +object MongoListTestRecord extends MongoListTestRecord with MongoMetaRecord[MongoListTestRecord] { + override def formats = DefaultFormats.lossless + new ObjectIdSerializer + new PatternSerializer + new DateSerializer +} + +class MongoJodaListTestRecord private () extends MongoRecord[MongoJodaListTestRecord] with UUIDPk[MongoJodaListTestRecord] { + def meta = MongoJodaListTestRecord + + object dateTimeListField extends MongoListField[MongoJodaListTestRecord, DateTime](this) +} +object MongoJodaListTestRecord extends MongoJodaListTestRecord with MongoMetaRecord[MongoJodaListTestRecord] { + override def formats = DefaultFormats.lossless + new DateTimeSerializer +} + class MapTestRecord private () extends MongoRecord[MapTestRecord] with StringPk[MapTestRecord] { def meta = MapTestRecord diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoFieldSpec.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoFieldSpec.scala index 096616775b..2de7ff7e90 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoFieldSpec.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoFieldSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2006-2013 WorldWide Conferencing, LLC + * Copyright 2006-2014 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. @@ -22,13 +22,15 @@ import java.util.{Calendar, Date, UUID} import java.util.regex.Pattern import org.bson.types.ObjectId -import org.specs2.mutable.Specification -import org.specs2.specification.Fragment -import org.specs2.specification.AroundExample +import org.specs2.mutable._ +import org.specs2.specification._ +import org.specs2.execute.AsResult + +import org.joda.time.DateTime import common._ import json._ -import BsonDSL._ +import mongodb.BsonDSL._ import util.Helpers.randomString import http.{LiftSession, S} import http.js.JE._ @@ -52,6 +54,8 @@ object MongoFieldSpec extends Specification with MongoTestKit with AroundExample lazy val session = new LiftSession("", randomString(20), Empty) + // One of these is for specs2 2.x, the other for specs2 1.x + protected def around[T : AsResult](t: =>T) = S.initIfUninitted(session) { AsResult(t) } protected def around[T <% org.specs2.execute.Result](t: =>T) = S.initIfUninitted(session) { t } def passBasicTests[A]( @@ -62,7 +66,7 @@ object MongoFieldSpec extends Specification with MongoTestKit with AroundExample canCheckDefaultValues: Boolean = true )(implicit m: scala.reflect.Manifest[A]): Unit = { - def commonBehaviorsForAllFlavors(field: MandatoryTypedField[A]): Unit = { + def commonBehaviorsForAllFlavors(field: MandatoryTypedField[A]) = { "which have the correct initial value" in { field.value must be_==(field.defaultValue).when(canCheckDefaultValues) @@ -250,19 +254,23 @@ object MongoFieldSpec extends Specification with MongoTestKit with AroundExample } "ObjectIdField" should { - val rec = MongoFieldTypeTestRecord.createRecord - val oid = ObjectId.get - val oid2 = ObjectId.get - passBasicTests(oid, oid2, rec.mandatoryObjectIdField, Full(rec.legacyOptionalObjectIdField), false) - passConversionTests( - oid, - rec.mandatoryObjectIdField, - JsObj(("$oid", oid.toString)), - JObject(List(JField("$oid", JString(oid.toString)))), - Full() - ) - rec.mandatoryObjectIdField(oid) - new Date(oid.getTime) mustEqual rec.mandatoryObjectIdField.createdAt + // The extra `in` here is required for compilation, or we get a strange ambiguous overload warning. + "work and provide the appropriate date" in { + val rec = MongoFieldTypeTestRecord.createRecord + val oid = ObjectId.get + val oid2 = ObjectId.get + passBasicTests(oid, oid2, rec.mandatoryObjectIdField, Full(rec.legacyOptionalObjectIdField), false) + passConversionTests( + oid, + rec.mandatoryObjectIdField, + JsObj(("$oid", oid.toString)), + JObject(List(JField("$oid", JString(oid.toString)))), + Full() + ) + rec.mandatoryObjectIdField(oid) + + oid.getDate must_== rec.mandatoryObjectIdField.createdAt + } } "PatternField" should { @@ -346,6 +354,134 @@ object MongoFieldSpec extends Specification with MongoTestKit with AroundExample } } + "MongoListField (ObjectId)" should { + "function correctly" in { + val rec = MongoListTestRecord.createRecord + val oid1 = ObjectId.get + val oid2 = ObjectId.get + val oid3 = ObjectId.get + val oid4 = ObjectId.get + val oid5 = ObjectId.get + val oid6 = ObjectId.get + val lst = List(oid1, oid2, oid3) + val lst2 = List(oid4, oid5, oid6) + passBasicTests(lst, lst2, rec.objectIdRefListField, Empty) + passConversionTests( + lst, + rec.objectIdRefListField, + JsArray(Str(oid1.toString), Str(oid2.toString), Str(oid3.toString)), + JArray(List( + JObject(List(JField("$oid", JString(oid1.toString)))), + JObject(List(JField("$oid", JString(oid2.toString)))), + JObject(List(JField("$oid", JString(oid3.toString)))) + )), + Empty + ) + } + } + + "MongoListField (Pattern)" should { + "function correctly" in { + val rec = MongoListTestRecord.createRecord + val ptrn1 = Pattern.compile("^Mo", Pattern.CASE_INSENSITIVE) + val ptrn2 = Pattern.compile("^MON", Pattern.CASE_INSENSITIVE) + val ptrn3 = Pattern.compile("^TUE") + val ptrn4 = Pattern.compile("^WED") + val lst1 = List(ptrn1, ptrn2) + val lst2 = List(ptrn3, ptrn4) + passBasicTests(lst1, lst2, rec.patternListField, Empty) + passConversionTests( + lst1, + rec.patternListField, + JsArray(Str(ptrn1.toString), Str(ptrn2.toString)), + JArray(List( + JsonRegex(ptrn1), + JsonRegex(ptrn2) + )), + Empty, + false + ) + } + } + + "MongoListField (Date)" should { + "function correctly" in { + val rec = MongoListTestRecord.createRecord + val dt1 = new Date + val dt2 = new Date + val dt3 = new Date + val dt4 = new Date + val dt5 = new Date + val dt6 = new Date + val lst = List(dt1, dt2, dt3) + val lst2 = List(dt4, dt5, dt6) + passBasicTests(lst, lst2, rec.dateListField, Empty) + passConversionTests( + lst, + rec.dateListField, + JsArray(Str(dt1.toString), Str(dt2.toString), Str(dt3.toString)), + JArray(List( + JsonDate(dt1)(MongoListTestRecord.formats), + JsonDate(dt2)(MongoListTestRecord.formats), + JsonDate(dt3)(MongoListTestRecord.formats) + )), + Empty + ) + } + } + + "MongoListField (UUID)" should { + "function correctly" in { + val rec = MongoListTestRecord.createRecord + val uuid1 = UUID.randomUUID + val uuid2 = UUID.randomUUID + val uuid3 = UUID.randomUUID + val uuid4 = UUID.randomUUID + val uuid5 = UUID.randomUUID + val uuid6 = UUID.randomUUID + val lst = List(uuid1, uuid2, uuid3) + val lst2 = List(uuid4, uuid5, uuid6) + passBasicTests(lst, lst2, rec.uuidListField, Empty) + passConversionTests( + lst, + rec.uuidListField, + JsArray(Str(uuid1.toString), Str(uuid2.toString), Str(uuid3.toString)), + JArray(List( + JsonUUID(uuid1), + JsonUUID(uuid2), + JsonUUID(uuid3) + )), + Empty + ) + } + } + + "MongoListField (DateTime)" should { + "function correctly" in { + val rec = MongoJodaListTestRecord.createRecord + val dt1 = new DateTime + val dt2 = new DateTime + val dt3 = new DateTime + val dt4 = new DateTime + val dt5 = new DateTime + val dt6 = new DateTime + val lst = List(dt1, dt2, dt3) + val lst2 = List(dt4, dt5, dt6) + passBasicTests(lst, lst2, rec.dateTimeListField, Empty) + passConversionTests( + lst, + rec.dateTimeListField, + JsArray(Str(dt1.toString), Str(dt2.toString), Str(dt3.toString)), + JArray(List( + JsonDate(dt1.toDate)(MongoListTestRecord.formats), + JsonDate(dt2.toDate)(MongoListTestRecord.formats), + JsonDate(dt3.toDate)(MongoListTestRecord.formats) + )), + Empty + ) + } + } + "MongoJsonObjectListField" should { "function correctly" in { val rec = ListTestRecord.createRecord diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala index fa0b5d975f..798c67e2bd 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/MongoRecordSpec.scala @@ -66,15 +66,21 @@ class MongoRecordSpec extends Specification with MongoTestKit { } "correctly look up fields by name" in { - for (name <- allExpectedFieldNames) yield { - rec.fieldByName(name).isDefined must_== true - } + val fields = + allExpectedFieldNames.flatMap { name => + rec.fieldByName(name) + } + + fields.length must_== allExpectedFieldNames.length } "not look up fields by bogus names" in { - for (name <- allExpectedFieldNames) yield { - rec.fieldByName("x" + name + "y").isDefined must_== false - } + val fields = + allExpectedFieldNames.flatMap { name => + rec.fieldByName("x" + name + "y") + } + + fields.length must_== 0 } } @@ -231,6 +237,7 @@ class MongoRecordSpec extends Specification with MongoTestKit { .mandatoryIntListField(List(4, 5, 6)) .mandatoryMongoJsonObjectListField(List(TypeTestJsonObject(1, "jsonobj1", Map("x" -> "1")), TypeTestJsonObject(2, "jsonobj2", Map("x" -> "2")))) .mongoCaseClassListField(List(MongoCaseClassTestObject(1,"str",MyTestEnum.TWO))) + .mandatoryMongoRefListField(Nil) val ltrJson = ("_id" -> ("$uuid" -> ltr.id.toString)) ~ @@ -242,7 +249,8 @@ class MongoRecordSpec extends Specification with MongoTestKit { )) ~ ("mongoCaseClassListField" -> List( ("intField" -> 1) ~ ("stringField" -> "str") ~ ("enum" -> 1) - )) + )) ~ + ("mandatoryMongoRefListField" -> JArray(Nil)) val mtr = MapTestRecord.createRecord .mandatoryStringMapField(Map("a" -> "abc", "b" -> "def", "c" -> "ghi")) @@ -331,9 +339,9 @@ class MongoRecordSpec extends Specification with MongoTestKit { bftr.save() val bftrFromDb = BinaryFieldTestRecord.find(bftr.id.value) - bftrFromDb.isDefined must_== true - bftrFromDb.toList map { tr => - tr mustEqual bftr + bftrFromDb must beLike { + case Full(tr) => + tr mustEqual bftr } } } @@ -512,10 +520,7 @@ class MongoRecordSpec extends Specification with MongoTestKit { } val joftrFromJson = JObjectFieldTestRecord.fromJsonString(compact(render(joftrJson))) - joftrFromJson.isDefined must_== true - joftrFromJson.toList map { tr => - tr mustEqual joftr - } + joftrFromJson must_== Full(joftr) } "handle null" in { @@ -527,23 +532,22 @@ class MongoRecordSpec extends Specification with MongoTestKit { val ntrFromDb = NullTestRecord.find(ntr.id.value) - ntrFromDb.isDefined must_== true - - ntrFromDb.toList map { n => - // goes in as - ntr.nullstring.valueBox.map(_ must beNull) - ntr.nullstring.value must beNull - // comes out as - n.nullstring.valueBox.map(_ must_== "") - n.nullstring.value must_== "" - // JsonObjects - n.jsonobjlist.value.size must_== 2 - ntr.jsonobjlist.value.size must_== 2 - n.jsonobjlist.value(0).id must_== ntr.jsonobjlist.value(0).id - n.jsonobjlist.value(0).name must beNull - ntr.jsonobjlist.value(0).name must beNull - n.jsonobjlist.value(1).id must_== ntr.jsonobjlist.value(1).id - n.jsonobjlist.value(1).name must_== ntr.jsonobjlist.value(1).name + ntrFromDb must beLike { + case Full(n) => + // goes in as + ntr.nullstring.valueBox.map(_ must beNull) + ntr.nullstring.value must beNull + // comes out as + n.nullstring.valueBox.map(_ must_== "") + n.nullstring.value must_== "" + // JsonObjects + n.jsonobjlist.value.size must_== 2 + ntr.jsonobjlist.value.size must_== 2 + n.jsonobjlist.value(0).id must_== ntr.jsonobjlist.value(0).id + n.jsonobjlist.value(0).name must beNull + ntr.jsonobjlist.value(0).name must beNull + n.jsonobjlist.value(1).id must_== ntr.jsonobjlist.value(1).id + n.jsonobjlist.value(1).name must_== ntr.jsonobjlist.value(1).name } } @@ -559,15 +563,14 @@ class MongoRecordSpec extends Specification with MongoTestKit { val btrFromDb = BoxTestRecord.find(btr.id.value) - btrFromDb.isDefined must_== true - - btrFromDb.toList map { b => - b.jsonobjlist.value.size must_== 2 - btr.jsonobjlist.value.size must_== 2 - val sortedList = b.jsonobjlist.value.sortWith(_.id < _.id) - sortedList(0).boxEmpty must_== Empty - sortedList(0).boxFull must_== Full("Full String1") - sortedList(0).boxFail must_== Failure("Failure1") + btrFromDb must beLike { + case Full(b) => + b.jsonobjlist.value.size must_== 2 + btr.jsonobjlist.value.size must_== 2 + val sortedList = b.jsonobjlist.value.sortWith(_.id < _.id) + sortedList(0).boxEmpty must_== Empty + sortedList(0).boxFull must_== Full("Full String1") + sortedList(0).boxFail must_== Failure("Failure1") } } @@ -641,48 +644,47 @@ class MongoRecordSpec extends Specification with MongoTestKit { val recFromDb = FieldTypeTestRecord.find(missingFieldDocId) - recFromDb.isDefined must_== true - - recFromDb.toList map { r => - r.mandatoryBooleanField.get must_== false - r.legacyOptionalBooleanField - r.optionalBooleanField.get must beEmpty - r.mandatoryCountryField.get must_== Countries.C1 - r.legacyOptionalCountryField.valueBox must beEmpty - r.optionalCountryField.get must beEmpty - r.mandatoryDecimalField.get must_== 0.00 - r.legacyOptionalDecimalField.valueBox must beEmpty - r.optionalDecimalField.get must beEmpty - r.mandatoryDoubleField.get must_== 0d - r.legacyOptionalDoubleField.valueBox must beEmpty - r.optionalDoubleField.get must beEmpty - r.mandatoryEmailField.get must_== "" - r.legacyOptionalEmailField.valueBox must beEmpty - r.optionalEmailField.get must beEmpty - r.mandatoryEnumField.get must_== MyTestEnum.ONE - r.legacyOptionalEnumField.valueBox must beEmpty - r.optionalEnumField.get must beEmpty - r.mandatoryIntField.get must_== 0 - r.legacyOptionalIntField.valueBox must beEmpty - r.optionalIntField.get must beEmpty - r.mandatoryLocaleField.get must_== Locale.getDefault.toString - r.legacyOptionalLocaleField.valueBox must beEmpty - r.optionalLocaleField.get must beEmpty - r.mandatoryLongField.get must_== 0L - r.legacyOptionalLongField.valueBox must beEmpty - r.optionalLongField.get must beEmpty - r.mandatoryPostalCodeField.get must_== "" - r.legacyOptionalPostalCodeField.valueBox must beEmpty - r.optionalPostalCodeField.get must beEmpty - r.mandatoryStringField.get must_== "" - r.legacyOptionalStringField.valueBox must beEmpty - r.optionalStringField.get must beEmpty - r.mandatoryTextareaField.get must_== "" - r.legacyOptionalTextareaField.valueBox must beEmpty - r.optionalTextareaField.get must beEmpty - // r.mandatoryTimeZoneField.get must_== "America/Chicago" - r.legacyOptionalTimeZoneField.valueBox must beEmpty - r.optionalTimeZoneField.get must beEmpty + recFromDb must beLike { + case Full(r) => + r.mandatoryBooleanField.get must_== false + r.legacyOptionalBooleanField + r.optionalBooleanField.get must beEmpty + r.mandatoryCountryField.get must_== Countries.C1 + r.legacyOptionalCountryField.valueBox must beEmpty + r.optionalCountryField.get must beEmpty + r.mandatoryDecimalField.get must_== 0.00 + r.legacyOptionalDecimalField.valueBox must beEmpty + r.optionalDecimalField.get must beEmpty + r.mandatoryDoubleField.get must_== 0d + r.legacyOptionalDoubleField.valueBox must beEmpty + r.optionalDoubleField.get must beEmpty + r.mandatoryEmailField.get must_== "" + r.legacyOptionalEmailField.valueBox must beEmpty + r.optionalEmailField.get must beEmpty + r.mandatoryEnumField.get must_== MyTestEnum.ONE + r.legacyOptionalEnumField.valueBox must beEmpty + r.optionalEnumField.get must beEmpty + r.mandatoryIntField.get must_== 0 + r.legacyOptionalIntField.valueBox must beEmpty + r.optionalIntField.get must beEmpty + r.mandatoryLocaleField.get must_== Locale.getDefault.toString + r.legacyOptionalLocaleField.valueBox must beEmpty + r.optionalLocaleField.get must beEmpty + r.mandatoryLongField.get must_== 0L + r.legacyOptionalLongField.valueBox must beEmpty + r.optionalLongField.get must beEmpty + r.mandatoryPostalCodeField.get must_== "" + r.legacyOptionalPostalCodeField.valueBox must beEmpty + r.optionalPostalCodeField.get must beEmpty + r.mandatoryStringField.get must_== "" + r.legacyOptionalStringField.valueBox must beEmpty + r.optionalStringField.get must beEmpty + r.mandatoryTextareaField.get must_== "" + r.legacyOptionalTextareaField.valueBox must beEmpty + r.optionalTextareaField.get must beEmpty + // r.mandatoryTimeZoneField.get must_== "America/Chicago" + r.legacyOptionalTimeZoneField.valueBox must beEmpty + r.optionalTimeZoneField.get must beEmpty } } } @@ -753,10 +755,10 @@ class MongoRecordSpec extends Specification with MongoTestKit { fttr2.dirty_? must_== false val fromDb2 = FieldTypeTestRecord.find(fttr2.id.get) - fromDb2.isDefined must_== true - fromDb2.toList map { rec => - rec must_== fttr2 - rec.dirty_? must_== false + fromDb2 must beLike { + case Full(rec) => + rec must_== fttr2 + rec.dirty_? must_== false } } } @@ -811,10 +813,10 @@ class MongoRecordSpec extends Specification with MongoTestKit { mfttr2.dirty_? must_== false val fromDb2 = MongoFieldTypeTestRecord.find(mfttr2.id.get) - fromDb2.isDefined must_== true - fromDb2.toList map { rec => - rec must_== mfttr2 - rec.dirty_? must_== false + fromDb2 must beLike { + case Full(rec) => + rec must_== mfttr2 + rec.dirty_? must_== false } } @@ -829,12 +831,11 @@ class MongoRecordSpec extends Specification with MongoTestKit { pftrd.dirty_? must_== false val fromDb = PatternFieldTestRecord.find(pftrd.id.get) - fromDb.isDefined must_== true - fromDb foreach { rec => - rec must_== pftrd - rec.dirty_? must_== false + fromDb must beLike { + case Full(rec) => + rec must_== pftrd + rec.dirty_? must_== false } - success } "update dirty fields for a ListTestRecord" in { @@ -857,10 +858,10 @@ class MongoRecordSpec extends Specification with MongoTestKit { ltr.dirty_? must_== false val fromDb = ListTestRecord.find(ltr.id.get) - fromDb.isDefined must_== true - fromDb.toList map { rec => - rec must_== ltr - rec.dirty_? must_== false + fromDb must beLike { + case Full(rec) => + rec must_== ltr + rec.dirty_? must_== false } } @@ -878,10 +879,10 @@ class MongoRecordSpec extends Specification with MongoTestKit { mtr.dirty_? must_== false val fromDb = MapTestRecord.find(mtr.id.get) - fromDb.isDefined must_== true - fromDb.toList map { rec => - rec must_== mtr - rec.dirty_? must_== false + fromDb must beLike { + case Full(rec) => + rec must_== mtr + rec.dirty_? must_== false } } @@ -914,10 +915,10 @@ class MongoRecordSpec extends Specification with MongoTestKit { srtr.dirty_? must_== false val fromDb = SubRecordTestRecord.find(srtr.id.get) - fromDb.isDefined must_== true - fromDb.toList map { rec => - rec must_== srtr - rec.dirty_? must_== false + fromDb must beLike { + case Full(rec) => + rec must_== srtr + rec.dirty_? must_== false } } diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumFieldSpec.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumFieldSpec.scala index 440e710bf6..1045c27dd1 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumFieldSpec.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumFieldSpec.scala @@ -81,12 +81,12 @@ class EnumFieldSpec extends Specification with MongoTestKit { val er = EnumRec.createRecord.save() val erFromDb = EnumRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dow.value mustEqual WeekDay.Mon - er2.dowOptional.valueBox mustEqual Empty - er2.jsonobj.value mustEqual JsonObj(WeekDay.Mon) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dow.value mustEqual WeekDay.Mon + er2.dowOptional.valueBox mustEqual Empty + er2.jsonobj.value mustEqual JsonObj(WeekDay.Mon) } } @@ -99,11 +99,11 @@ class EnumFieldSpec extends Specification with MongoTestKit { .save() val erFromDb = EnumRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dow.value mustEqual WeekDay.Tue - er2.jsonobj.value mustEqual JsonObj(WeekDay.Sun) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dow.value mustEqual WeekDay.Tue + er2.jsonobj.value mustEqual JsonObj(WeekDay.Sun) } } @@ -115,10 +115,10 @@ class EnumFieldSpec extends Specification with MongoTestKit { er.save() val erFromDb = EnumRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dowOptional.valueBox mustEqual Empty + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dowOptional.valueBox mustEqual Empty } } @@ -130,10 +130,10 @@ class EnumFieldSpec extends Specification with MongoTestKit { er.save() val erFromDb = EnumRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dowOptional.valueBox mustEqual Full(WeekDay.Sat) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dowOptional.valueBox mustEqual Full(WeekDay.Sat) } } } diff --git a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumNameFieldSpec.scala b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumNameFieldSpec.scala index 992755843b..e08f332988 100644 --- a/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumNameFieldSpec.scala +++ b/persistence/mongodb-record/src/test/scala/net/liftweb/mongodb/record/field/EnumNameFieldSpec.scala @@ -81,12 +81,12 @@ object EnumNameFieldSpec extends Specification with MongoTestKit { val er = EnumNameRec.createRecord.save() val erFromDb = EnumNameRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dow.value mustEqual WeekDay.Mon - er2.dowOptional.valueBox mustEqual Empty - er2.jsonobj.value mustEqual JsonObj(WeekDay.Mon) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dow.value mustEqual WeekDay.Mon + er2.dowOptional.valueBox mustEqual Empty + er2.jsonobj.value mustEqual JsonObj(WeekDay.Mon) } } @@ -99,11 +99,11 @@ object EnumNameFieldSpec extends Specification with MongoTestKit { .save() val erFromDb = EnumNameRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dow.value mustEqual WeekDay.Tue - er2.jsonobj.value mustEqual JsonObj(WeekDay.Sun) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dow.value mustEqual WeekDay.Tue + er2.jsonobj.value mustEqual JsonObj(WeekDay.Sun) } } @@ -115,10 +115,10 @@ object EnumNameFieldSpec extends Specification with MongoTestKit { er.save() val erFromDb = EnumNameRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dowOptional.valueBox mustEqual Empty + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dowOptional.valueBox mustEqual Empty } } @@ -130,10 +130,10 @@ object EnumNameFieldSpec extends Specification with MongoTestKit { er.save() val erFromDb = EnumNameRec.find(er.id.get) - erFromDb.isDefined must_== true - erFromDb.toList map { er2 => - er2 mustEqual er - er2.dowOptional.valueBox mustEqual Full(WeekDay.Sat) + erFromDb must beLike { + case Full(er2) => + er2 mustEqual er + er2.dowOptional.valueBox mustEqual Full(WeekDay.Sat) } } } diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/AsObjectId.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/AsObjectId.scala new file mode 100644 index 0000000000..c8868cc664 --- /dev/null +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/AsObjectId.scala @@ -0,0 +1,31 @@ +/** + * Copyright 2014 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 mongodb + +import org.bson.types.ObjectId + +/** + * An ObjectId extractor. + */ +object AsObjectId { + def unapply(in: String): Option[ObjectId] = asObjectId(in) + + def asObjectId(in: String): Option[ObjectId] = + if (ObjectId.isValid(in)) Some(new ObjectId(in)) + else None +} diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/BsonDSL.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/BsonDSL.scala index b25f5f7eb5..5b4881679c 100644 --- a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/BsonDSL.scala +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/BsonDSL.scala @@ -24,11 +24,13 @@ import java.util.{Date, UUID} import java.util.regex.Pattern import org.bson.types.ObjectId +import org.joda.time.DateTime object BsonDSL extends JsonDSL { - implicit def objectid2jvalue(oid: ObjectId): JValue = Meta.objectIdAsJValue(oid) - implicit def pattern2jvalue(p: Pattern): JValue = Meta.patternAsJValue(p) - implicit def regex2jvalue(r: Regex): JValue = Meta.patternAsJValue(r.pattern) - implicit def uuid2jvalue(u: UUID): JValue = Meta.uuidAsJValue(u) - implicit def date2jvalue(d: Date)(implicit formats: Formats): JValue = Meta.dateAsJValue(d, formats) + implicit def objectid2jvalue(oid: ObjectId): JValue = JsonObjectId(oid) + implicit def pattern2jvalue(p: Pattern): JValue = JsonRegex(p) + implicit def regex2jvalue(r: Regex): JValue = JsonRegex(r.pattern) + implicit def uuid2jvalue(u: UUID): JValue = JsonUUID(u) + implicit def date2jvalue(d: Date)(implicit formats: Formats): JValue = JsonDate(d) + implicit def datetime2jvalue(d: DateTime)(implicit formats: Formats): JValue = JsonDateTime(d) } diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JObjectParser.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JObjectParser.scala index 18263fcf7f..c44e0fb26e 100644 --- a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JObjectParser.scala +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JObjectParser.scala @@ -76,50 +76,42 @@ object JObjectParser extends SimpleInjector { object Parser { def parse(jo: JObject, formats: Formats): DBObject = { - parseObject(jo.obj, formats) + parseObject(jo.obj)(formats) } - private def parseArray(arr: List[JValue], formats: Formats): BasicDBList = { + private def parseArray(arr: List[JValue])(implicit formats: Formats): BasicDBList = { val dbl = new BasicDBList trimArr(arr).foreach { a => a match { - case JObject(JField("$oid", JString(s)) :: Nil) if (ObjectId.isValid(s)) => - dbl.add(new ObjectId(s)) - case JObject(JField("$regex", JString(s)) :: JField("$flags", JInt(f)) :: Nil) => - dbl.add(Pattern.compile(s, f.intValue)) - case JObject(JField("$dt", JString(s)) :: Nil) => - formats.dateFormat.parse(s) foreach { d => dbl.add(d) } - case JObject(JField("$uuid", JString(s)) :: Nil) => - dbl.add(UUID.fromString(s)) - case JArray(arr) => dbl.add(parseArray(arr, formats)) - case JObject(jo) => dbl.add(parseObject(jo, formats)) - case jv: JValue => dbl.add(renderValue(jv, formats)) + case JsonObjectId(objectId) => dbl.add(objectId) + case JsonRegex(regex) => dbl.add(regex) + case JsonUUID(uuid) => dbl.add(uuid) + case JsonDate(date) => dbl.add(date) + case JArray(arr) => dbl.add(parseArray(arr)) + case JObject(jo) => dbl.add(parseObject(jo)) + case jv: JValue => dbl.add(renderValue(jv)) } } dbl } - private def parseObject(obj: List[JField], formats: Formats): BasicDBObject = { + private def parseObject(obj: List[JField])(implicit formats: Formats): BasicDBObject = { val dbo = new BasicDBObject trimObj(obj).foreach { jf => jf.value match { - case JObject(JField("$oid", JString(s)) :: Nil) if (ObjectId.isValid(s)) => - dbo.put(jf.name, new ObjectId(s)) - case JObject(JField("$regex", JString(s)) :: JField("$flags", JInt(f)) :: Nil) => - dbo.put(jf.name, Pattern.compile(s, f.intValue)) - case JObject(JField("$dt", JString(s)) :: Nil) => - formats.dateFormat.parse(s) foreach { d => dbo.put(jf.name, d) } - case JObject(JField("$uuid", JString(s)) :: Nil) => - dbo.put(jf.name, UUID.fromString(s)) - case JArray(arr) => dbo.put(jf.name, parseArray(arr, formats)) - case JObject(jo) => dbo.put(jf.name, parseObject(jo, formats)) - case jv: JValue => dbo.put(jf.name, renderValue(jv, formats)) + case JsonObjectId(objectId) => dbo.put(jf.name, objectId) + case JsonRegex(regex) => dbo.put(jf.name, regex) + case JsonUUID(uuid) => dbo.put(jf.name, uuid) + case JsonDate(date) => dbo.put(jf.name, date) + case JArray(arr) => dbo.put(jf.name, parseArray(arr)) + case JObject(jo) => dbo.put(jf.name, parseObject(jo)) + case jv: JValue => dbo.put(jf.name, renderValue(jv)) } } dbo } - private def renderValue(jv: JValue, formats: Formats): Object = jv match { + private def renderValue(jv: JValue)(implicit formats: Formats): Object = jv match { case JBool(b) => java.lang.Boolean.valueOf(b) case JInt(n) => renderInteger(n) case JDouble(n) => new java.lang.Double(n) diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JsonExtractors.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JsonExtractors.scala new file mode 100644 index 0000000000..3a4a7c299d --- /dev/null +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/JsonExtractors.scala @@ -0,0 +1,107 @@ +/** + * Copyright 2014 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 mongodb + +import json._ +import util.Helpers.tryo +import JsonDSL._ + +import java.util.{Date, UUID} +import java.util.regex.Pattern + +import org.bson.types.ObjectId +import org.joda.time.DateTime + +object JsonObjectId { + def unapply(json: JValue): Option[ObjectId] = { + json match { + case JObject(JField("$oid", JString(objectIdString)) :: Nil) if ObjectId.isValid(objectIdString) => + Some(new ObjectId(objectIdString)) + case _ => + None + } + } + + def apply(objectId: ObjectId): JValue = ("$oid" -> objectId.toString) + + def asJValue(objectId: ObjectId, formats: Formats): JValue = + if (isObjectIdSerializerUsed(formats)) + apply(objectId) + else + JString(objectId.toString) + + /** + * Check to see if the ObjectIdSerializer is being used. + */ + private def isObjectIdSerializerUsed(formats: Formats): Boolean = + formats.customSerializers.exists(_.getClass == objectIdSerializerClass) + + private val objectIdSerializerClass = classOf[net.liftweb.mongodb.ObjectIdSerializer] +} + +object JsonRegex { + def unapply(json: JValue): Option[Pattern] = { + json match { + case JObject(JField("$regex", JString(regex)) :: JField("$flags", JInt(f)) :: Nil) => + Some(Pattern.compile(regex, f.intValue)) + case _ => + None + } + } + + def apply(p: Pattern): JValue = ("$regex" -> p.pattern) ~ ("$flags" -> p.flags) +} + +object JsonUUID { + def unapply(json: JValue): Option[UUID] = { + json match { + case JObject(JField("$uuid", JString(s)) :: Nil) => + tryo(UUID.fromString(s)) + case _ => + None + } + } + + def apply(uuid: UUID): JValue = ("$uuid" -> uuid.toString) +} + +object JsonDate { + def unapply(json: JValue)(implicit formats: Formats): Option[Date] = { + json match { + case JObject(JField("$dt", JString(s)) :: Nil) => + formats.dateFormat.parse(s) + case _ => + None + } + } + + def apply(dt: Date)(implicit formats: Formats): JValue = ("$dt" -> formats.dateFormat.format(dt)) +} + +object JsonDateTime { + def unapply(json: JValue)(implicit formats: Formats): Option[DateTime] = { + json match { + case JObject(JField("$dt", JString(s)) :: Nil) => + formats.dateFormat.parse(s).map(dt => new DateTime(dt)) + case _ => + None + } + } + + def apply(dt: DateTime)(implicit formats: Formats): JValue = ("$dt" -> formats.dateFormat.format(dt.toDate)) +} diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Meta.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Meta.scala index 98e3f08963..d005c79519 100644 --- a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Meta.scala +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Meta.scala @@ -81,9 +81,9 @@ private[mongodb] object Meta { def datetype_?(clazz: Class[_]) = datetypes contains clazz def datetype2jvalue(a: Any)(implicit formats: Formats) = a match { - case x: Calendar => dateAsJValue(x.getTime, formats) - case x: Date => dateAsJValue(x, formats) - case x: DateTime => dateAsJValue(x.toDate, formats) + case x: Calendar => JsonDate(x.getTime)(formats) + case x: Date => JsonDate(x)(formats) + case x: DateTime => JsonDateTime(x)(formats) } def datetype2dbovalue(a: Any) = a match { @@ -104,31 +104,27 @@ private[mongodb] object Meta { * Definitive place for JValue conversion of mongo types */ def mongotype2jvalue(a: Any)(implicit formats: Formats) = a match { - case x: ObjectId => objectIdAsJValue(x, formats) - case x: Pattern => patternAsJValue(x) - case x: UUID => uuidAsJValue(x) + case x: ObjectId => JsonObjectId.asJValue(x, formats) + case x: Pattern => JsonRegex(x) + case x: UUID => JsonUUID(x) case x: DBRef => sys.error("DBRefs are not supported.") case _ => sys.error("not a mongotype " + a.asInstanceOf[AnyRef].getClass) } } + @deprecated("use JsonDate.apply", "2.6") def dateAsJValue(d: Date, formats: Formats): JValue = ("$dt" -> formats.dateFormat.format(d)) - def objectIdAsJValue(oid: ObjectId): JValue = ("$oid" -> oid.toString) - def patternAsJValue(p: Pattern): JValue = ("$regex" -> p.pattern) ~ ("$flags" -> p.flags) - def uuidAsJValue(u: UUID): JValue = ("$uuid" -> u.toString) - + @deprecated("use JsonObjectId.apply", "2.6") + def objectIdAsJValue(oid: ObjectId): JValue = JsonObjectId(oid) + @deprecated("use JsonRegex.apply", "2.6") + def patternAsJValue(p: Pattern): JValue = JsonRegex(p) + @deprecated("use JsonUUID.apply", "2.6") + def uuidAsJValue(u: UUID): JValue = JsonUUID(u) + + @deprecated("use JsonObjectId.asJValue", "2.6") def objectIdAsJValue(oid: ObjectId, formats: Formats): JValue = - if (isObjectIdSerializerUsed(formats)) - objectIdAsJValue(oid) - else - JString(oid.toString) + JsonObjectId.asJValue(oid, formats) - /* - * Check to see if the ObjectIdSerializer is being used. - */ - private def isObjectIdSerializerUsed(formats: Formats): Boolean = - formats.customSerializers.exists(_.getClass == objectIdSerializerClass) - private val objectIdSerializerClass = classOf[net.liftweb.mongodb.ObjectIdSerializer] } diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoMeta.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoMeta.scala index 4c11bedf8f..a53d674fdf 100644 --- a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoMeta.scala +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoMeta.scala @@ -18,6 +18,7 @@ import org.bson.types.ObjectId import json.{DefaultFormats, Formats} import json.JsonAST.JObject +import util.ConnectionIdentifier import com.mongodb.{BasicDBObject, DB, DBCollection, DBObject} @@ -35,10 +36,10 @@ trait JsonFormats { */ trait MongoMeta[BaseDocument] extends JsonFormats { + def connectionIdentifier: ConnectionIdentifier + // class name has a $ at the end. - private lazy val _collectionName = { - getClass.getName.split("\\.").toList.last.replace("$", "")+"s" - } + private lazy val _collectionName = getClass.getName.replaceAllLiterally("$", "") /* * Collection names should begin with letters or an underscore and may include @@ -49,9 +50,11 @@ trait MongoMeta[BaseDocument] extends JsonFormats { * -- the collection namespace is flat from the database's perspective. * From: http://www.mongodb.org/display/DOCS/Collections */ - def fixCollectionName = _collectionName.toLowerCase match { - case name if (name.contains("$")) => name.replace("$", "_d_") - case name => name + def fixCollectionName = { + val colName = MongoRules.collectionName.vend.apply(connectionIdentifier, _collectionName) + + if (colName.contains("$")) colName.replaceAllLiterally("$", "_d_") + else colName } /** @@ -117,12 +120,14 @@ trait MongoMeta[BaseDocument] extends JsonFormats { /* * Ensure an index exists */ + @deprecated("use createIndex(JObject) instead.", "2.6") def ensureIndex(keys: JObject): Unit = useColl { coll => coll.ensureIndex(JObjectParser.parse(keys)) } /* * Ensure an index exists and make unique */ + @deprecated("use createIndex(JObject, Boolean) instead.", "2.6") def ensureIndex(keys: JObject, unique: Boolean): Unit = { val options = new BasicDBObject if (unique) options.put("unique", true) @@ -131,14 +136,28 @@ trait MongoMeta[BaseDocument] extends JsonFormats { } } + def createIndex(keys: JObject, unique: Boolean = false): Unit = { + val options = new BasicDBObject + if (unique) options.put("unique", true) + useColl { coll => + coll.createIndex(JObjectParser.parse(keys), options) + } + } + /* * Ensure an index exists with options */ + @deprecated("use createIndex(JObject, JObject) instead.", "2.6") def ensureIndex(keys: JObject, opts: JObject): Unit = useColl { coll => coll.ensureIndex(JObjectParser.parse(keys), JObjectParser.parse(opts)) } + def createIndex(keys: JObject, opts: JObject): Unit = + useColl { coll => + coll.createIndex(JObjectParser.parse(keys), JObjectParser.parse(opts)) + } + /* * Update document with a DBObject query using the given Mongo instance. */ diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoRules.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoRules.scala new file mode 100644 index 0000000000..b574974421 --- /dev/null +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/MongoRules.scala @@ -0,0 +1,36 @@ +/** + * Copyright 2014 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 mongodb + +import util.{ConnectionIdentifier, SimpleInjector} +import util.Helpers._ + +object MongoRules extends SimpleInjector { + private def defaultCollectionNameFunc(conn: ConnectionIdentifier, name: String): String = { + charSplit(name, '.').last.toLowerCase+"s" + } + + /** + * Calculate the name of a collection based on the full + * class name of the MongoDocument/MongoRecord. Must be + * set in Boot before any code that touches the + * MongoDocumentMeta/MongoMetaRecord. + * + * To get snake_case, use this + * + * RecordRules.collectionName.default.set((_,name) => StringHelpers.snakify(name)) + */ + val collectionName = new Inject[(ConnectionIdentifier, String) => String](defaultCollectionNameFunc _) {} +} diff --git a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Serializers.scala b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Serializers.scala index 37b102c234..7c1ae6d822 100644 --- a/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Serializers.scala +++ b/persistence/mongodb/src/main/scala/net/liftweb/mongodb/Serializers.scala @@ -1,5 +1,5 @@ /* -* Copyright 2010-2011 WorldWide Conferencing, LLC +* Copyright 2010-2014 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. @@ -11,15 +11,15 @@ * limitations under the License. */ -package net.liftweb -package mongodb +package net.liftweb +package mongodb import json.{Formats, MappingException, Serializer, TypeInfo} import json.JsonAST._ import java.util.{Date, UUID} -import java.util.regex.{Pattern, PatternSyntaxException} +import java.util.regex.Pattern import org.bson.types.ObjectId @@ -36,14 +36,13 @@ class ObjectIdSerializer extends Serializer[ObjectId] { def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), ObjectId] = { case (TypeInfo(ObjectIdClass, _), json) => json match { - case JObject(JField("$oid", JString(s)) :: Nil) if (ObjectId.isValid(s)) => - new ObjectId(s) + case JsonObjectId(objectId) => objectId case x => throw new MappingException("Can't convert " + x + " to ObjectId") } } def serialize(implicit formats: Formats): PartialFunction[Any, JValue] = { - case x: ObjectId => Meta.objectIdAsJValue(x) + case x: ObjectId => JsonObjectId(x) } } @@ -59,14 +58,13 @@ class PatternSerializer extends Serializer[Pattern] { def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), Pattern] = { case (TypeInfo(PatternClass, _), json) => json match { - case JObject(JField("$regex", JString(s)) :: JField("$flags", JInt(f)) :: Nil) => - Pattern.compile(s, f.intValue) + case JsonRegex(regex) => regex case x => throw new MappingException("Can't convert " + x + " to Pattern") } } def serialize(implicit formats: Formats): PartialFunction[Any, JValue] = { - case x: Pattern => Meta.patternAsJValue(x) + case x: Pattern => JsonRegex(x) } } @@ -81,14 +79,13 @@ class DateSerializer extends Serializer[Date] { def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), Date] = { case (TypeInfo(DateClass, _), json) => json match { - case JObject(JField("$dt", JString(s)) :: Nil) => - format.dateFormat.parse(s).getOrElse(throw new MappingException("Can't parse "+ s + " to Date")) + case JsonDate(dt) => dt case x => throw new MappingException("Can't convert " + x + " to Date") } } def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case x: Date => Meta.dateAsJValue(x, format) + case x: Date => JsonDate(x) } } @@ -103,14 +100,13 @@ class DateTimeSerializer extends Serializer[DateTime] { def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), DateTime] = { case (TypeInfo(DateTimeClass, _), json) => json match { - case JObject(JField("$dt", JString(s)) :: Nil) => - new DateTime(format.dateFormat.parse(s).getOrElse(throw new MappingException("Can't parse "+ s + " to DateTime"))) + case JsonDateTime(dt) => dt case x => throw new MappingException("Can't convert " + x + " to Date") } } - def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case x: DateTime => Meta.dateAsJValue(x.toDate, format) + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case x: DateTime => JsonDateTime(x) } } @@ -125,13 +121,13 @@ class UUIDSerializer extends Serializer[UUID] { def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), UUID] = { case (TypeInfo(UUIDClass, _), json) => json match { - case JObject(JField("$uuid", JString(s)) :: Nil) => UUID.fromString(s) - case x => throw new MappingException("Can't convert " + x + " to Date") + case JsonUUID(uuid) => uuid + case x => throw new MappingException("Can't convert " + x + " to UUID") } } def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case x: UUID => Meta.uuidAsJValue(x) + case x: UUID => JsonUUID(x) } } diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/BsonDSLSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/BsonDSLSpec.scala index d2230093c4..2b8be81366 100644 --- a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/BsonDSLSpec.scala +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/BsonDSLSpec.scala @@ -27,6 +27,7 @@ import java.util.{Date, UUID} import java.util.regex.Pattern import org.bson.types.ObjectId +import org.joda.time.DateTime import org.specs2.mutable.Specification import com.mongodb.{BasicDBList, DBObject} @@ -75,6 +76,8 @@ object BsonDSLSpec extends Specification { ptrnList(i).pattern must_== ptrnList2(i).pattern ptrnList(i).flags must_== ptrnList2(i).flags } + + ptrnList2.length must_== ptrnList.length } "Convert Regex properly" in { @@ -122,5 +125,24 @@ object BsonDSLSpec extends Specification { dateList2 must_== dateList } + + "Convert DateTime properly" in { + implicit val formats = DefaultFormats.lossless + val dt: DateTime = new DateTime + val qry: JObject = ("now" -> dt) + val dbo: DBObject = JObjectParser.parse(qry) + + new DateTime(dbo.get("now")) must_== dt + } + + "Convert List[DateTime] properly" in { + implicit val formats = DefaultFormats.lossless + val dateList = new DateTime :: new DateTime :: new DateTime :: Nil + val qry: JObject = ("dts" -> dateList) + val dbo: DBObject = JObjectParser.parse(qry) + val dateList2: List[DateTime] = dbo.get("dts").asInstanceOf[BasicDBList].toList.map(_.asInstanceOf[Date]).map(d => new DateTime(d)) + + dateList2 must_== dateList + } } } diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/CustomSerializersSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/CustomSerializersSpec.scala index 3f0de3256c..3f5a61032e 100644 --- a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/CustomSerializersSpec.scala +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/CustomSerializersSpec.scala @@ -114,10 +114,9 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val jack2 = Person.find(jack._id) - jack2.isDefined must_== true - jack2.toList map { j => - j._id mustEqual jack._id + Person.find(jack._id) must beLike { + case Some(j) => + j._id mustEqual jack._id } } @@ -131,10 +130,9 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val jack2 = PersonWithObjectId.find(jack._id) - jack2.isDefined must_== true - jack2.toList map { j => - j._id mustEqual jack._id + PersonWithObjectId.find(jack._id) must beLike { + case Some(j) => + j._id mustEqual jack._id } } @@ -149,11 +147,10 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val jack2 = PersonWithPattern.find(jack._id) - jack2.isDefined must_== true - jack2.toList map { j => - j.pattern.pattern mustEqual jack.pattern.pattern - j.pattern.flags mustEqual jack.pattern.flags + PersonWithPattern.find(jack._id) must beLike { + case Some(j) => + j.pattern.pattern mustEqual jack.pattern.pattern + j.pattern.flags mustEqual jack.pattern.flags } } @@ -168,10 +165,9 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val findJack = PersonWithDateTime.find(jack._id) - findJack.isDefined must_== true - findJack.toList map { j => - j.birthDate mustEqual jack.birthDate + PersonWithDateTime.find(jack._id) must beLike { + case Some(j) => + j.birthDate mustEqual jack.birthDate } } @@ -188,10 +184,9 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val findJack = PersonWithDate.find(jack._id) - findJack.isDefined must_== true - findJack.toList map { j => - j.birthDate mustEqual jack.birthDate + PersonWithDate.find(jack._id) must beLike { + case Some(j) => + j.birthDate mustEqual jack.birthDate } } @@ -206,10 +201,9 @@ class CustomSerializersSpec extends Specification with MongoTestKit { jack.save // retrieve it and compare - val findJack = PersonWithUUID.find(jack._id) - findJack.isDefined must_== true - findJack.toList map { j => - j._id mustEqual jack._id + PersonWithUUID.find(jack._id) must beLike { + case Some(j) => + j._id mustEqual jack._id } } } diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/JObjectParserSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/JObjectParserSpec.scala index da650061b0..36f08ddf0e 100644 --- a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/JObjectParserSpec.scala +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/JObjectParserSpec.scala @@ -40,20 +40,22 @@ object JObjectParserSpec extends Specification { val (oid, dbo) = buildTestData val xval = tryo(dbo.get("x").asInstanceOf[ObjectId]) - xval.isDefined must_== true xval.toList map { x => x must_== oid } + + xval.isDefined must_== true } "not convert strings to ObjectId when configured not to" in { JObjectParser.stringProcessor.doWith((s: String) => s) { val (oid, dbo) = buildTestData val xval = tryo(dbo.get("x").asInstanceOf[String]) - xval.isDefined must_== true xval.toList map { x => x must_== oid.toString } + + xval.isDefined must_== true } } } diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoDocumentExamplesSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoDocumentExamplesSpec.scala index b3384e26db..1659c928bb 100644 --- a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoDocumentExamplesSpec.scala +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoDocumentExamplesSpec.scala @@ -635,8 +635,8 @@ class MongoDocumentExamplesSpec extends Specification with MongoTestKit { val pdoc1 = PatternDoc(ObjectId.get, Pattern.compile("^Mo", Pattern.CASE_INSENSITIVE)) pdoc1.save - PatternDoc.find(pdoc1._id).toList map { - pdoc => + PatternDoc.find(pdoc1._id) must beLike { + case Some(pdoc) => pdoc._id must_== pdoc1._id pdoc.regx.pattern must_== pdoc1.regx.pattern pdoc.regx.flags must_== pdoc1.regx.flags @@ -659,15 +659,14 @@ class MongoDocumentExamplesSpec extends Specification with MongoTestKit { } val fromDb = StringDateDoc.find(newId) - fromDb.isDefined must_== true - fromDb.toList flatMap { - sdd => + fromDb must beLike { + case Some(sdd) => sdd._id must_== newId sdd.dt must_== newDt sdd.save - StringDateDoc.find(newId) map { - sdd2 => + StringDateDoc.find(newId) must beLike { + case Some(sdd2) => sdd2.dt must_== sdd.dt } } diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoRulesSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoRulesSpec.scala new file mode 100644 index 0000000000..7a4517ef3d --- /dev/null +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/MongoRulesSpec.scala @@ -0,0 +1,49 @@ +/** + * Copyright 2014 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 mongodb + +import common._ +import util.Helpers._ + +import org.specs2.mutable._ + +import org.bson.types.ObjectId + +case class CollectionNameTestDoc(_id: ObjectId) extends MongoDocument[CollectionNameTestDoc] { + def meta = CollectionNameTestDoc +} +object CollectionNameTestDoc extends MongoDocumentMeta[CollectionNameTestDoc] + +/** + * Systems under specification for MongoRules. + */ +object MongoRulesSpec extends Specification { + "Mongo Rules Specification".title + sequential + + "MongoRules" should { + "default collection name" in { + CollectionNameTestDoc.collectionName must_== "collectionnametestdocs" + } + "snakify collection name" in { + MongoRules.collectionName.doWith((_, name) => snakify(name)+"s") { + CollectionNameTestDoc.collectionName must_== "net.liftweb.mongodb.collection_name_test_docs" + } + } + } +} diff --git a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/QueryExamplesSpec.scala b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/QueryExamplesSpec.scala index 0ddfb90f9e..0a640b23b4 100644 --- a/persistence/mongodb/src/test/scala/net/liftweb/mongodb/QueryExamplesSpec.scala +++ b/persistence/mongodb/src/test/scala/net/liftweb/mongodb/QueryExamplesSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2011 WorldWide Conferencing, LLC + * Copyright 2011-2014 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. @@ -33,7 +33,7 @@ package queryexamplesfixtures { object Person extends MongoDocumentMeta[Person] { override def formats = allFormats // index name - ensureIndex(("name" -> 1)) + createIndex(("name" -> 1)) // implicit formats already exists def findAllBornAfter(dt: Date) = findAll(("birthDate" -> ("$gt" -> dt))) diff --git a/persistence/record/src/main/scala/net/liftweb/record/Field.scala b/persistence/record/src/main/scala/net/liftweb/record/Field.scala index 89c26396d8..957afcbed2 100644 --- a/persistence/record/src/main/scala/net/liftweb/record/Field.scala +++ b/persistence/record/src/main/scala/net/liftweb/record/Field.scala @@ -18,6 +18,7 @@ package net.liftweb package record import net.liftweb.common._ +import net.liftweb.http.S import net.liftweb.http.js.{JsExp} import net.liftweb.json.JsonAST.{JNothing, JNull, JString, JValue} import net.liftweb.util._ @@ -148,6 +149,11 @@ trait OwnedField[OwnerType <: Record[OwnerType]] extends BaseField { */ def name: String = RecordRules.fieldName.vend.apply(owner.meta.connectionIdentifier, fieldName) + /** + * The display name of this field (e.g., "First Name") + */ + override def displayName: String = RecordRules.displayName.vend.apply(owner, S.locale, name) + /** * Are we in "safe" mode (i.e., the value of the field can be read or written without any security checks.) */ diff --git a/persistence/record/src/main/scala/net/liftweb/record/RecordRules.scala b/persistence/record/src/main/scala/net/liftweb/record/RecordRules.scala index 285539dbc0..1db9ad4efc 100644 --- a/persistence/record/src/main/scala/net/liftweb/record/RecordRules.scala +++ b/persistence/record/src/main/scala/net/liftweb/record/RecordRules.scala @@ -21,6 +21,8 @@ import http.Factory import util.ConnectionIdentifier import util.Helpers._ +import java.util.Locale + object RecordRules extends Factory { /** * Calculate the name of a field based on the name @@ -32,4 +34,12 @@ object RecordRules extends Factory { * RecordRules.fieldName.default.set((_, name) => StringHelpers.snakify(name)) */ val fieldName = new FactoryMaker[(ConnectionIdentifier, String) => String]((_: ConnectionIdentifier, name: String) => name) {} + + /** + * This function is used to calculate the displayName of a field. Can be + * used to easily localize fields based on the locale in the + * current request + */ + val displayName: FactoryMaker[(Record[_], Locale, String) => String] = + new FactoryMaker[(Record[_], Locale, String) => String]((m: Record[_], l: Locale, name: String) => name) {} } diff --git a/persistence/record/src/test/scala/net/liftweb/record/FieldSpec.scala b/persistence/record/src/test/scala/net/liftweb/record/FieldSpec.scala index 1a6c111f49..ce83e63b11 100644 --- a/persistence/record/src/test/scala/net/liftweb/record/FieldSpec.scala +++ b/persistence/record/src/test/scala/net/liftweb/record/FieldSpec.scala @@ -272,14 +272,14 @@ object FieldSpec extends Specification { val session = new LiftSession("", randomString(20), Empty) S.initIfUninitted(session) { val formXml = mandatory.toForm - formXml.isDefined must_== true - formXml.toList map { fprime => - val f = ("* [name]" #> ".*" & "select *" #> (((ns: NodeSeq) => ns.filter { - case e: Elem => e.attribute("selected").map(_.text) == Some("selected") - case _ => false - }) andThen "* [value]" #> ".*"))(fprime) - val ret: Boolean = Helpers.compareXml(f, fp) - ret must_== true + formXml must beLike { + case Full(fprime) => + val f = ("* [name]" #> ".*" & "select *" #> (((ns: NodeSeq) => ns.filter { + case e: Elem => e.attribute("selected").map(_.text) == Some("selected") + case _ => false + }) andThen "* [value]" #> ".*"))(fprime) + val ret: Boolean = Helpers.compareXml(f, fp) + ret must_== true } } } diff --git a/persistence/record/src/test/scala/net/liftweb/record/RecordRulesSpec.scala b/persistence/record/src/test/scala/net/liftweb/record/RecordRulesSpec.scala index 68041cc645..9676aaf1e1 100644 --- a/persistence/record/src/test/scala/net/liftweb/record/RecordRulesSpec.scala +++ b/persistence/record/src/test/scala/net/liftweb/record/RecordRulesSpec.scala @@ -42,5 +42,12 @@ object RecordRulesSpec extends Specification { rec.fieldThree.name must_== "field_three" } } + "camelify custom field display name" in { + RecordRules.displayName.doWith((_, _, name) => camelify(name)) { + val rec = BasicTestRecord.createRecord + + rec.fieldThree.displayName must_== "FieldThree" + } + } } } diff --git a/persistence/record/src/test/scala/net/liftweb/record/RecordSpec.scala b/persistence/record/src/test/scala/net/liftweb/record/RecordSpec.scala index 0f6548d2ec..7224188102 100644 --- a/persistence/record/src/test/scala/net/liftweb/record/RecordSpec.scala +++ b/persistence/record/src/test/scala/net/liftweb/record/RecordSpec.scala @@ -24,7 +24,7 @@ import org.specs2.specification.Fragment import org.joda.time._ import http.js.JE._ -import common.Empty +import common._ import http.{S, LiftSession} import json._ import util._ @@ -39,7 +39,7 @@ import JsonDSL._ /** * Systems under specification for Record. */ -object RecordSpec extends Specification { +object RecordSpec extends Specification { "Record Specification".title "Record field introspection" should { @@ -54,15 +54,18 @@ object RecordSpec extends Specification { } "correctly look up fields by name" in { - for (name <- allExpectedFieldNames) yield { - rec.fieldByName(name).isDefined must_== true - } + val fields = allExpectedFieldNames.flatMap(rec.fieldByName _) + + fields.length must_== allExpectedFieldNames.length } "not look up fields by bogus names" in { - for (name <- allExpectedFieldNames) yield { - rec.fieldByName("x" + name + "y").isDefined must_== false - } + val fields = + allExpectedFieldNames.flatMap { name => + rec.fieldByName("x" + name + "y") + } + + fields.length must_== 0 } "ignore synthetic methods" in { @@ -331,10 +334,8 @@ object RecordSpec extends Specification { "get set from json string using lift-json parser" in { S.initIfUninitted(new LiftSession("", randomString(20), Empty)) { val fttrFromJson = FieldTypeTestRecord.fromJsonString(fttrJson) - fttrFromJson.isDefined must_== true - fttrFromJson.toList map { r => - r mustEqual fttr - } + + fttrFromJson must_== Full(fttr) } } } diff --git a/persistence/squeryl-record/src/test/scala/net/liftweb/squerylrecord/SquerylRecordSpec.scala b/persistence/squeryl-record/src/test/scala/net/liftweb/squerylrecord/SquerylRecordSpec.scala index a5ffb46966..2aba6c79f0 100644 --- a/persistence/squeryl-record/src/test/scala/net/liftweb/squerylrecord/SquerylRecordSpec.scala +++ b/persistence/squeryl-record/src/test/scala/net/liftweb/squerylrecord/SquerylRecordSpec.scala @@ -22,7 +22,7 @@ import org.squeryl.dsl.DateExpression import org.specs2.mutable.Specification import org.specs2.specification.AroundExample -import org.specs2.execute.Result +import org.specs2.execute.{ AsResult , Result } import record.{ BaseField, Record } import record.field._ @@ -44,6 +44,7 @@ class SquerylRecordSpec extends Specification with AroundExample { sequential lazy val session = new LiftSession("", Helpers.randomString(20), Empty) + // One of these is for specs2 2.x, the other for specs2 1.x protected def around[T <% Result](t: =>T) = { S.initIfUninitted(session) { DBHelper.initSquerylRecordWithInMemoryDB() @@ -52,6 +53,14 @@ class SquerylRecordSpec extends Specification with AroundExample { } } + protected def around[T : AsResult](t: =>T) = { + S.initIfUninitted(session) { + DBHelper.initSquerylRecordWithInMemoryDB() + DBHelper.createSchema() + AsResult(t) + } + } + "SquerylRecord" should { "load record by ID" in { transaction { @@ -379,6 +388,7 @@ class SquerylRecordSpec extends Specification with AroundExample { val company = from(companies)(company => select(company)).page(0, 1).single company.allFields map { f => f.dirty_? must_== false } + success } } class ToChar(d: DateExpression[Timestamp], e: StringExpression[String], m: OutMapper[String]) diff --git a/project/Build.scala b/project/Build.scala index 6d23f498b4..751064bf47 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 WorldWide Conferencing, LLC + * Copyright 2012-2014 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. @@ -34,12 +34,17 @@ object BuildDef extends Build { // Core Projects // ------------- lazy val core: Seq[ProjectReference] = - Seq(common, actor, markdown, json, json_scalaz, json_scalaz7, json_ext, util) + Seq(common, actor, markdown, json, json_scalaz7, json_ext, util) lazy val common = coreProject("common") .settings(description := "Common Libraties and Utilities", - libraryDependencies ++= Seq(slf4j_api, logback, slf4j_log4j12)) + libraryDependencies ++= Seq(slf4j_api, logback, slf4j_log4j12), + libraryDependencies <++= scalaVersion { + case "2.11.0" | "2.11.1" => Seq(scala_xml, scala_parser) + case _ => Seq() + } + ) lazy val actor = coreProject("actor") @@ -51,10 +56,12 @@ object BuildDef extends Build { coreProject("markdown") .settings(description := "Markdown Parser", parallelExecution in Test := false, - libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "1.9.1" % "test", - "junit" % "junit" % "4.8.2" % "test" - )) + libraryDependencies <++= scalaVersion { sv => Seq(scalatest(sv), junit) }, + libraryDependencies <++= scalaVersion { + case "2.11.0" | "2.11.1" => Seq(scala_xml, scala_parser) + case _ => Seq() + } + ) lazy val json = coreProject("json") @@ -99,10 +106,10 @@ object BuildDef extends Build { .dependsOn(util) .settings(description := "Testkit for Webkit Library", libraryDependencies ++= Seq(commons_httpclient, servlet_api)) - lazy val webkit = webProject("webkit") .dependsOn(util, testkit % "provided") + .settings(libraryDependencies += mockito_all) .settings(yuiCompressor.Plugin.yuiSettings: _*) .settings(description := "Webkit Library", parallelExecution in Test := false, diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1a69b8c5df..823358c456 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -23,13 +23,15 @@ object Dependencies { type ModuleMap = String => ModuleID - lazy val CVMapping210 = crossMapped("2.10.0" -> "2.10", "2.10.1" -> "2.10", "2.10.2" -> "2.10", "2.10.3" -> "2.10", "2.10.4" -> "2.10") + lazy val CVMappingAll = crossMapped("2.10.0" -> "2.10", "2.10.1" -> "2.10", "2.10.2" -> "2.10", "2.10.3" -> "2.10", "2.10.4" -> "2.10", "2.11.0" -> "2.11", "2.11.1" -> "2.11") lazy val slf4jVersion = "1.7.2" - lazy val scalazGroup = defaultOrMapped("org.scalaz") - lazy val scalazVersion = defaultOrMapped("6.0.4") - lazy val scalaz7Version = defaultOrMapped("7.0.0") + lazy val scalazGroup = defaultOrMapped("org.scalaz") + lazy val scalazVersion = defaultOrMapped("6.0.4") + lazy val scalaz7Version = defaultOrMapped("7.0.0", "2.11.0" -> "7.0.6", "2.11.1" -> "7.0.6") + lazy val specs2Version = defaultOrMapped("1.12.3", "2.11.0" -> "2.3.11", "2.11.1" -> "2.3.11") + lazy val scalatestVersion = defaultOrMapped("1.9.1", "2.11.0" -> "2.1.3", "2.11.1" -> "2.1.3") // Compile scope: // Scope available in all classpath, transitive by default. @@ -40,16 +42,18 @@ object Dependencies { lazy val joda_time = "joda-time" % "joda-time" % "2.1" lazy val joda_convert = "org.joda" % "joda-convert" % "1.2" lazy val htmlparser = "nu.validator.htmlparser" % "htmlparser" % "1.4" - lazy val mongo_java_driver = "org.mongodb" % "mongo-java-driver" % "2.11.4" + lazy val mongo_java_driver = "org.mongodb" % "mongo-java-driver" % "2.12.2" lazy val paranamer = "com.thoughtworks.paranamer" % "paranamer" % "2.4.1" - lazy val scalajpa = "org.scala-libs" % "scalajpa" % "1.4" + lazy val scalajpa = "org.scala-libs" % "scalajpa" % "1.5" cross CVMappingAll lazy val scalap: ModuleMap = "org.scala-lang" % "scalap" % _ lazy val scala_compiler: ModuleMap = "org.scala-lang" % "scala-compiler" % _ - lazy val scalaz_core: ModuleMap = sv => scalazGroup(sv) % "scalaz-core" % scalazVersion(sv) cross CVMapping210 - lazy val scalaz7_core: ModuleMap = sv => scalazGroup(sv) % "scalaz-core" % scalaz7Version(sv) cross CVMapping210 + lazy val scalaz_core: ModuleMap = sv => scalazGroup(sv) % "scalaz-core" % scalazVersion(sv) cross CVMappingAll + lazy val scalaz7_core: ModuleMap = sv => scalazGroup(sv) % "scalaz-core" % scalaz7Version(sv) cross CVMappingAll lazy val slf4j_api = "org.slf4j" % "slf4j-api" % slf4jVersion - lazy val squeryl = "org.squeryl" % "squeryl" % "0.9.5-6" cross CVMapping210 + lazy val squeryl = "org.squeryl" % "squeryl" % "0.9.5-7" cross CVMappingAll + lazy val scala_xml = "org.scala-lang.modules" %% "scala-xml" % "1.0.1" lazy val rhino = "org.mozilla" % "rhino" % "1.7R4" + lazy val scala_parser = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.1" // Aliases lazy val mongo_driver = mongo_java_driver @@ -75,7 +79,6 @@ object Dependencies { lazy val h2 = h2database - // Test scope: // Scope available only in test classpath, non-transitive by default. // TODO: See if something alternative with lesser footprint can be used instead of mega heavy apacheds @@ -83,8 +86,9 @@ object Dependencies { lazy val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.26" % "test" lazy val jwebunit = "net.sourceforge.jwebunit" % "jwebunit-htmlunit-plugin" % "2.5" % "test" lazy val mockito_all = "org.mockito" % "mockito-all" % "1.9.0" % "test" - lazy val scalacheck = "org.scalacheck" %% "scalacheck" % "1.10.0" % "test" - lazy val specs2: ModuleMap = - "org.specs2" %% "specs2" % defaultOrMapped("1.12.3")(_) % "test" + lazy val scalacheck = "org.scalacheck" %% "scalacheck" % "1.10.1" % "test" + lazy val specs2: ModuleMap = sv => "org.specs2" %% "specs2" % specs2Version(sv) % "test" + lazy val scalatest: ModuleMap = sv => "org.scalatest" %% "scalatest" % scalatestVersion(sv) % "test" + lazy val junit = "junit" % "junit" % "4.8.2" % "test" } diff --git a/project/build.properties b/project/build.properties index a2a2e1da53..4bedffccc8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ # Deprecate using build.properties, use -Dsbt.version=... in launcher arg instead -sbt.version=0.12.4 +sbt.version=0.13.5 diff --git a/project/plugins.sbt b/project/plugins.sbt index 91927a54fb..436c7c0a40 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,5 @@ DefaultOptions.addPluginResolvers -addSbtPlugin("in.drajit.sbt" % "sbt-yui-compressor" % "0.2.1") - resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" -addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.1.0") \ No newline at end of file +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") diff --git a/project/project/Plugin.scala b/project/project/Plugin.scala index f5b6dc0fd2..ecd25ad4f3 100644 --- a/project/project/Plugin.scala +++ b/project/project/Plugin.scala @@ -1,6 +1,7 @@ import sbt._ object PluginDef extends Build { - lazy val root = Project("plugins", file(".")) dependsOn(buildPlugin) - lazy val buildPlugin = uri("git://github.com/lift/sbt-lift-build.git#c78f617f62") + lazy val root = Project("plugins", file(".")) dependsOn(buildPlugin, yuiCompressorPlugin) + lazy val buildPlugin = uri("git://github.com/lift/sbt-lift-build.git#724fb133beac77bbd06d3fb8ea086a1c88ee2a7d") + lazy val yuiCompressorPlugin = uri("git://github.com/indrajitr/sbt-yui-compressor.git#89304ec0c988183d1f1a889e665e0269fe513031") } diff --git a/project/sbt-launch-0.12.1.jar b/project/sbt-launch-0.12.1.jar deleted file mode 100644 index 06ad8d8805..0000000000 Binary files a/project/sbt-launch-0.12.1.jar and /dev/null differ diff --git a/unsafePublishLift.sh b/unsafePublishLift.sh index 74949f632e..4e2468cba2 100755 --- a/unsafePublishLift.sh +++ b/unsafePublishLift.sh @@ -157,7 +157,7 @@ for MODULE in framework ; do # Do a separate build for each configured Scala version so we don't blow the heap for SCALA_VERSION in $(grep crossScalaVersions build.sbt | cut -d '(' -f 2 | sed s/[,\)\"]//g ); do echo -n " Building against Scala ${SCALA_VERSION}..." - if ! ./liftsh ++${SCALA_VERSION} clean update test publish-signed >> ${BUILDLOG} ; then + if ! ./liftsh ++${SCALA_VERSION} clean update test publishSigned >> ${BUILDLOG} ; then echo "failed! See build log for details" exit fi @@ -169,7 +169,7 @@ for MODULE in framework ; do done echo -e "\n\nRelease complete!" -echo -e "\n\nPlease update the lift_sbt_2.5 templates!" +echo -e "\n\nPlease update the lift_sbt_2.6 templates!" echo -e "\n\nand write something about this release on the liftweb.net site." diff --git a/web/testkit/src/main/scala/net/liftweb/http/testing/TestFramework.scala b/web/testkit/src/main/scala/net/liftweb/http/testing/TestFramework.scala index 2b99a01845..2f1cf7d7f4 100644 --- a/web/testkit/src/main/scala/net/liftweb/http/testing/TestFramework.scala +++ b/web/testkit/src/main/scala/net/liftweb/http/testing/TestFramework.scala @@ -931,6 +931,8 @@ abstract class BaseResponse(override val baseUrl: String, f(st) st } + + def withFilter(f: FuncType => Unit): FuncType = this.filter(f) } class CompleteFailure(val serverName: String, val exception: Box[Throwable]) extends TestResponse { 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 18ddb00070..dd3b6e09ba 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/CometActor.scala @@ -771,6 +771,21 @@ trait CometActor extends LiftActor with LiftCometActor with CssBindImplicits { protected implicit def nodeSeqFuncToBoxNodeSeq(f: NodeSeq => NodeSeq): Box[NodeSeq] = Full(f(defaultHtml)) + /** + * By default, `CometActor` handles `RedirectShortcutException`, which is + * used to handle many types of redirects in Lift. If you override this + * `PartialFunction` to do your own exception handling and want redirects + * from e.g. `S.redirectTo` to continue working correctly, make sure you + * chain back to this implementation. + */ + override def exceptionHandler : PartialFunction[Throwable, Unit] = { + case ResponseShortcutException(_, Full(redirectUri), _) => + partialUpdate(RedirectTo(redirectUri)) + + case other if super.exceptionHandler.isDefinedAt(other) => + super.exceptionHandler(other) + } + /** * Handle messages sent to this Actor before the */ 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 1004329d3e..719d31ac21 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala @@ -20,8 +20,7 @@ package http import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap -import collection.mutable.{HashMap, ListBuffer} -import collection.concurrent.{Map=>ConcurrentMap} +import scala.collection.mutable.{HashMap, ListBuffer} import collection.JavaConversions import collection.mutable.{HashMap, ListBuffer} @@ -155,24 +154,38 @@ object LiftSession { /** * Cache for findSnippetClass lookups. */ - private val snippetClassMap: ConcurrentMap[String, Box[Class[AnyRef]]] = JavaConversions.mapAsScalaConcurrentMap(new ConcurrentHashMap()) + private val snippetClassMap = new ConcurrentHashMap[String, Box[Class[AnyRef]]]() /* * Given a Snippet name, try to determine the fully-qualified Class * so that we can instantiate it via reflection. */ def findSnippetClass(name: String): Box[Class[AnyRef]] = { - if (name == null) Empty - else { - snippetClassMap.getOrElseUpdate(name,{ - // Name might contain some relative packages, so split them out and put them in the proper argument of findClass - val (packageSuffix, terminal) = name.lastIndexOf('.') match { - case -1 => ("", name) - case i => ("." + name.substring(0, i), name.substring(i + 1)) - } - findClass(terminal, LiftRules.buildPackage("snippet").map(_ + packageSuffix) ::: - (("lift.app.snippet" + packageSuffix) :: ("net.liftweb.builtin.snippet" + packageSuffix) :: Nil)) - }) + if (name == null) { + Empty + } else { + // putIfAbsent isn't lazy, so we pay the price of checking for the + // absence twice when the snippet hasn't been initialized to avoid + // the cost of computing the snippet class every time. + // + // We're using ConcurrentHashMap, so no `getOrElseUpdate` here (and + // `getOrElseUpdate` isn't atomic anyway). + if (! snippetClassMap.contains(name)) { + snippetClassMap.putIfAbsent(name, { + // Name might contain some relative packages, so split them out and put them in the proper argument of findClass + val (packageSuffix, terminal) = name.lastIndexOf('.') match { + case -1 => ("", name) + case i => ("." + name.substring(0, i), name.substring(i + 1)) + } + findClass(terminal, LiftRules.buildPackage("snippet").map(_ + packageSuffix) ::: + (("lift.app.snippet" + packageSuffix) :: ("net.liftweb.builtin.snippet" + packageSuffix) :: Nil)) + }) + } + + // We don't test for null because we never remove an item from the + // map. If I'm wrong about this and you're seeing null pointers, + // add a legacyNullTest openOr Empty. + snippetClassMap.get(name) } } @@ -762,7 +775,6 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, */ def runParams(state: Req): List[Any] = { - val toRun = { // get all the commands, sorted by owner, (state.uploadedFiles.map(_.name) ::: state.paramNames) @@ -1098,7 +1110,6 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, !nmessageCallback.find(_._2.owner == owner).isEmpty } - private def shutDown() = { import scala.collection.JavaConversions._ @@ -1113,7 +1124,6 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String, SessionMaster.sendMsg(RemoveSession(this.uniqueId)) - import scala.collection.JavaConversions._ nasyncComponents.foreach { case (_, comp) => done ::= (() => tryo(comp ! ShutDown)) @@ -2965,7 +2975,6 @@ private object SnippetNode { } } - private def isLiftClass(s: String): Boolean = s.startsWith("lift:") || s.startsWith("l:") diff --git a/web/webkit/src/main/scala/net/liftweb/http/Req.scala b/web/webkit/src/main/scala/net/liftweb/http/Req.scala index 9c4242cb1f..75a0ca75bc 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/Req.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/Req.scala @@ -984,9 +984,29 @@ class Req(val path: ParsePath, sid <- httpRequest.sessionId } yield sid - lazy val json: Box[JsonAST.JValue] = - if (!json_?) Empty - else try { + /** + * The JValue representation of this Req's body, if the body is JSON-parsable + * AND the content-type of the request is JSON. Returns a Failure if + * the request is not considered a JSON request (see json_?), or if + * there was an error parsing the JSON. + * + * If you want to forcibly evaluate the request body as JSON, ignoring + * content type, see `forcedBodyAsJson`. + */ + lazy val json: Box[JsonAST.JValue] = { + if (!json_?) { + Failure("Cannot parse non-JSON request as JSON; please check content-type.") + } else { + forcedBodyAsJson + } + } + + /** + * Forcibly tries to parse the request body as JSON. Does not perform any + * content type checks, unlike the json method. + */ + lazy val forcedBodyAsJson: Box[JsonAST.JValue] = { + try { import java.io._ def r = """; *charset=(.*)""".r @@ -1002,6 +1022,7 @@ class Req(val path: ParsePath, case e: LiftFlowOfControlException => throw e case e: Exception => Failure(e.getMessage, Full(e), Empty) } + } private def containerRequest = Box !! request /** @@ -1022,9 +1043,28 @@ class Req(val path: ParsePath, case (sch, port) => sch + "://" + r.serverName + ":" + port + contextPath }) openOr "" + /** + * The Elem representation of this Req's body, if the body is XML-parsable + * AND the content-type of the request is XML. Returns a Failure if + * the request is not considered a XML request (see xml_?), or if + * there was an error parsing the XML. + * + * If you want to forcibly evaluate the request body as XML, ignoring + * content type, see `forcedBodyAsXml`. + */ + lazy val xml: Box[Elem] = { + if (!xml_?) { + Failure("Cannot parse non-XML request as XML; please check content-type.") + } else { + forcedBodyAsXml + } + } - lazy val xml: Box[Elem] = if (!xml_?) Empty - else + /** + * Forcibly tries to parse the request body as XML. Does not perform any + * content type checks, unlike the xml method. + */ + lazy val forcedBodyAsXml: Box[Elem] = { try { import java.io._ body.map(b => XML.load(new ByteArrayInputStream(b))) @@ -1032,6 +1072,7 @@ class Req(val path: ParsePath, case e: LiftFlowOfControlException => throw e case e: Exception => Failure(e.getMessage, Full(e), Empty) } + } /** * The SiteMap Loc associated with this Req 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 511770d071..8de81892d5 100644 --- a/web/webkit/src/main/scala/net/liftweb/http/S.scala +++ b/web/webkit/src/main/scala/net/liftweb/http/S.scala @@ -19,7 +19,7 @@ package http import java.util.{Locale, TimeZone, ResourceBundle} -import collection.mutable.{HashMap, ListBuffer} +import scala.collection.mutable.{HashMap, ListBuffer} import xml._ import common._ @@ -1757,7 +1757,7 @@ trait S extends HasParams with Loggable with UserAgentCalculator { * @see # prefixedAttrsToMetaData ( String ) * @see # prefixedAttrsToMetaData ( String, Map ) */ - def attrs: List[(Either[String, (String, String)], String)] = S._attrs.value match { + def attrs: List[(Either[String, (String, String)], String)] = _attrs.value match { case null => Nil case (current,full) => full } diff --git a/web/webkit/src/main/scala/net/liftweb/mockweb/WebSpec.scala b/web/webkit/src/main/scala/net/liftweb/mockweb/WebSpec.scala index fbb1d6d3c9..bdac8a83e8 100644 --- a/web/webkit/src/main/scala/net/liftweb/mockweb/WebSpec.scala +++ b/web/webkit/src/main/scala/net/liftweb/mockweb/WebSpec.scala @@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletRequest import scala.xml.NodeSeq +import org.specs2.matcher.XmlMatchers import org.specs2.mutable._ import org.specs2.execute.Result @@ -43,7 +44,7 @@ import mocks.MockHttpServletRequest * initialize LiftRules, Mapper, etc. The simplest approach * is to just point this at your Boostrap.boot method. */ -abstract class WebSpec(boot : () => Any = () => {}) extends Specification { +abstract class WebSpec(boot : () => Any = () => {}) extends Specification with XmlMatchers { /** * This is our private spec instance of Liftrules. Everything we run will * use this instance instead of the global instance diff --git a/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgSpec.scala b/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgSpec.scala index 5dc335b037..bb543f68f3 100644 --- a/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgSpec.scala @@ -18,16 +18,16 @@ package net.liftweb package builtin.snippet import xml._ +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ import http._ - /** * System under specification for Msg. */ -object MsgSpec extends Specification { +object MsgSpec extends Specification with XmlMatchers { "Msg Specification".title def withSession[T](f: => T) : T = diff --git a/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgsSpec.scala b/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgsSpec.scala index 338f50be38..965b8a5256 100644 --- a/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgsSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/builtin/snippet/MsgsSpec.scala @@ -18,6 +18,7 @@ package net.liftweb package builtin.snippet import xml.XML +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ @@ -27,7 +28,7 @@ import http._ /** * System under specification for Msgs. */ -object MsgsSpec extends Specification { +object MsgsSpec extends Specification with XmlMatchers { "Msgs Specification".title def withSession[T](f: => T) : T = diff --git a/web/webkit/src/test/scala/net/liftweb/http/BindingsSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/BindingsSpec.scala index 79ec9393be..6f042263ca 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/BindingsSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/http/BindingsSpec.scala @@ -18,16 +18,18 @@ package net.liftweb package http import xml.{NodeSeq, Text} +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ import util.Helpers._ + /** * System under specification for Bindings. */ -object BindingsSpec extends Specification { +object BindingsSpec extends Specification with XmlMatchers { "Bindings Bindings".title case class MyClass(str: String, i: Int, other: MyOtherClass) diff --git a/web/webkit/src/test/scala/net/liftweb/http/CometActorSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/CometActorSpec.scala new file mode 100644 index 0000000000..6645975bfa --- /dev/null +++ b/web/webkit/src/test/scala/net/liftweb/http/CometActorSpec.scala @@ -0,0 +1,96 @@ +/* + * Copyright 2010-2014 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 http + +import scala.xml.NodeSeq + +import org.specs2.mutable.Specification + +import actor.LAScheduler +import common._ +import js.JsCmds._ + +object CometActorSpec extends Specification { + private case object TestMessage + + private val testSession = new LiftSession("Test Session", "", Empty) + + private class SpecCometActor extends CometActor { + var receivedMessages = List[Any]() + + def render = NodeSeq.Empty + override def theSession = testSession + + override def !(msg: Any) = { + receivedMessages ::= msg + + LAScheduler.onSameThread = true + + super.!(msg) + + LAScheduler.onSameThread = false + } + } + + "A CometActor" should { + class RedirectingComet extends SpecCometActor { + override def lowPriority = { + case TestMessage => + S.redirectTo("place") + } + } + + "redirect the user when a ResponseShortcutException with redirect occurs" in { + val comet = new RedirectingComet + + comet ! TestMessage + + comet.receivedMessages.exists { + case PartialUpdateMsg(update) if update() == RedirectTo("place") => + true + case _ => + false + } must beTrue + } + + class FunctionRedirectingComet extends SpecCometActor { + override def lowPriority = { + case TestMessage => + S.redirectTo("place", () => "do stuff") + } + } + + "redirect the user with a function when a ResponseShortcutException with redirect+function occurs" in { + val comet = new FunctionRedirectingComet + + comet ! TestMessage + + val matchingMessage = + comet.receivedMessages.collect { + case PartialUpdateMsg(update) => + update() + } + + matchingMessage must beLike { + case List(RedirectTo(redirectUri)) => + redirectUri must startWith("place") + redirectUri must beMatching("^[^?]+\\?F[^=]+=_$".r) + } + } + } +} diff --git a/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala index 7accc87437..32000fe55c 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/http/ReqSpec.scala @@ -17,15 +17,29 @@ package net.liftweb package http +import java.io.ByteArrayInputStream + +import scala.xml.XML + +import org.specs2.matcher.XmlMatchers + +import org.mockito.Mockito._ + import org.specs2.mutable.Specification +import org.specs2.mock.Mockito +import org.specs2.specification.Scope import common._ +import json.JsonDSL._ +import json.JsonParser +import util.Helpers.tryo +import provider._ /** * System under specification for Req. */ -object ReqSpec extends Specification { +object ReqSpec extends Specification with XmlMatchers with Mockito { "Req Specification".title private val iPhoneUserAgents = @@ -63,6 +77,8 @@ object ReqSpec extends Specification { uac.isIPad must_== false } } + + success } "Do the right thing with iPad" in { @@ -75,6 +91,8 @@ object ReqSpec extends Specification { uac.isIPad must_== true } } + + success } "Correctly recognize IE versions 6-11" in { @@ -88,6 +106,115 @@ object ReqSpec extends Specification { ieVersions must_== List(6, 7, 8, 9, 10, 11) } + + trait mockReq extends Scope { + val mockHttpRequest = mock[HTTPRequest] + def paramCalcInfo = ParamCalcInfo(Nil, Map.empty, Nil, Full(BodyOrInputStream(new ByteArrayInputStream(bodyBytes)))) + + def bodyBytes: Array[Byte] + + def req(contentType: String) = { + new Req( + Req.NilPath, "/", GetRequest, + Full(contentType), + mockHttpRequest, + 0l, 1l, true, + () => paramCalcInfo, + Map.empty + ) + } + } + + class mockJsonReq(jsonString: String = """{ "booyan": "shazam", "booyak": 5, "bazam": 2.5 }""") extends mockReq { + val testJson = jsonString + val parsedJson = tryo(JsonParser.parse(jsonString)) openOr json.JsonAST.JNothing + + def bodyBytes = { + testJson.getBytes("UTF-8") + } + } + + class mockXmlReq(xmlString: String = """Oh yeah""") extends mockReq { + val testXml = xmlString + val parsedXml = tryo(XML.loadString(xmlString)) openOr "totally failed" + + def bodyBytes = { + testXml.getBytes("UTF-8") + } + } + + "when trying to JSON parse the request body" in { + "with an invalid Content-Type should return a Failure" in new mockJsonReq { + req("text/plain").json should beAnInstanceOf[Failure] + } + + "with an application/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { + req("application/json").json should_== Full(parsedJson) + } + + "with a text/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { + req("text/json").json should_== Full(parsedJson) + } + + "with invalid JSON and a text/json Content-Type should return a Failure" in new mockJsonReq("epic fail") { + req("text/json").json should beAnInstanceOf[Failure] + } + } + + "when forcing a request body JSON parse with forcedBodyAsJson" in { + "with an invalid Content-Type should return the result of parsing the JSON" in new mockJsonReq { + req("text/plain").forcedBodyAsJson should_== Full(parsedJson) + } + + "with an application/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { + req("application/json").forcedBodyAsJson should_== Full(parsedJson) + } + + "with a text/json Content-Type should return the result of parsing the JSON" in new mockJsonReq { + req("text/json").forcedBodyAsJson should_== Full(parsedJson) + } + + "with invalid JSON should return a Failure" in new mockJsonReq("epic fail") { + req("text/json").json should beAnInstanceOf[Failure] + } + } + + "when trying to XML parse the request body" in { + "with an invalid Content-Type should return a Failure" in new mockXmlReq { + req("text/plain").xml should beAnInstanceOf[Failure] + } + + "with an application/xml Content-Type should return the result of parsing the JSON" in new mockXmlReq { + req("application/xml").xml should_== Full(parsedXml) + } + + "with a text/xml Content-Type should return the result of parsing the JSON" in new mockXmlReq { + req("text/xml").xml should_== Full(parsedXml) + } + + "with invalid XML and a text/xml Content-Type should return a Failure" in new mockXmlReq("epic fail") { + req("text/xml").forcedBodyAsXml should beAnInstanceOf[Failure] + } + } + + "when forcing a request body XML parse with forcedBodyAsXml" in { + "with an invalid Content-Type should return the result of parsing the JSON" in new mockXmlReq { + req("text/plain").forcedBodyAsXml should_== Full(parsedXml) + } + + "with an application/json Content-Type should return the result of parsing the JSON" in new mockXmlReq { + req("application/xml").forcedBodyAsXml should_== Full(parsedXml) + } + + "with a text/json Content-Type should return the result of parsing the JSON" in new mockXmlReq { + req("text/xml").forcedBodyAsXml should_== Full(parsedXml) + } + + "with invalid XML should return a Failure" in new mockXmlReq("epic fail") { + req("text/palin").forcedBodyAsXml should beAnInstanceOf[Failure] + req("text/xml").forcedBodyAsXml should beAnInstanceOf[Failure] + } + } } } diff --git a/web/webkit/src/test/scala/net/liftweb/http/SHtmlSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/SHtmlSpec.scala index c83f8090a1..d055d7d9e6 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/SHtmlSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/http/SHtmlSpec.scala @@ -17,12 +17,13 @@ package net.liftweb package http +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import util._ import Helpers._ import net.liftweb.mockweb.MockWeb._ -object SHtmlSpec extends Specification { +object SHtmlSpec extends Specification with XmlMatchers { "NamedCometPerTabSpec Specification".title val html1= diff --git a/web/webkit/src/test/scala/net/liftweb/http/SSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/SSpec.scala index 31bb8d382c..3f658df524 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/SSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/http/SSpec.scala @@ -17,12 +17,13 @@ package net.liftweb package http +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import util.Helpers import util.Props.RunModes import LiftRules.defaultFuncNameGenerator -object SSpec extends Specification { +object SSpec extends Specification with XmlMatchers { "S Specification".title "formFuncName" should { @@ -33,6 +34,8 @@ object SSpec extends Specification { a.length must_== Helpers.nextFuncName.length a must_!= b } + + success } "generate predictable names in Test mode" in { diff --git a/web/webkit/src/test/scala/net/liftweb/http/SnippetSpec.scala b/web/webkit/src/test/scala/net/liftweb/http/SnippetSpec.scala index d3949c9873..a1595412ed 100644 --- a/web/webkit/src/test/scala/net/liftweb/http/SnippetSpec.scala +++ b/web/webkit/src/test/scala/net/liftweb/http/SnippetSpec.scala @@ -18,6 +18,7 @@ package net.liftweb package http import xml._ +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import common._ @@ -27,7 +28,7 @@ import util.Helpers._ /** * System under specification for SnippetSpec. */ -object SnippetSpec extends Specification { +object SnippetSpec extends Specification with XmlMatchers { "SnippetSpec Specification".title def makeReq = new Req(Req.NilPath, "", GetRequest, Empty, null, diff --git a/web/webkit/src/test/scala/net/liftweb/webapptest/OneShot.scala b/web/webkit/src/test/scala/net/liftweb/webapptest/OneShot.scala index c96692a52a..bdbba8c8b6 100644 --- a/web/webkit/src/test/scala/net/liftweb/webapptest/OneShot.scala +++ b/web/webkit/src/test/scala/net/liftweb/webapptest/OneShot.scala @@ -17,6 +17,7 @@ package net.liftweb package webapptest +import org.specs2.matcher.XmlMatchers import org.specs2.mutable.Specification import util._ @@ -30,7 +31,7 @@ import snippet.Counter import net.liftweb.common.Full -object OneShot extends Specification with RequestKit { +object OneShot extends Specification with RequestKit with XmlMatchers { sequential private def reachableLocalAddress = {