diff --git a/documentation/manual/releases/release26/migration26/JodaMigration26.md b/documentation/manual/releases/release26/migration26/JodaMigration26.md new file mode 100644 index 00000000000..2903575a017 --- /dev/null +++ b/documentation/manual/releases/release26/migration26/JodaMigration26.md @@ -0,0 +1,26 @@ + +# Joda Migration Guide + +In order to make the default play distribution a bit smaller we removed all occurences of Joda Time from play. +This means that you should migrate to Java8 `java.time`, if you can. + +## Use Joda-Time in Forms and Play-Json + +If you still need to use Joda-Time in Play Forms and Play-Json you can just add the `play-joda` project: + +``` +libraryDependencies += "com.typesafe.play" % "play-joda" % "1.0.0" +``` + +And then import the corresponding Object for Forms: + +``` +import play.api.data.JodaForms._ +``` + +or for Play-Json + +``` +import play.api.data.JodaWrites._ +import play.api.data.JodaReads._ +``` \ No newline at end of file diff --git a/documentation/manual/working/javaGuide/main/forms/JavaForms.md b/documentation/manual/working/javaGuide/main/forms/JavaForms.md index 821b42c941b..df7aaedaae2 100644 --- a/documentation/manual/working/javaGuide/main/forms/JavaForms.md +++ b/documentation/manual/working/javaGuide/main/forms/JavaForms.md @@ -80,7 +80,7 @@ You can use a `DynamicForm` if you need to retrieve data from an html form that In case you want to define a mapping from a custom object to a form field string and vice versa you need to register a new Formatter for this object. You can achieve this by registering a provider for `Formatters` which will do the proper initialization. -For an object like JodaTime's `LocalTime` it could look like this: +For an object like JavaTime's `LocalTime` it could look like this: @[register-formatter](code/javaguide/forms/FormattersProvider.java) diff --git a/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/FormattersProvider.java b/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/FormattersProvider.java index dfe95e3dcaa..a36b80b3e23 100644 --- a/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/FormattersProvider.java +++ b/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/FormattersProvider.java @@ -5,6 +5,7 @@ //#register-formatter import java.text.ParseException; +import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -13,7 +14,7 @@ import javax.inject.Provider; import javax.inject.Singleton; -import org.joda.time.LocalTime; +import java.time.LocalTime; import play.data.format.Formatters; import play.data.format.Formatters.SimpleFormatter; @@ -46,12 +47,12 @@ public LocalTime parse(String input, Locale l) throws ParseException { if (!m.find()) throw new ParseException("No valid Input", 0); int hour = Integer.valueOf(m.group(1)); int min = m.group(2) == null ? 0 : Integer.valueOf(m.group(2)); - return new LocalTime(hour, min); + return LocalTime.of(hour, min); } @Override public String print(LocalTime localTime, Locale l) { - return localTime.toString("HH:mm"); + return localTime.format(DateTimeFormatter.ofPattern("HH:mm")); } }); diff --git a/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/JavaForms.java b/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/JavaForms.java index bcd795f3281..90650ba66cd 100644 --- a/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/JavaForms.java +++ b/documentation/manual/working/javaGuide/main/forms/code/javaguide/forms/JavaForms.java @@ -4,7 +4,6 @@ package javaguide.forms; import com.google.common.collect.ImmutableMap; -import org.joda.time.LocalTime; import org.junit.Test; import play.Application; import play.data.DynamicForm; @@ -21,6 +20,7 @@ import javaguide.forms.u1.User; import java.text.ParseException; +import java.time.LocalTime; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -212,7 +212,7 @@ public void registerFormatter() { Form form = application.injector().instanceOf(FormFactory.class).form(WithLocalTime.class); WithLocalTime obj = form.bind(ImmutableMap.of("time", "23:45")).get(); - assertThat(obj.getTime(), equalTo(new LocalTime(23, 45))); + assertThat(obj.getTime(), equalTo(LocalTime.of(23, 45))); assertThat(form.fill(obj).field("time").value(), equalTo("23:45")); } diff --git a/documentation/manual/working/scalaGuide/main/forms/ScalaForms.md b/documentation/manual/working/scalaGuide/main/forms/ScalaForms.md index 4fe32d68018..60244f8f896 100644 --- a/documentation/manual/working/scalaGuide/main/forms/ScalaForms.md +++ b/documentation/manual/working/scalaGuide/main/forms/ScalaForms.md @@ -76,8 +76,7 @@ The out of the box constraints are defined on the [Forms object](api/scala/play/ * [`number`](api/scala/play/api/data/Forms$.html#number%3AMapping%5BInt%5D): maps to `scala.Int`, optionally takes `min`, `max`, and `strict`. * [`longNumber`](api/scala/play/api/data/Forms$.html#longNumber%3AMapping%5BLong%5D): maps to `scala.Long`, optionally takes `min`, `max`, and `strict`. * [`bigDecimal`](api/scala/play/api/data/Forms$.html#bigDecimal%3AMapping%5BBigDecimal%5D): takes `precision` and `scale`. -* [`date`](api/scala/play/api/data/Forms$.html#date%3AMapping%5BDate%5D), [`sqlDate`](api/scala/play/api/data/Forms$.html#sqlDate%3AMapping%5BDate%5D), [`jodaDate`](api/scala/play/api/data/Forms$.html#jodaDate%3AMapping%5BDateTime%5D): maps to `java.util.Date`, `java.sql.Date` and `org.joda.time.DateTime`, optionally takes `pattern` and `timeZone`. -* [`jodaLocalDate`](api/scala/play/api/data/Forms$.html#jodaLocalDate%3AMapping%5BLocalDate%5D): maps to `org.joda.time.LocalDate`, optionally takes `pattern`. +* [`date`](api/scala/play/api/data/Forms$.html#date%3AMapping%5BDate%5D), [`sqlDate`](api/scala/play/api/data/Forms$.html#sqlDate%3AMapping%5BDate%5D): maps to `java.util.Date`, `java.sql.Date`, optionally takes `pattern` and `timeZone`. * [`email`](api/scala/play/api/data/Forms$.html#email%3AMapping%5BString%5D): maps to `scala.String`, using an email regular expression. * [`boolean`](api/scala/play/api/data/Forms$.html#boolean%3AMapping%5BBoolean%5D): maps to `scala.Boolean`. * [`checked`](api/scala/play/api/data/Forms$.html#checked%3AMapping%5BBoolean%5D): maps to `scala.Boolean`. diff --git a/framework/project/Dependencies.scala b/framework/project/Dependencies.scala index 1f02c75524a..4cb9c17f6ce 100644 --- a/framework/project/Dependencies.scala +++ b/framework/project/Dependencies.scala @@ -106,9 +106,6 @@ object Dependencies { mockitoAll ).map(_ % Test) - val jodatime = "joda-time" % "joda-time" % "2.9.3" - val jodaConvert = "org.joda" % "joda-convert" % "1.8.1" - val guiceDeps = Seq( "com.google.inject" % "guice" % "4.0", "com.google.inject.extensions" % "guice-assistedinject" % "4.0" @@ -123,8 +120,6 @@ object Dependencies { "commons-codec" % "commons-codec" % "1.10", guava, - jodatime, - jodaConvert, "org.apache.commons" % "commons-lang3" % "3.4", @@ -250,8 +245,6 @@ object Dependencies { ) ++ specsBuild.map(_ % "test") ++ javaTestDeps def jsonDependencies(scalaVersion: String) = Seq( - jodatime, - jodaConvert, "org.scala-lang" % "scala-reflect" % scalaVersion, logback % Test) ++ jacksons ++ diff --git a/framework/src/play-cache/src/main/scala/play/api/cache/Cached.scala b/framework/src/play-cache/src/main/scala/play/api/cache/Cached.scala index 74c779ef65b..f175c497f36 100644 --- a/framework/src/play-cache/src/main/scala/play/api/cache/Cached.scala +++ b/framework/src/play-cache/src/main/scala/play/api/cache/Cached.scala @@ -3,6 +3,7 @@ */ package play.api.cache +import java.time.Instant import javax.inject.Inject import akka.stream.Materializer @@ -169,7 +170,7 @@ final class CachedBuilder( private def handleResult(result: Result, etagKey: String, resultKey: String): Result = { cachingWithEternity.andThen { duration => // Format expiration date according to http standard - val expirationDate = http.dateFormat.print(System.currentTimeMillis() + duration.toMillis) + val expirationDate = http.dateFormat.format(Instant.ofEpochMilli(System.currentTimeMillis() + duration.toMillis)) // Generate a fresh ETAG for it // Use quoted sha1 hash of expiration date as ETAG val etag = s""""${Codecs.sha1(expirationDate)}"""" diff --git a/framework/src/play-cache/src/test/scala/play/api/cache/CachedSpec.scala b/framework/src/play-cache/src/test/scala/play/api/cache/CachedSpec.scala index b18fa597a38..a8ef4f98490 100644 --- a/framework/src/play-cache/src/test/scala/play/api/cache/CachedSpec.scala +++ b/framework/src/play-cache/src/test/scala/play/api/cache/CachedSpec.scala @@ -3,10 +3,10 @@ */ package play.api.cache +import java.time.Instant import java.util.concurrent.atomic.AtomicInteger import javax.inject._ -import org.joda.time.DateTime import play.api.cache.ehcache.EhCacheApi import play.api.{ Application, http } import play.api.mvc.{ Action, Results } @@ -207,8 +207,8 @@ class CachedSpec extends PlaySpecification { val res1 = header(EXPIRES, actionNotFound(FakeRequest("GET", "/b")).run()) def toDuration(header: String) = { - val now = DateTime.now().getMillis - val target = http.dateFormat.parseDateTime(header).getMillis + val now = Instant.now().toEpochMilli + val target = Instant.from(http.dateFormat.parse(header)).toEpochMilli Duration(target - now, MILLISECONDS) } diff --git a/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala b/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala index 93d6eab8071..3299a7b19d4 100644 --- a/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala +++ b/framework/src/play-json/src/main/scala/play/api/libs/json/Reads.scala @@ -639,97 +639,6 @@ trait DefaultReads extends LowPriorityDefaultReads { } } - /** - * Reads for the `org.joda.time.DateTime` type. - * - * @param pattern a date pattern, as specified in `java.text.SimpleDateFormat`. - * @param corrector a simple string transformation function that can be used to transform input String before parsing. Useful when standards are not exactly respected and require a few tweaks - */ - def jodaDateReads(pattern: String, corrector: String => String = identity): Reads[org.joda.time.DateTime] = new Reads[org.joda.time.DateTime] { - import org.joda.time.DateTime - - val df = org.joda.time.format.DateTimeFormat.forPattern(pattern) - - def reads(json: JsValue): JsResult[DateTime] = json match { - case JsNumber(d) => JsSuccess(new DateTime(d.toLong)) - case JsString(s) => parseDate(corrector(s)) match { - case Some(d) => JsSuccess(d) - case None => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jodadate.format", pattern)))) - } - case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.date")))) - } - - private def parseDate(input: String): Option[DateTime] = - scala.util.control.Exception.allCatch[DateTime] opt (DateTime.parse(input, df)) - - } - - /** - * the default implicit JodaDate reads - */ - implicit val DefaultJodaDateReads = jodaDateReads("yyyy-MM-dd") - - /** - * Reads for the `org.joda.time.LocalDate` type. - * - * @param pattern a date pattern, as specified in `org.joda.time.format.DateTimeFormat`. - * @param corrector string transformation function (See jodaDateReads) - */ - def jodaLocalDateReads(pattern: String, corrector: String => String = identity): Reads[org.joda.time.LocalDate] = new Reads[org.joda.time.LocalDate] { - - import org.joda.time.LocalDate - import org.joda.time.format.{ DateTimeFormat, ISODateTimeFormat } - - val df = if (pattern == "") ISODateTimeFormat.localDateParser else DateTimeFormat.forPattern(pattern) - - def reads(json: JsValue): JsResult[LocalDate] = json match { - case JsString(s) => parseDate(corrector(s)) match { - case Some(d) => JsSuccess(d) - case None => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jodadate.format", pattern)))) - } - case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.date")))) - } - - private def parseDate(input: String): Option[LocalDate] = - scala.util.control.Exception.allCatch[LocalDate] opt (LocalDate.parse(input, df)) - } - - /** - * the default implicit joda.time.LocalDate reads - */ - implicit val DefaultJodaLocalDateReads = jodaLocalDateReads("") - - /** - * Reads for the `org.joda.time.LocalTime` type. - * - * @param pattern a date pattern, as specified in `org.joda.time.format.DateTimeFormat`. - * @param corrector string transformation function (See jodaTimeReads) - */ - def jodaLocalTimeReads(pattern: String, corrector: String => String = identity): Reads[org.joda.time.LocalTime] = new Reads[org.joda.time.LocalTime] { - - import org.joda.time.LocalTime - import org.joda.time.format.{ DateTimeFormat, ISODateTimeFormat } - - val df = if (pattern == "") ISODateTimeFormat.localTimeParser else DateTimeFormat.forPattern(pattern) - - def reads(json: JsValue): JsResult[LocalTime] = json match { - case JsNumber(n) => JsSuccess(new LocalTime(n.toLong)) - case JsString(s) => parseTime(corrector(s)) match { - case Some(d) => JsSuccess(d) - case None => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jodatime.format", pattern)))) - } - case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.time")))) - } - - private def parseTime(input: String): Option[LocalTime] = - scala.util.control.Exception.allCatch[LocalTime] opt (LocalTime.parse(input, df)) - } - - /** - * the default implicit joda.time.LocalTime reads - */ - implicit val DefaultJodaLocalTimeReads = jodaLocalTimeReads("") - /** * Reads for the `java.sql.Date` type. * @@ -740,7 +649,7 @@ trait DefaultReads extends LowPriorityDefaultReads { dateReads(pattern, corrector).map(d => new java.sql.Date(d.getTime)) /** - * the default implicit JodaDate reads + * the default implicit SqlDate reads */ implicit val DefaultSqlDateReads = sqlDateReads("yyyy-MM-dd") diff --git a/framework/src/play-json/src/main/scala/play/api/libs/json/Writes.scala b/framework/src/play-json/src/main/scala/play/api/libs/json/Writes.scala index 76fe676cdad..eb0ff9d7147 100644 --- a/framework/src/play-json/src/main/scala/play/api/libs/json/Writes.scala +++ b/framework/src/play-json/src/main/scala/play/api/libs/json/Writes.scala @@ -395,53 +395,6 @@ trait DefaultWrites { JsNumber(BigDecimal valueOf t.toEpochMilli) } - /** - * Serializer for org.joda.time.DateTime - * @param pattern the pattern used by SimpleDateFormat - */ - def jodaDateWrites(pattern: String): Writes[org.joda.time.DateTime] = new Writes[org.joda.time.DateTime] { - val df = org.joda.time.format.DateTimeFormat.forPattern(pattern) - def writes(d: org.joda.time.DateTime): JsValue = JsString(d.toString(df)) - } - - /** - * Default Serializer org.joda.time.DateTime -> JsNumber(d.getMillis (nb of ms)) - */ - implicit object DefaultJodaDateWrites extends Writes[org.joda.time.DateTime] { - def writes(d: org.joda.time.DateTime): JsValue = JsNumber(d.getMillis) - } - - /** - * Serializer for org.joda.time.LocalDate - * @param pattern the pattern used by org.joda.time.format.DateTimeFormat - */ - def jodaLocalDateWrites(pattern: String): Writes[org.joda.time.LocalDate] = new Writes[org.joda.time.LocalDate] { - val df = org.joda.time.format.DateTimeFormat.forPattern(pattern) - def writes(d: org.joda.time.LocalDate): JsValue = JsString(d.toString(df)) - } - - /** - * Default Serializer org.joda.time.LocalDate -> JsString(ISO8601 format (yyyy-MM-dd)) - */ - implicit object DefaultJodaLocalDateWrites extends Writes[org.joda.time.LocalDate] { - def writes(d: org.joda.time.LocalDate): JsValue = JsString(d.toString) - } - - /** - * Serializer for org.joda.time.LocalTime - * @param pattern the pattern used by org.joda.time.format.DateTimeFormat - */ - def jodaLocalTimeWrites(pattern: String): Writes[org.joda.time.LocalTime] = new Writes[org.joda.time.LocalTime] { - def writes(d: org.joda.time.LocalTime): JsValue = JsString(d.toString(pattern)) - } - - /** - * Default Serializer org.joda.time.LocalDate -> JsString(ISO8601 format (HH:mm:ss.SSS)) - */ - implicit object DefaultJodaLocalTimeWrites extends Writes[org.joda.time.LocalTime] { - def writes(d: org.joda.time.LocalTime): JsValue = JsString(d.toString) - } - /** * Serializer for java.sql.Date * @param pattern the pattern used by SimpleDateFormat diff --git a/framework/src/play-json/src/test/scala/play/api/libs/json/JsonValidSpec.scala b/framework/src/play-json/src/test/scala/play/api/libs/json/JsonValidSpec.scala index 8a9a4844dbb..283c5c67478 100644 --- a/framework/src/play-json/src/test/scala/play/api/libs/json/JsonValidSpec.scala +++ b/framework/src/play-json/src/test/scala/play/api/libs/json/JsonValidSpec.scala @@ -132,25 +132,10 @@ object JsonValidSpec extends Specification { Json.toJson[java.util.Date](dd).validate[java.util.Date] must beEqualTo(JsSuccess(dd)) JsNumber(dd.getTime).validate[java.util.Date] must beEqualTo(JsSuccess(dd)) - val dj = new org.joda.time.DateTime() - val dfj = org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd") - val ddj = org.joda.time.DateTime.parse(dfj.print(dj), dfj) - - Json.toJson[org.joda.time.DateTime](ddj).validate[org.joda.time.DateTime] must beEqualTo(JsSuccess(ddj)) - JsNumber(ddj.getMillis).validate[org.joda.time.DateTime] must beEqualTo(JsSuccess(ddj)) - - val ldj = org.joda.time.LocalDate.parse(dfj.print(dj), dfj) - Json.toJson[org.joda.time.LocalDate](ldj).validate[org.joda.time.LocalDate] must beEqualTo(JsSuccess(ldj)) - val ds = new java.sql.Date(dd.getTime()) - - val dtfj = org.joda.time.format.DateTimeFormat.forPattern("HH:mm:ss.SSS") Json.toJson[java.sql.Date](ds).validate[java.sql.Date] must beEqualTo(JsSuccess(dd)) JsNumber(dd.getTime).validate[java.sql.Date] must beEqualTo(JsSuccess(dd)) - val ltj = org.joda.time.LocalTime.parse(dtfj.print(dj), dtfj) - Json.toJson[org.joda.time.LocalTime](ltj).validate[org.joda.time.LocalTime] must beEqualTo(JsSuccess(ltj)) - // very poor test to do really crappy java date APIs // TODO ISO8601 test doesn't work on CI platform... val c = java.util.Calendar.getInstance() diff --git a/framework/src/play-netty-server/src/main/scala/play/core/server/netty/NettyModelConversion.scala b/framework/src/play-netty-server/src/main/scala/play/core/server/netty/NettyModelConversion.scala index 07c64414bde..00328db3e4e 100644 --- a/framework/src/play-netty-server/src/main/scala/play/core/server/netty/NettyModelConversion.scala +++ b/framework/src/play-netty-server/src/main/scala/play/core/server/netty/NettyModelConversion.scala @@ -3,8 +3,9 @@ */ package play.core.server.netty -import java.net.{ URI, InetSocketAddress } +import java.net.{ InetSocketAddress, URI } import java.security.cert.X509Certificate +import java.time.Instant import javax.net.ssl.SSLEngine import javax.net.ssl.SSLPeerUnverifiedException @@ -20,7 +21,7 @@ import play.api.Logger import play.api.http._ import play.api.http.HeaderNames._ import play.api.mvc._ -import play.core.server.common.{ ConnectionInfo, ServerResultUtils, ForwardedHeaderHandler } +import play.core.server.common.{ ConnectionInfo, ForwardedHeaderHandler, ServerResultUtils } import scala.collection.JavaConverters._ import scala.concurrent.Future @@ -299,7 +300,7 @@ private[server] class NettyModelConversion(forwardedHeaderHandler: ForwardedHead case (cachedSeconds, dateHeaderString) if cachedSeconds == currentTimeSeconds => dateHeaderString case _ => - val dateHeaderString = ResponseHeader.httpDateFormat.print(currentTimeMillis) + val dateHeaderString = ResponseHeader.httpDateFormat.format(Instant.ofEpochMilli(currentTimeMillis)) cachedDateHeader = currentTimeSeconds -> dateHeaderString dateHeaderString } diff --git a/framework/src/play-server/src/main/scala/play/core/server/ssl/CertificateGenerator.scala b/framework/src/play-server/src/main/scala/play/core/server/ssl/CertificateGenerator.scala index ec9f6b68dda..c1209df6b75 100644 --- a/framework/src/play-server/src/main/scala/play/core/server/ssl/CertificateGenerator.scala +++ b/framework/src/play-server/src/main/scala/play/core/server/ssl/CertificateGenerator.scala @@ -11,7 +11,7 @@ import java.security._ import java.math.BigInteger import java.util.Date import sun.security.util.ObjectIdentifier -import org.joda.time.{ Duration, Days, Instant } +import java.time.{ Duration, Instant } import scala.util.Properties.isJavaAtLeast /** @@ -25,24 +25,24 @@ object CertificateGenerator { /** * Generates a certificate using RSA (which is available in 1.6). */ - def generateRSAWithSHA256(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Days.days(365).toStandardDuration): X509Certificate = { + def generateRSAWithSHA256(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Duration.ofDays(365)): X509Certificate = { val dn = "CN=localhost, OU=Unit Testing, O=Mavericks, L=Moon Base 1, ST=Cyberspace, C=CY" val to = from.plus(duration) val keyGen = KeyPairGenerator.getInstance("RSA") keyGen.initialize(keySize, new SecureRandom()) val pair = keyGen.generateKeyPair() - generateCertificate(dn, pair, from.toDate, to.toDate, "SHA256withRSA", AlgorithmId.sha256WithRSAEncryption_oid) + generateCertificate(dn, pair, Date.from(from), Date.from(to), "SHA256withRSA", AlgorithmId.sha256WithRSAEncryption_oid) } - def generateRSAWithSHA1(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Days.days(365).toStandardDuration): X509Certificate = { + def generateRSAWithSHA1(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Duration.ofDays(365)): X509Certificate = { val dn = "CN=localhost, OU=Unit Testing, O=Mavericks, L=Moon Base 1, ST=Cyberspace, C=CY" val to = from.plus(duration) val keyGen = KeyPairGenerator.getInstance("RSA") keyGen.initialize(keySize, new SecureRandom()) val pair = keyGen.generateKeyPair() - generateCertificate(dn, pair, from.toDate, to.toDate, "SHA1withRSA", AlgorithmId.sha256WithRSAEncryption_oid) + generateCertificate(dn, pair, Date.from(from), Date.from(to), "SHA1withRSA", AlgorithmId.sha256WithRSAEncryption_oid) } def toPEM(certificate: X509Certificate) = { @@ -57,14 +57,14 @@ object CertificateGenerator { pemCert } - def generateRSAWithMD5(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Days.days(365).toStandardDuration): X509Certificate = { + def generateRSAWithMD5(keySize: Int = 2048, from: Instant = Instant.now, duration: Duration = Duration.ofDays(365)): X509Certificate = { val dn = "CN=localhost, OU=Unit Testing, O=Mavericks, L=Moon Base 1, ST=Cyberspace, C=CY" val to = from.plus(duration) val keyGen = KeyPairGenerator.getInstance("RSA") keyGen.initialize(keySize, new SecureRandom()) val pair = keyGen.generateKeyPair() - generateCertificate(dn, pair, from.toDate, to.toDate, "MD5WithRSA", AlgorithmId.md5WithRSAEncryption_oid) + generateCertificate(dn, pair, Date.from(from), Date.from(to), "MD5WithRSA", AlgorithmId.md5WithRSAEncryption_oid) } private[play] def generateCertificate(dn: String, pair: KeyPair, from: Date, to: Date, algorithm: String, oid: ObjectIdentifier): X509Certificate = { diff --git a/framework/src/play-ws/src/main/scala/play/api/libs/ws/ssl/AlgorithmChecker.scala b/framework/src/play-ws/src/main/scala/play/api/libs/ws/ssl/AlgorithmChecker.scala index 9a3bbdee686..6feb0c401e5 100644 --- a/framework/src/play-ws/src/main/scala/play/api/libs/ws/ssl/AlgorithmChecker.scala +++ b/framework/src/play-ws/src/main/scala/play/api/libs/ws/ssl/AlgorithmChecker.scala @@ -4,11 +4,10 @@ package play.api.libs.ws.ssl import java.security.cert._ +import java.time.{ LocalDateTime, ZoneId, ZonedDateTime } import javax.naming.InvalidNameException import javax.naming.ldap.{ LdapName, Rdn } -import org.joda.time.{ DateTime, Interval } - import scala.collection.JavaConverters._ /** @@ -61,6 +60,7 @@ class AlgorithmChecker(val signatureConstraints: Set[AlgorithmConstraint], val k /** * Checks for signature algorithms in the certificate and throws CertPathValidatorException if matched. + * * @param x509Cert */ def checkSignatureAlgorithms(x509Cert: X509Certificate): Unit = { @@ -83,6 +83,7 @@ class AlgorithmChecker(val signatureConstraints: Set[AlgorithmConstraint], val k /** * Checks for key algorithms in the certificate and throws CertPathValidatorException if matched. + * * @param x509Cert */ def checkKeyAlgorithms(x509Cert: X509Certificate): Unit = { @@ -118,7 +119,7 @@ class AlgorithmChecker(val signatureConstraints: Set[AlgorithmConstraint], val k val commonName = getCommonName(x509Cert) val subAltNames = x509Cert.getSubjectAlternativeNames val certName = x509Cert.getSubjectX500Principal.getName - val expirationDate = new DateTime(x509Cert.getNotAfter.getTime) + val expirationDate = LocalDateTime.ofInstant(x509Cert.getNotAfter.toInstant, ZoneId.systemDefault()) logger.debug(s"check: checking certificate commonName = $commonName, subjAltName = $subAltNames, certName = $certName, expirationDate = $expirationDate") sunsetSHA1SignatureAlgorithm(x509Cert) @@ -136,7 +137,9 @@ class AlgorithmChecker(val signatureConstraints: Set[AlgorithmConstraint], val k * @param x509Cert */ def sunsetSHA1SignatureAlgorithm(x509Cert: X509Certificate): Unit = { - + def intervalContains(date1: LocalDateTime, date2: LocalDateTime, date3: LocalDateTime): Boolean = { + (date3.isEqual(date1) || date3.isAfter(date1)) && (date3.isEqual(date2) || date3.isBefore(date2)) + } val sigAlgName = x509Cert.getSigAlgName val sigAlgorithms = Algorithms.decomposes(sigAlgName) if (sigAlgorithms.contains("SHA1") || sigAlgorithms.contains("SHA-1")) { @@ -145,31 +148,30 @@ class AlgorithmChecker(val signatureConstraints: Set[AlgorithmConstraint], val k // // Sites with end-entity certificates that expire between 1 June 2016 to 31 December 2016 (inclusive), // and which include a SHA-1-based signature as part of the certificate chain, will be treated as “secure, but with minor errors”. - val june2016 = new DateTime(2016, 6, 1, 0, 0, 0, 0) - val december2016 = new DateTime(2016, 12, 31, 0, 0, 0, 0) - val secureInterval = new Interval(june2016, december2016) + val june2016 = LocalDateTime.of(2016, 6, 1, 0, 0, 0, 0) + val december2016 = LocalDateTime.of(2016, 12, 31, 0, 0, 0, 0) - val expirationDate = new DateTime(x509Cert.getNotAfter.getTime) - if (secureInterval.contains(expirationDate)) { + val expirationDate = LocalDateTime.ofInstant(x509Cert.getNotAfter.toInstant, ZoneId.systemDefault()) + if (intervalContains(june2016, december2016, expirationDate)) { infoOnSunset(x509Cert, expirationDate) } // Sites with end-entity certificates that expire on or after 1 January 2017, and which include // a SHA-1-based signature as part of the certificate chain, will be treated as // “neutral, lacking security”. - val january2017 = new DateTime(2017, 1, 1, 0, 0, 0, 0) + val january2017 = LocalDateTime.of(2017, 1, 1, 0, 0, 0, 0) if (january2017.isEqual(expirationDate) || january2017.isBefore(expirationDate)) { warnOnSunset(x509Cert, expirationDate) } } } - def infoOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + def infoOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { val certName = x509Cert.getSubjectX500Principal.getName logger.info(s"Certificate $certName uses SHA-1 and expires $expirationDate: this certificate expires soon, but SHA-1 is being sunsetted.") } - def warnOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + def warnOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { val certName = x509Cert.getSubjectX500Principal.getName logger.warn(s"Certificate $certName uses SHA-1 and expires $expirationDate: SHA-1 cannot be considered secure and this certificate should be replaced.") } diff --git a/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmCheckerSpec.scala b/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmCheckerSpec.scala index 029625d4f6b..fd881deb671 100644 --- a/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmCheckerSpec.scala +++ b/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmCheckerSpec.scala @@ -6,9 +6,9 @@ package play.api.libs.ws.ssl import java.security.cert.{ CertPathValidatorException, Certificate, X509Certificate } +import java.time.{ Duration, Instant, LocalDateTime } import java.util.Collections._ -import org.joda.time.{ DateTime, Days, Instant } import org.specs2.mutable._ import play.api.libs.ws.ssl.AlgorithmConstraintsParser._ import play.core.server.ssl.CertificateGenerator @@ -45,16 +45,16 @@ object AlgorithmCheckerSpec extends Specification { } "neither info nor warning on a signature containing sha-1 that expires before 1 June 2016" in { - val oneHundredAndEightyDays = Days.days(180).toStandardDuration + val oneHundredAndEightyDays = Duration.ofDays(180) val certificate = CertificateGenerator.generateRSAWithSHA1(2048, from = Instant.parse("2015-06-01T12:00:00Z"), duration = oneHundredAndEightyDays) var infoCalled = false var warningCalled = false val checker = new AlgorithmChecker(Set.empty, Set.empty) { - override def infoOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + override def infoOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { infoCalled = true } - override def warnOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + override def warnOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { warningCalled = true } } @@ -65,12 +65,12 @@ object AlgorithmCheckerSpec extends Specification { } "info on a signature containing sha-1 that expires between 1 June 2016 to 31 December 2016" in { - val thirtyDays = Days.days(30).toStandardDuration + val thirtyDays = Duration.ofDays(30) val certificate = CertificateGenerator.generateRSAWithSHA1(2048, from = Instant.parse("2016-06-01T12:00:00Z"), duration = thirtyDays) var infoCalled = false val checker = new AlgorithmChecker(Set.empty, Set.empty) { - override def infoOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + override def infoOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { infoCalled = true } } @@ -80,12 +80,12 @@ object AlgorithmCheckerSpec extends Specification { } "warn on a signature containing sha-1 that expires after 2017" in { - val tenYears = Days.days(365 * 10).toStandardDuration + val tenYears = Duration.ofDays(365 * 10) val certificate = CertificateGenerator.generateRSAWithSHA1(2048, from = Instant.parse("2016-06-01T12:00:00Z"), duration = tenYears) var warningCalled = false val checker = new AlgorithmChecker(Set.empty, Set.empty) { - override def warnOnSunset(x509Cert: X509Certificate, expirationDate: DateTime): Unit = { + override def warnOnSunset(x509Cert: X509Certificate, expirationDate: LocalDateTime): Unit = { warningCalled = true } } diff --git a/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmsSpec.scala b/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmsSpec.scala index 20ea49bd056..420fa96134e 100644 --- a/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmsSpec.scala +++ b/framework/src/play-ws/src/test/scala/play/api/libs/ws/ssl/AlgorithmsSpec.scala @@ -6,9 +6,11 @@ package play.api.libs.ws.ssl import org.specs2.mutable._ +import java.security.{ KeyPairGenerator, SecureRandom } +import java.time.Instant +import java.time.temporal.{ ChronoUnit, TemporalUnit } +import java.util.Date -import java.security.{ SecureRandom, KeyPairGenerator } -import org.joda.time.Instant import play.core.server.ssl.CertificateGenerator import sun.security.x509.AlgorithmId @@ -20,13 +22,13 @@ object AlgorithmsSpec extends Specification { "show a keysize of 1024 for RSA" in { val dn = "cn=Common Name, ou=eng ineering, o=company, c=US" val from = Instant.now - val to = from.plus(5000000) + val to = from.plus(5000000, ChronoUnit.MILLIS) // Use RSA with a SHA1 certificate signing algoirthm. val keyGen = KeyPairGenerator.getInstance("RSA") keyGen.initialize(1024, new SecureRandom()) val pair = keyGen.generateKeyPair() - val cert = CertificateGenerator.generateCertificate(dn, pair, from.toDate, to.toDate, "SHA1WithRSA", AlgorithmId.sha1WithRSAEncryption_oid) + val cert = CertificateGenerator.generateCertificate(dn, pair, Date.from(from), Date.from(to), "SHA1WithRSA", AlgorithmId.sha1WithRSAEncryption_oid) // RSA is getModulus.bitLength keySize(cert.getPublicKey) must_== Some(1024) @@ -35,13 +37,13 @@ object AlgorithmsSpec extends Specification { "show a keysize of 1024 for DSA" in { val dn = "cn=Common Name, ou=engineering, o=company, c=US" val from = Instant.now - val to = from.plus(5000000) + val to = from.plus(5000000, ChronoUnit.MILLIS) // Use RSA with a DSA certificate signing algoirthm. val keyGen = KeyPairGenerator.getInstance("DSA") keyGen.initialize(1024, new SecureRandom()) val pair = keyGen.generateKeyPair() - val cert = CertificateGenerator.generateCertificate(dn, pair, from.toDate, to.toDate, "SHA1WithDSA", AlgorithmId.sha1WithDSA_oid) + val cert = CertificateGenerator.generateCertificate(dn, pair, Date.from(from), Date.from(to), "SHA1WithDSA", AlgorithmId.sha1WithDSA_oid) // DSA is getP.bitLength keySize(cert.getPublicKey) must_== Some(1024) diff --git a/framework/src/play/src/main/resources/messages.default b/framework/src/play/src/main/resources/messages.default index 13c5c7accba..119eaee2bb7 100644 --- a/framework/src/play/src/main/resources/messages.default +++ b/framework/src/play/src/main/resources/messages.default @@ -43,8 +43,6 @@ error.uuid=Valid UUID required error.expected.date=Date value expected error.expected.date.isoformat=Iso date value expected error.expected.time=Time value expected -error.expected.jodadate.format=Joda date value expected -error.expected.jodatime.format=Joda time value expected error.expected.jsarray=Array value expected error.expected.jsboolean=Boolean value expected error.expected.jsnumber=Number value expected diff --git a/framework/src/play/src/main/scala/play/api/controllers/Assets.scala b/framework/src/play/src/main/scala/play/api/controllers/Assets.scala index ed252392951..d9f7661bf88 100644 --- a/framework/src/play/src/main/scala/play/api/controllers/Assets.scala +++ b/framework/src/play/src/main/scala/play/api/controllers/Assets.scala @@ -7,8 +7,7 @@ import play.api.libs._ import java.io._ import java.net.{JarURLConnection, URL, URLConnection} -import org.joda.time.format.{DateTimeFormat, DateTimeFormatter} -import org.joda.time.DateTimeZone +import java.time.format.DateTimeFormatter import play.utils.{InvalidUriEncodingException, Resources, UriEncoding} import scala.concurrent.{ExecutionContext, Future, Promise, blocking} @@ -31,7 +30,9 @@ package play.api.controllers { package controllers { -import akka.stream.scaladsl.{StreamConverters, Source} +import java.time._ + +import akka.stream.scaladsl.{ Source, StreamConverters } import play.api.controllers.TrampolineContextProvider object Execution extends TrampolineContextProvider @@ -109,9 +110,9 @@ private object AssetInfo { import ResponseHeader.basicDateFormatPattern val standardDateParserWithoutTZ: DateTimeFormatter = - DateTimeFormat.forPattern(basicDateFormatPattern).withLocale(java.util.Locale.ENGLISH).withZone(DateTimeZone.UTC) + DateTimeFormatter.ofPattern(basicDateFormatPattern).withLocale(java.util.Locale.ENGLISH).withZone(ZoneOffset.UTC) val alternativeDateFormatWithTZOffset: DateTimeFormatter = - DateTimeFormat.forPattern("EEE MMM dd yyyy HH:mm:ss 'GMT'Z").withLocale(java.util.Locale.ENGLISH).withZone(DateTimeZone.UTC).withOffsetParsed + DateTimeFormatter.ofPattern("EEE MMM dd yyyy HH:mm:ss 'GMT'Z").withLocale(java.util.Locale.ENGLISH) /** * A regex to find two types of date format. This regex silently ignores any @@ -125,19 +126,16 @@ private object AssetInfo { """^(((\w\w\w, \d\d \w\w\w \d\d\d\d \d\d:\d\d:\d\d)(( GMT)?))|""" + """(\w\w\w \w\w\w \d\d \d\d\d\d \d\d:\d\d:\d\d GMT.\d\d\d\d))(\b.*)""") - /* - * jodatime does not parse timezones, so we handle that manually - */ def parseModifiedDate(date: String): Option[Date] = { val matcher = dateRecognizer.matcher(date) if (matcher.matches()) { val standardDate = matcher.group(3) try { if (standardDate != null) { - Some(standardDateParserWithoutTZ.parseDateTime(standardDate).toDate) + Some(Date.from(ZonedDateTime.parse(standardDate, standardDateParserWithoutTZ).toInstant)) } else { val alternativeDate = matcher.group(6) // Cannot be null otherwise match would have failed - Some(alternativeDateFormatWithTZOffset.parseDateTime(alternativeDate).toDate) + Some(Date.from(ZonedDateTime.parse(alternativeDate, alternativeDateFormatWithTZOffset).toInstant)) } } catch { case e: IllegalArgumentException => @@ -187,11 +185,11 @@ private class AssetInfo( } finally { Resources.closeUrlConnection(urlConnection) } - }.filterNot(_ == -1).map(httpDateFormat.print) + }.filterNot(_ == -1).map(millis => httpDateFormat.format(Instant.ofEpochMilli(millis))) } url.getProtocol match { - case "file" => Some(httpDateFormat.print(new File(url.toURI).lastModified)) + case "file" => Some(httpDateFormat.format(Instant.ofEpochMilli(new File(url.toURI).lastModified))) case "jar" => getLastModified[JarURLConnection](c => c.getJarEntry.getTime) case "bundle" => getLastModified[URLConnection](c => c.getLastModified) case _ => None diff --git a/framework/src/play/src/main/scala/play/api/data/Forms.scala b/framework/src/play/src/main/scala/play/api/data/Forms.scala index 776fc3b57a5..5c391d623a9 100644 --- a/framework/src/play/src/main/scala/play/api/data/Forms.scala +++ b/framework/src/play/src/main/scala/play/api/data/Forms.scala @@ -549,51 +549,6 @@ object Forms { */ def sqlDate(pattern: String, timeZone: java.util.TimeZone = java.util.TimeZone.getDefault): Mapping[java.sql.Date] = of[java.sql.Date] as sqlDateFormat(pattern, timeZone) - /** - * Constructs a simple mapping for a date field (mapped as `org.joda.time.DateTime type`). - * - * For example: - * {{{ - * Form("birthdate" -> jodaDate) - * }}} - */ - val jodaDate: Mapping[org.joda.time.DateTime] = of[org.joda.time.DateTime] - - /** - * Constructs a simple mapping for a date field (mapped as `org.joda.time.DateTime type`). - * - * For example: - * {{{ - * Form("birthdate" -> jodaDate("dd-MM-yyyy")) - * }}} - * - * @param pattern the date pattern, as defined in `org.joda.time.format.DateTimeFormat` - * @param timeZone the `org.joda.time.DateTimeZone` to use for parsing and formatting - */ - def jodaDate(pattern: String, timeZone: org.joda.time.DateTimeZone = org.joda.time.DateTimeZone.getDefault): Mapping[org.joda.time.DateTime] = of[org.joda.time.DateTime] as jodaDateTimeFormat(pattern, timeZone) - - /** - * Constructs a simple mapping for a date field (mapped as `org.joda.time.LocalDatetype`). - * - * For example: - * {{{ - * Form("birthdate" -> jodaLocalDate) - * }}} - */ - val jodaLocalDate: Mapping[org.joda.time.LocalDate] = of[org.joda.time.LocalDate] - - /** - * Constructs a simple mapping for a date field (mapped as `org.joda.time.LocalDate type`). - * - * For example: - * {{{ - * Form("birthdate" -> jodaLocalDate("dd-MM-yyyy")) - * }}} - * - * @param pattern the date pattern, as defined in `org.joda.time.format.DateTimeFormat` - */ - def jodaLocalDate(pattern: String): Mapping[org.joda.time.LocalDate] = of[org.joda.time.LocalDate] as jodaLocalDateFormat(pattern) - /** * Constructs a simple mapping for an e-mail field. * diff --git a/framework/src/play/src/main/scala/play/api/data/format/Format.scala b/framework/src/play/src/main/scala/play/api/data/format/Format.scala index 567e785bc08..2051048d1a5 100644 --- a/framework/src/play/src/main/scala/play/api/data/format/Format.scala +++ b/framework/src/play/src/main/scala/play/api/data/format/Format.scala @@ -3,6 +3,10 @@ */ package play.api.data.format +import java.text.{ DateFormat, SimpleDateFormat } +import java.time.temporal.{ ChronoField, TemporalAccessor, TemporalField, TemporalQueries } +import java.time._ +import java.time.format.{ DateTimeFormatter, DateTimeFormatterBuilder, ResolverStyle } import java.util.UUID import play.api.data._ @@ -189,20 +193,23 @@ object Formats { /** * Formatter for the `java.util.Date` type. * - * @param pattern a date pattern, as specified in `org.joda.time.format.DateTimeFormat`. + * @param pattern a date pattern, as specified in `java.time.format.DateTimeFormatter`. * @param timeZone the `java.util.TimeZone` to use for parsing and formatting */ def dateFormat(pattern: String, timeZone: TimeZone = TimeZone.getDefault): Formatter[Date] = new Formatter[Date] { + val javaTimeZone = timeZone.toZoneId + val formatter = DateTimeFormatter.ofPattern(pattern) - val jodaTimeZone = org.joda.time.DateTimeZone.forTimeZone(timeZone) - val formatter = org.joda.time.format.DateTimeFormat.forPattern(pattern).withZone(jodaTimeZone) - def dateParse(data: String) = formatter.parseDateTime(data).toDate + def dateParse(data: String) = { + val instant = PlayDate.parse(data, formatter).toZonedDateTime(ZoneOffset.UTC) + Date.from(instant.withZoneSameLocal(javaTimeZone).toInstant) + } override val format = Some(("format.date", Seq(pattern))) def bind(key: String, data: Map[String, String]) = parsing(dateParse, "error.date", Nil)(key, data) - def unbind(key: String, value: Date) = Map(key -> formatter.print(new org.joda.time.DateTime(value).withZone(jodaTimeZone))) + def unbind(key: String, value: Date) = Map(key -> formatter.format(value.toInstant.atZone(javaTimeZone))) } /** @@ -213,7 +220,7 @@ object Formats { /** * Formatter for the `java.sql.Date` type. * - * @param pattern a date pattern as specified in `org.joda.time.format.DateTimeFormat`. + * @param pattern a date pattern as specified in `java.time.DateTimeFormatter`. * @param timeZone the `java.util.TimeZone` to use for parsing and formatting */ def sqlDateFormat(pattern: String, timeZone: TimeZone = TimeZone.getDefault): Formatter[java.sql.Date] = new Formatter[java.sql.Date] { @@ -234,52 +241,6 @@ object Formats { */ implicit val sqlDateFormat: Formatter[java.sql.Date] = sqlDateFormat("yyyy-MM-dd") - /** - * Formatter for the `org.joda.time.DateTime` type. - * - * @param pattern a date pattern as specified in `org.joda.time.format.DateTimeFormat`. - * @param timeZone the `org.joda.time.DateTimeZone` to use for parsing and formatting - */ - def jodaDateTimeFormat(pattern: String, timeZone: org.joda.time.DateTimeZone = org.joda.time.DateTimeZone.getDefault): Formatter[org.joda.time.DateTime] = new Formatter[org.joda.time.DateTime] { - - val formatter = org.joda.time.format.DateTimeFormat.forPattern(pattern).withZone(timeZone) - - override val format = Some(("format.date", Seq(pattern))) - - def bind(key: String, data: Map[String, String]) = parsing(formatter.parseDateTime, "error.date", Nil)(key, data) - - def unbind(key: String, value: org.joda.time.DateTime) = Map(key -> value.withZone(timeZone).toString(pattern)) - } - - /** - * Default formatter for `org.joda.time.DateTime` type with pattern `yyyy-MM-dd`. - */ - implicit val jodaDateTimeFormat: Formatter[org.joda.time.DateTime] = jodaDateTimeFormat("yyyy-MM-dd") - - /** - * Formatter for the `org.joda.time.LocalDate` type. - * - * @param pattern a date pattern as specified in `org.joda.time.format.DateTimeFormat`. - */ - def jodaLocalDateFormat(pattern: String): Formatter[org.joda.time.LocalDate] = new Formatter[org.joda.time.LocalDate] { - - import org.joda.time.LocalDate - - val formatter = org.joda.time.format.DateTimeFormat.forPattern(pattern) - def jodaLocalDateParse(data: String) = LocalDate.parse(data, formatter) - - override val format = Some(("format.date", Seq(pattern))) - - def bind(key: String, data: Map[String, String]) = parsing(jodaLocalDateParse, "error.date", Nil)(key, data) - - def unbind(key: String, value: LocalDate) = Map(key -> value.toString(pattern)) - } - - /** - * Default formatter for `org.joda.time.LocalDate` type with pattern `yyyy-MM-dd`. - */ - implicit val jodaLocalDateFormat: Formatter[org.joda.time.LocalDate] = jodaLocalDateFormat("yyyy-MM-dd") - /** * Formatter for the `java.time.LocalDate` type. * diff --git a/framework/src/play/src/main/scala/play/api/data/format/PlayDate.scala b/framework/src/play/src/main/scala/play/api/data/format/PlayDate.scala new file mode 100644 index 00000000000..dee8ee8fccc --- /dev/null +++ b/framework/src/play/src/main/scala/play/api/data/format/PlayDate.scala @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009-2016 Lightbend Inc. + */ +package play.api.data.format + +import java.time.format.DateTimeFormatter +import java.time.temporal._ +import java.time.{ LocalDateTime, ZoneId, ZonedDateTime } + +private[play] object PlayDate { + def parse(text: CharSequence, formatter: DateTimeFormatter): PlayDate = new PlayDate(formatter.parse(text)) +} + +private[play] class PlayDate(accessor: TemporalAccessor) { + + private[this] def getOrDefault(field: TemporalField, default: Int): Int = { + if (accessor.isSupported(field)) accessor.get(field) else default + } + + def toZonedDateTime(zoneId: ZoneId): ZonedDateTime = { + val year: Int = getOrDefault(ChronoField.YEAR, 1970) + val month: Int = getOrDefault(ChronoField.MONTH_OF_YEAR, 1) + val day: Int = getOrDefault(ChronoField.DAY_OF_MONTH, 1) + val hour: Int = getOrDefault(ChronoField.HOUR_OF_DAY, 0) + val minute: Int = getOrDefault(ChronoField.MINUTE_OF_HOUR, 0) + + ZonedDateTime.of(LocalDateTime.of(year, month, day, hour, minute), zoneId) + } + +} \ No newline at end of file diff --git a/framework/src/play/src/main/scala/play/api/http/package.scala b/framework/src/play/src/main/scala/play/api/http/package.scala index 0d5d9527236..f08b1f7c529 100644 --- a/framework/src/play/src/main/scala/play/api/http/package.scala +++ b/framework/src/play/src/main/scala/play/api/http/package.scala @@ -3,8 +3,8 @@ */ package play.api -import org.joda.time.format.DateTimeFormat -import org.joda.time.DateTimeZone +import java.time.format.DateTimeFormatter +import java.time.ZoneId /** * Contains standard HTTP constants. @@ -17,5 +17,5 @@ import org.joda.time.DateTimeZone */ package object http { /** HTTP date formatter, compliant to RFC 1123 */ - val dateFormat = DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'").withLocale(java.util.Locale.ENGLISH).withZone(DateTimeZone.forID("GMT")) + val dateFormat = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'").withLocale(java.util.Locale.ENGLISH).withZone(ZoneId.of("GMT")) } diff --git a/framework/src/play/src/main/scala/play/api/mvc/Results.scala b/framework/src/play/src/main/scala/play/api/mvc/Results.scala index 600319c86c0..0050a43ba89 100644 --- a/framework/src/play/src/main/scala/play/api/mvc/Results.scala +++ b/framework/src/play/src/main/scala/play/api/mvc/Results.scala @@ -5,11 +5,11 @@ package play.api.mvc import java.nio.charset.StandardCharsets import java.nio.file.{ Files, Path } +import java.time.{ ZoneOffset, ZonedDateTime } +import java.time.format.DateTimeFormatter import akka.stream.scaladsl.{ FileIO, Source, StreamConverters } import akka.util.ByteString -import org.joda.time.format.{ DateTimeFormat, DateTimeFormatter } -import org.joda.time.{ DateTime, DateTimeZone } import play.api.http.HeaderNames._ import play.api.http._ import play.api.i18n.{ Lang, MessagesApi } @@ -51,9 +51,9 @@ final class ResponseHeader(val status: Int, _headers: Map[String, String] = Map. object ResponseHeader { val basicDateFormatPattern = "EEE, dd MMM yyyy HH:mm:ss" val httpDateFormat: DateTimeFormatter = - DateTimeFormat.forPattern(basicDateFormatPattern + " 'GMT'") + DateTimeFormatter.ofPattern(basicDateFormatPattern + " 'GMT'") .withLocale(java.util.Locale.ENGLISH) - .withZone(DateTimeZone.UTC) + .withZone(ZoneOffset.UTC) def apply(status: Int, headers: Map[String, String] = Map.empty, reasonPhrase: Option[String] = None): ResponseHeader = new ResponseHeader(status, headers) @@ -89,9 +89,9 @@ case class Result(header: ResponseHeader, body: HttpEntity) { * @param headers the headers with a DateTime to add to this result. * @return the new result. */ - def withDateHeaders(headers: (String, DateTime)*): Result = { + def withDateHeaders(headers: (String, ZonedDateTime)*): Result = { copy(header = header.copy(headers = header.headers ++ headers.map { - case (name, dateTime) => (name, ResponseHeader.httpDateFormat.print(dateTime.getMillis)) + case (name, dateTime) => (name, dateTime.format(ResponseHeader.httpDateFormat)) })) } diff --git a/framework/src/play/src/test/scala/play/api/controllers/AssetsInfoSpec.scala b/framework/src/play/src/test/scala/play/api/controllers/AssetsInfoSpec.scala index 7c769dc28dc..e957313e200 100644 --- a/framework/src/play/src/test/scala/play/api/controllers/AssetsInfoSpec.scala +++ b/framework/src/play/src/test/scala/play/api/controllers/AssetsInfoSpec.scala @@ -3,10 +3,11 @@ */ package controllers +import java.time._ + import org.specs2.mutable.Specification import java.util.Date -import org.joda.time.format.ISODateTimeFormat -import org.joda.time.DateTimeZone +import java.time.format.DateTimeFormatter object AssetInfoSpec extends Specification { @@ -15,8 +16,8 @@ object AssetInfoSpec extends Specification { def parseAndReformat(s: String): Option[String] = { val parsed: Option[Date] = AssetInfo.parseModifiedDate(s) parsed.map { date => - val format = ISODateTimeFormat.dateTime.withZone(DateTimeZone.UTC) - format.print(date.getTime) + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.000z") + .format(ZonedDateTime.ofInstant(date.toInstant, ZoneOffset.UTC)) } } diff --git a/framework/src/play/src/test/scala/play/api/controllers/AssetsSpec.scala b/framework/src/play/src/test/scala/play/api/controllers/AssetsSpec.scala index 729cdc32f12..22ae4bbc8a3 100644 --- a/framework/src/play/src/test/scala/play/api/controllers/AssetsSpec.scala +++ b/framework/src/play/src/test/scala/play/api/controllers/AssetsSpec.scala @@ -3,6 +3,8 @@ */ package controllers +import java.time.Instant + import org.specs2.mutable.Specification import play.api.mvc.ResponseHeader import play.utils.InvalidUriEncodingException @@ -103,9 +105,9 @@ object AssetsSpec extends Specification { "use the unescaped path when finding the last modified date of an asset" in { val url = AssetsSpec.getClass.getClassLoader.getResource("file withspace.css") val assetInfo = new AssetInfo("file withspace.css", url, None, None) - val lastModified = ResponseHeader.httpDateFormat.parseDateTime(assetInfo.lastModified.get) + val lastModified = ResponseHeader.httpDateFormat.parse(assetInfo.lastModified.get) // If it uses the escaped path, the file won't be found, and so last modified will be 0 - lastModified.toDate.getTime must_!= 0 + Instant.from(lastModified).toEpochMilli must_!= 0 } } } diff --git a/framework/src/play/src/test/scala/play/api/data/FormSpec.scala b/framework/src/play/src/test/scala/play/api/data/FormSpec.scala index 7283eaf507c..7a6cfb8d935 100644 --- a/framework/src/play/src/test/scala/play/api/data/FormSpec.scala +++ b/framework/src/play/src/test/scala/play/api/data/FormSpec.scala @@ -10,7 +10,6 @@ import play.api.data.format.Formats._ import play.api.i18n.{ DefaultLangs, DefaultMessagesApi } import play.api.libs.json.Json import org.specs2.mutable.Specification -import org.joda.time.{ DateTime, LocalDate } object FormSpec extends Specification { "A form" should { @@ -230,24 +229,6 @@ object FormSpec extends Specification { ScalaForms.helloForm.bind(Map("name" -> "foo", "repeat" -> "1")).get.toString must equalTo("(foo,1,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None)") } - "render form using jodaDate" in { - val dateForm = Form(("date" -> jodaDate)) - val data = Map("date" -> "2012-01-01") - dateForm.bind(data).get mustEqual (new DateTime(2012, 1, 1, 0, 0)) - } - - "render form using jodaDate with format(30/1/2012)" in { - val dateForm = Form(("date" -> jodaDate("dd/MM/yyyy"))) - val data = Map("date" -> "30/1/2012") - dateForm.bind(data).get mustEqual (new DateTime(2012, 1, 30, 0, 0)) - } - - "render form using jodaLocalDate with format(30/1/2012)" in { - val dateForm = Form(("date" -> jodaLocalDate("dd/MM/yyyy"))) - val data = Map("date" -> "30/1/2012") - dateForm.bind(data).get mustEqual (new LocalDate(2012, 1, 30)) - } - "reject input if it contains global errors" in { Form("value" -> nonEmptyText).withGlobalError("some.error") .bind(Map("value" -> "some value")) diff --git a/framework/src/play/src/test/scala/play/api/data/format/PlayDateSpec.scala b/framework/src/play/src/test/scala/play/api/data/format/PlayDateSpec.scala new file mode 100644 index 00000000000..7a39ee4b788 --- /dev/null +++ b/framework/src/play/src/test/scala/play/api/data/format/PlayDateSpec.scala @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016. envisia GmbH + * All Rights Reserved. + */ +package play.api.data.format + +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +import org.specs2.mutable.Specification + +object PlayDateSpec extends Specification { + + "PlayDate.toZonedDateTime(ZoneId)" should { + "return a valid date" in { + val date = PlayDate.parse("2016 16:01", DateTimeFormatter.ofPattern("yyyy HH:mm")) + + date.toZonedDateTime(ZoneOffset.UTC).getHour must_=== 16 + date.toZonedDateTime(ZoneOffset.UTC).getYear must_=== 2016 + } + } + +} diff --git a/framework/src/play/src/test/scala/play/api/mvc/ResultsSpec.scala b/framework/src/play/src/test/scala/play/api/mvc/ResultsSpec.scala index 989a7377b8f..931e8ad57d8 100644 --- a/framework/src/play/src/test/scala/play/api/mvc/ResultsSpec.scala +++ b/framework/src/play/src/test/scala/play/api/mvc/ResultsSpec.scala @@ -6,9 +6,9 @@ package play.api.mvc import java.io.File import java.nio.charset.StandardCharsets import java.nio.file.{ Files, Path, Paths } +import java.time.{ LocalDateTime, ZoneOffset } import java.util.concurrent.atomic.AtomicInteger -import org.joda.time.{ DateTime, DateTimeZone } import org.specs2.mutable._ import play.api.http.HeaderNames._ import play.api.http.Status._ @@ -67,7 +67,7 @@ object ResultsSpec extends Specification { "support date headers manipulation" in { val Result(ResponseHeader(_, headers, _), _) = Ok("hello").as("text/html").withDateHeaders(DATE -> - new DateTime(2015, 4, 1, 0, 0).withZoneRetainFields(DateTimeZone.UTC)) + LocalDateTime.of(2015, 4, 1, 0, 0).atZone(ZoneOffset.UTC)) headers must havePair(DATE -> "Wed, 01 Apr 2015 00:00:00 GMT") }