--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/boot/ProtoBoot.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.boot
+
+import net.liftweb.common._
+import net.liftweb.http._
+import net.liftweb.mapper._
+import net.liftweb.sitemap._
+import net.liftweb.sitemap.Loc._
+import net.liftweb.util._
+import net.liftweb.widgets.menu.MenuWidget
+import net.tz.lift.snippet.ActionLinks
+import net.tz.lift.util.YmdDateTimeConverter
+
+/**
+ * Prototype boot class to be either extended or copied.
+ */
+class ProtoBoot extends Logger {
+
+ def boot = {
+ /* DB stuff */
+ /*
+ S.addAround(DB.buildLoanWrapper())
+ */
+
+ if (Props.mode == Props.RunModes.Development)
+ DB.addLogFunc { (dbLog, l) => dbLog.statementEntries.foreach { e =>
+ debug("Query: " + e.statement)
+ }}
+
+ /* Date format */
+ LiftRules.dateTimeConverter.default.set { () => YmdDateTimeConverter }
+
+ /* Handle end slash and drop it (except for home page) */
+ LiftRules.statelessRewrite.append {
+ case RewriteRequest(ParsePath(xs,_,_,true),_,_) if (xs.size > 1) &&
+ (xs.lastOption == Some("index")) =>
+ RewriteResponse(xs dropRight 1)
+ }
+
+ /* Snippet dispatch */
+ LiftRules.snippetDispatch.append {
+ case "Menubar" => new AnyRef with DispatchSnippet {
+ def dispatch: DispatchIt = {
+ case _ => { xhtml => MenuWidget() }
+ }
+ }
+ case "action-links" => ActionLinks
+ }
+
+ /* Sitemap */
+ SiteMap.enforceUniqueLinks = false
+
+ /*
+ LiftRules.setSiteMap(SiteMap(
+ Menu.i("Home") / "index" >> Hidden
+ ))
+ */
+
+ /* Menu widget */
+ MenuWidget.init()
+
+ /* Http conf */
+ LiftRules.logServiceRequestTiming = false
+ LiftRules.early.append(_.setCharacterEncoding("UTF-8"))
+ }
+
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/snippet/Panel.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.snippet
+
+import net.liftweb.mapper.MappedField
+import scala.xml.{NodeSeq, Text}
+
+object AttrRow {
+ def apply(name: => NodeSeq, value: => NodeSeq) = new AttrRow(name, value,
+ "attr-name", "attr-value")
+ def apply(f: MappedField[_, _]) = new AttrRow(Text(f.displayName), f.asHtml,
+ "attr-name", "attr-value")
+ def formRow(name: => NodeSeq, input: => NodeSeq) = new AttrRow(name, input,
+ "form-name", "form-value")
+ def submitRow(button: => NodeSeq) = new SpanRow(button)
+}
+
+class AttrRow(name: => NodeSeq, value: => NodeSeq, nameCss: String,
+ valueCss: String) extends Function0[NodeSeq] {
+ def tdN = <td class={nameCss}>{name}</td>
+ def tdV = <td class={valueCss}>{value}</td>
+ def apply(): NodeSeq = <tr>{tdN :: tdV :: Nil}</tr>
+}
+
+class SpanRow(value: => NodeSeq) extends AttrRow(NodeSeq.Empty, value, "", "")
+{
+ override def apply(): NodeSeq = <tr><td colspan="2">{value}</td></tr>
+}
+
+object Panel {
+ def apply(attrs: Iterable[AttrRow]) = new Panel(attrs)
+ def fromFields(fields: Iterable[MappedField[_,_]]) =
+ new Panel(fields.map(AttrRow(_)))
+}
+
+class Panel(attrs: => Iterable[AttrRow]) extends Function1[NodeSeq, NodeSeq]
+{
+ def apply(in: NodeSeq): NodeSeq = <table>{attrs.map(_())}</table>
+ def &(other: Function1[NodeSeq, NodeSeq]) = new Function1[NodeSeq, NodeSeq] {
+ def apply(in: NodeSeq): NodeSeq = List(Panel.this, other) flatMap (_(in))
+ }
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/snippet/SnippetHelpers.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.snippet
+
+import net.liftweb.http._
+import scala.xml.{NodeSeq, Text}
+
+trait SnippetHelpers {
+ def mkPath(prefix: String, l: String*) =
+ (prefix :: l.toList) mkString ("/", "/", "")
+}
+
+class A(href: => String) extends Function1[NodeSeq, NodeSeq] {
+ def apply(in: NodeSeq): NodeSeq = <a href={href}>{in}</a>
+}
+
+object A {
+ def apply(href: String, in: NodeSeq): NodeSeq = (new A(href))(in)
+ def apply(href: String): A = new A(href)
+ def apply(href: String, cnt: String): NodeSeq = (new A(href))(Text(cnt))
+}
+
+object ActionLinks extends DispatchSnippet {
+ def dispatch: DispatchIt = {
+ case "are" => { xhtml => if (links.is.isEmpty) NodeSeq.Empty else xhtml }
+ case _ => render _
+ }
+
+ import scala.collection.mutable.{BufferLike, ListBuffer}
+
+ private object links extends RequestVar[ListBuffer[Either[Function0[NodeSeq], Function1[NodeSeq, NodeSeq]]]](new ListBuffer())
+
+ def render(in: NodeSeq): NodeSeq = links.is.flatMap { _.fold( _(), _(in) ) ++
+ Text(" ") }
+
+ def append(l: NodeSeq) = links.is.append(Left( { () => l } ))
+ def append(l: () => NodeSeq) = links.is.append(Left(l))
+ def append(l: NodeSeq => NodeSeq) = links.is.append(Right(l))
+}
+
+object SimpleForm {
+ def apply(l: Iterable[AttrRow], submitVal: String, onSubmit: () => Any):
+ (NodeSeq => NodeSeq) = {
+ val curS = S.currentSnippet
+ def doit() = {
+ onSubmit()
+ curS foreach { S.mapSnippet(_, loop) }
+ }
+
+ def loop: (NodeSeq => NodeSeq) = {
+ Panel(l ++ List(AttrRow.submitRow(SHtml.submit(submitVal, doit))))
+ }
+
+ loop
+ }
+
+ def apply(l: Iterable[AttrRow], submitVal: String): (NodeSeq => NodeSeq) =
+ apply(l, submitVal, () => ())
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/snippet/Table.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.snippet
+
+import net.liftweb.common._
+import net.liftweb.util.Helpers._
+import scala.xml.{Elem, NodeSeq, Text}
+
+object Column {
+ def apply[T](name: String, f: T => NodeSeq): Column[T] =
+ new Column[T](Text(name), f, { _ => Empty})
+ def apply[T](name: String, f: T => NodeSeq, tdCss: String): Column[T] =
+ new Column[T](Text(name), f, { _ => Full(tdCss)})
+}
+
+class Column[T](name: => NodeSeq, f: T => NodeSeq, tdCss: T => Box[String])
+ extends Function1[T, NodeSeq] {
+
+ def th: Elem = <th>{name}</th>
+ def td(in: T): Elem = {
+ val r = <td>{apply(in)}</td>
+ tdCss(in) map { css => r % ("class" -> css) } openOr r
+ }
+ def apply(in: T): NodeSeq = f(in)
+}
+
+object Table {
+ def apply[T](cols: Iterable[Column[T]], rows: Iterable[T]) = new Table(Empty,
+ cols, rows, Empty)
+ def apply[T](css: String, cols: Iterable[Column[T]], rows: Iterable[T],
+ foot: NodeSeq) = new Table(Full(css), cols, rows, Full(foot))
+}
+
+class Table[T](css: Box[String], cols: Iterable[Column[T]],
+ rows: => Iterable[T], foot: Box[NodeSeq]) extends
+ Function1[NodeSeq, NodeSeq] {
+
+ def oddEven(i: Int) = (i % 2) match {
+ case 1 => "even"
+ case 0 => "odd"
+ }
+
+ def thead: Elem = <thead><tr>{ cols.map(c => c.th) }</tr></thead>
+ def tbody: Elem = <tbody>{
+ rows.zipWithIndex.map( r => tr(r._1, r._2))
+ }</tbody>
+ def tr(in: T, idx: Int): Elem = <tr class={oddEven(idx)}>{
+ cols.map(_.td(in))
+ }</tr>
+
+ def tfoot: Elem = <tfoot>{
+ foot map { f => <tr><td colspan={cols.size.toString}>{f}</td></tr>
+ } openOr NodeSeq.Empty
+ }</tfoot>
+
+ def apply(ns: NodeSeq): NodeSeq = {
+ val t = <table>{
+ List(thead, tfoot, tbody)
+ }</table>
+ css map { cl => t % ("class" -> cl) } openOr t
+ }
+
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/util/DB.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,166 @@
+/*
+ * Fix for ProtoDBVendor/StandardDBVendor connection leak.
+ *
+ * Original code is:
+ *
+ * Copyright 2006-2011 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * All fixes are under the same license.
+ */
+package net.tz.lift.util
+
+import java.sql.{Connection, DriverManager}
+import net.liftweb.common._
+import net.liftweb.db.{ConnectionIdentifier, ConnectionManager}
+import net.liftweb.util._
+import net.liftweb.util.Helpers._
+
+/**
+ * The standard DB vendor.
+ * @param driverName the name of the database driver
+ * @param dbUrl the URL for the JDBC data connection
+ * @param dbUser the optional username
+ * @param dbPassword the optional db password
+ */
+class StandardDBVendor(driverName: String,
+ dbUrl: String,
+ dbUser: Box[String],
+ dbPassword: Box[String]) extends ProtoDBVendor {
+ protected def createOne: Box[Connection] = try {
+ Class.forName(driverName)
+
+ val dm = (dbUser, dbPassword) match {
+ case (Full(user), Full(pwd)) =>
+ DriverManager.getConnection(dbUrl, user, pwd)
+
+ case _ => DriverManager.getConnection(dbUrl)
+ }
+
+ Full(dm)
+ } catch {
+ case e: Exception => e.printStackTrace; Empty
+ }
+}
+
+trait ProtoDBVendor extends ConnectionManager {
+ private val logger = Logger(classOf[ProtoDBVendor])
+ private var pool: List[Connection] = Nil
+ private var poolSize = 0
+ private var tempMaxSize = maxPoolSize
+
+ /**
+ * Override and set to false if the maximum pool size can temporarilly be expanded to avoid pool starvation
+ */
+ protected def allowTemporaryPoolExpansion = true
+
+ /**
+ * Override this method if you want something other than
+ * 4 connections in the pool
+ */
+ protected def maxPoolSize = 4
+
+ /**
+ * The absolute maximum that this pool can extend to
+ * The default is 20. Override this method to change.
+ */
+ protected def doNotExpandBeyond = 20
+
+ /**
+ * The logic for whether we can expand the pool beyond the current size. By
+ * default, the logic tests allowTemporaryPoolExpansion && poolSize <= doNotExpandBeyond
+ */
+ protected def canExpand_? : Boolean = allowTemporaryPoolExpansion && poolSize <= doNotExpandBeyond
+
+ /**
+ * How is a connection created?
+ */
+ protected def createOne: Box[Connection]
+
+ /**
+ * Test the connection. By default, setAutoCommit(false),
+ * but you can do a real query on your RDBMS to see if the connection is alive
+ */
+ protected def testConnection(conn: Connection) {
+ conn.setAutoCommit(false)
+ }
+
+ def newConnection(name: ConnectionIdentifier): Box[Connection] =
+ synchronized {
+ pool match {
+ case Nil if poolSize < tempMaxSize =>
+ val ret = createOne
+ ret foreach { c =>
+ c.setAutoCommit(false)
+ poolSize = poolSize + 1
+ logger.debug("Created new pool entry. name=%s, poolSize=%d".format(name, poolSize))
+ }
+ ret
+
+ case Nil =>
+ val curSize = poolSize
+ logger.trace("No connection left in pool, waiting...")
+ wait(50L)
+ // if we've waited 50 ms and the pool is still empty, temporarily expand it
+ if (pool.isEmpty && poolSize == curSize && canExpand_?) {
+ tempMaxSize += 1
+ logger.debug("Temporarily expanding pool. name=%s, tempMaxSize=%d".format(name, tempMaxSize))
+ }
+ newConnection(name)
+
+ case x :: xs =>
+ logger.trace("Found connection in pool, name=%s".format(name))
+ pool = xs
+ try {
+ this.testConnection(x)
+ Full(x)
+ } catch {
+ case e => try {
+ logger.debug("Test connection failed, removing connection from pool, name=%s".format(name))
+ poolSize = poolSize - 1
+ tryo(x.close)
+ newConnection(name)
+ } catch {
+ case e => newConnection(name)
+ }
+ }
+ }
+ }
+
+ def releaseConnection(conn: Connection): Unit = synchronized {
+ if (tempMaxSize > maxPoolSize) {
+ tryo {conn.close()}
+ tempMaxSize -= 1
+ poolSize -= 1
+ } else {
+ pool = conn :: pool
+ }
+ logger.debug("Released connection. poolSize=%d".format(poolSize))
+ notifyAll
+ }
+
+ def closeAllConnections_!(): Unit = synchronized {
+ logger.info("Closing all connections, poolSize=%d".format(poolSize))
+ if (poolSize == 0) ()
+ else {
+ pool.foreach {c => tryo(c.close); poolSize -= 1}
+ pool = Nil
+
+ if (poolSize > 0) wait(250)
+
+ closeAllConnections_!()
+ }
+ }
+}
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/util/DateExtension.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,44 @@
+/*
+ * Refactored from net.liftweb.util.TimeHelpers.scala, which is
+ *
+ * Copyright 2006-2011 WorldWide Conferencing, LLC and
+ *
+ * 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.tz.lift.util
+
+import java.util.{Calendar, Date}
+
+/**
+ * Fixed version of DateExtension (from
+ * net.liftweb.util.TimeSpan.DateExtension).
+ */
+class DateExtension(d: Date) {
+ /** @returns a Date object starting at 00:00 from date */
+ def noTime = {
+ val calendar = Calendar.getInstance
+ calendar.setTime(d)
+ calendar.set(Calendar.HOUR_OF_DAY, 0)
+ calendar.set(Calendar.MINUTE, 0)
+ calendar.set(Calendar.SECOND, 0)
+ calendar.set(Calendar.MILLISECOND, 0)
+ calendar.getTime
+ }
+}
+
+object DateExtension {
+ implicit def date2dateExtension(d: Date): DateExtension =
+ new DateExtension(d)
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/util/Helpers.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.util
+
+import java.text.SimpleDateFormat
+import java.util.Date
+import net.liftweb.http.LiftRules
+import net.liftweb.util.Helpers.tryo
+import org.joda.time.{DateMidnight, DateTime, Period, ReadableInstant}
+import org.joda.time.format.{DateTimeFormat, PeriodFormatterBuilder}
+import scala.xml.{NodeSeq, Text}
+
+object AsIsoDateTime {
+ val df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
+ def unapply(in: String): Option[Date] = tryo { df.parse(in) }
+ def apply(d: Date): String = df.format(d)
+}
+
+object AsDate {
+ import DateExtension._
+ def unapply(in: String): Option[Date] = LiftRules.dateTimeConverter().
+ parseDate(in).map(_.noTime)
+ def apply(d: Date) = LiftRules.dateTimeConverter().formatDate(d)
+}
+
+object AsDateTime {
+ def unapply(in: String): Option[Date] = LiftRules.dateTimeConverter().
+ parseDateTime(in)
+ def apply(d: Date) = LiftRules.dateTimeConverter().formatDateTime(d)
+}
+
+object AsDateMidnight {
+ lazy val fmt = DateTimeFormat.forPattern("yyyy-MM-dd")
+ def unapply(in: String): Option[DateMidnight] = apply(in)
+ def apply(in: String): Option[DateMidnight] = tryo {
+ fmt.parseDateTime(in).toDateMidnight
+ }
+ def apply(d: ReadableInstant) = fmt.print(d)
+
+ implicit def dt2d(d: DateTime): Date = d.toDate
+ implicit def dm2d(d: DateMidnight): Date = d.toDate
+ implicit def d2dm(d: Date): DateMidnight = new DateMidnight(d)
+ implicit def dm2dt(d: DateMidnight): DateTime = d.toDateTime
+}
+
+object AsTimePeriod {
+ lazy val fmt = (new PeriodFormatterBuilder).printZeroAlways.
+ minimumPrintedDigits(2).appendHours.
+ appendSeparator(":").appendMinutes.toFormatter
+ lazy val fmtDt = DateTimeFormat.forPattern("HH:mm")
+
+ /** Parses HH:mm time into period. */
+ def apply(in: String): Option[Period] = tryo { fmt.parsePeriod(in) }
+ def apply(p: Period) = fmt.print(p)
+ def apply(dt: DateTime) = fmtDt.print(dt)
+}
+
+object Bytes {
+ implicit def dbl2ns(d: Double): NodeSeq =
+ Text(String.format(fmt, d.asInstanceOf[AnyRef]))
+ def fmt = "%.2f"
+ def kb(v: Long): NodeSeq = v.toDouble / 1024
+ def mb(v: Long): NodeSeq = v.toDouble / (1024*1024)
+}
+
+// vim: set ts=2 sw=2 et:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/net/tz/lift/util/YmdDateTimeConverter.scala Fri Feb 10 09:53:04 2012 +0100
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.tz.lift.util
+
+import java.text.SimpleDateFormat
+import java.util.Date
+import net.liftweb.common._
+import net.liftweb.util.DateTimeConverter
+import net.liftweb.util.TimeHelpers._
+
+object YmdDateTimeConverter extends DateTimeConverter {
+ val df = new SimpleDateFormat("yyyy-MM-dd")
+ val dtf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ def formatDateTime(d: Date) = dtf.format(d)
+ def formatDate(d: Date) = df.format(d)
+ /** Uses Helpers.hourFormat which includes seconds but not time zone */
+ def formatTime(d: Date) = hourFormat.format(d)
+
+ def parseDateTime(s: String) = tryo { dtf.parse(s) }
+ def parseDate(s: String) = tryo { df.parse(s) }
+ /** Tries Helpers.hourFormat and Helpers.timeFormat */
+ def parseTime(s: String) =
+ tryo{hourFormat.parse(s)} or tryo{timeFormatter.parse(s)}
+}
+
+// vim: set ts=2 sw=2 et: