diff --git a/documentation/README.md b/documentation/README.md index 38b2bfa8e43..2c50d0b4354 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -81,7 +81,7 @@ cd $PLAY_HOME/framework sbt compile doc package ``` -All Play projects can see documentation embedded by going to [http://localhost:9000/@documentation](http://localhost:9000/@documentation). Internally, the @documentation route goes to `DocumentationServer` in the play-docs subproject, which relies on [play-doc](https://github.com/playframework/play-doc] for generating HTML from the raw Markdown. +All Play projects can see documentation embedded by going to [http://localhost:9000/@documentation](http://localhost:9000/@documentation). Internally, the @documentation route goes to `DocumentationServer` in the play-docs subproject, which relies on [play-doc](https://github.com/playframework/play-doc) for generating HTML from the raw Markdown. ## Running diff --git a/documentation/manual/working/javaGuide/main/http/JavaActions.md b/documentation/manual/working/javaGuide/main/http/JavaActions.md index c974617b775..95d0eb56614 100644 --- a/documentation/manual/working/javaGuide/main/http/JavaActions.md +++ b/documentation/manual/working/javaGuide/main/http/JavaActions.md @@ -9,7 +9,7 @@ An action is basically a Java method that processes the request parameters, and @[simple-action](code/javaguide/http/JavaActions.java) -An action returns a `play.mvc.Result` value, representing the HTTP response to send to the web client. In this example `ok` constructs a **200 OK** response containing a **text/plain** response body. For more examples of HTTP responses see [`play.mvc.Result` methods](api/java/play/mvc/Results.html#method.summary). +An action returns a `play.mvc.Result` value, representing the HTTP response to send to the web client. In this example `ok` constructs a **200 OK** response containing a **text/plain** response body. For more examples of HTTP responses see [`play.mvc.Results` methods](api/java/play/mvc/Results.html#method.summary). ## Controllers diff --git a/documentation/manual/working/scalaGuide/main/http/ScalaActions.md b/documentation/manual/working/scalaGuide/main/http/ScalaActions.md index d2055d469b4..ca3573fc9e0 100644 --- a/documentation/manual/working/scalaGuide/main/http/ScalaActions.md +++ b/documentation/manual/working/scalaGuide/main/http/ScalaActions.md @@ -29,6 +29,10 @@ It is often useful to mark the `request` parameter as `implicit` so it can be im @[implicit-request-action](code/ScalaActions.scala) +If you have broken up your code into methods, then you can pass through the implicit request from the action: + +@[implicit-request-action-with-more-methods](code/ScalaActions.scala) + The last way of creating an Action value is to specify an additional `BodyParser` argument: @[json-parser-action](code/ScalaActions.scala) diff --git a/documentation/manual/working/scalaGuide/main/http/code/ScalaActions.scala b/documentation/manual/working/scalaGuide/main/http/code/ScalaActions.scala index dc3c0849234..d652055d30d 100644 --- a/documentation/manual/working/scalaGuide/main/http/code/ScalaActions.scala +++ b/documentation/manual/working/scalaGuide/main/http/code/ScalaActions.scala @@ -55,6 +55,20 @@ class ScalaActionsSpec extends AbstractController(Helpers.stubControllerComponen ) } + "pass the request implicitly to the action with more methods" in { + //#implicit-request-action-with-more-methods + def action = Action { implicit request => + anotherMethod("Some para value") + Ok("Got request [" + request + "]") + } + + def anotherMethod(p: String)(implicit request: Request[_]) = { + // do something that needs access to the request + } + //#implicit-request-action-with-more-methods + testAction(action) + } + "allow specifying a parser" in { testAction(action = //#json-parser-action diff --git a/framework/project/Dependencies.scala b/framework/project/Dependencies.scala index cd1aded6cb7..0dcdbaeb355 100644 --- a/framework/project/Dependencies.scala +++ b/framework/project/Dependencies.scala @@ -25,7 +25,7 @@ object Dependencies { val specsSbt = specsBuild - val jacksonVersion = "2.8.10" + val jacksonVersion = "2.8.11" val jacksons = Seq( "com.fasterxml.jackson.core" % "jackson-core", "com.fasterxml.jackson.core" % "jackson-annotations", @@ -56,7 +56,7 @@ object Dependencies { val jdbcDeps = Seq( "com.jolbox" % "bonecp" % "0.8.0.RELEASE", - "com.zaxxer" % "HikariCP" % "2.7.4", + "com.zaxxer" % "HikariCP" % "2.7.5", "com.googlecode.usc" % "jdbcdslog" % "1.0.6.2", h2database % Test, acolyte % Test, @@ -152,7 +152,7 @@ object Dependencies { specsBuild.map(_ % Test) ++ javaTestDeps - val nettyVersion = "4.1.18.Final" + val nettyVersion = "4.1.19.Final" val netty = Seq( "com.typesafe.netty" % "netty-reactive-streams-http" % "2.0.0", diff --git a/framework/src/play-akka-http-server/src/main/resources/reference.conf b/framework/src/play-akka-http-server/src/main/resources/reference.conf index 2abf208502b..c3e400d53df 100644 --- a/framework/src/play-akka-http-server/src/main/resources/reference.conf +++ b/framework/src/play-akka-http-server/src/main/resources/reference.conf @@ -13,8 +13,8 @@ play { # How long to wait when binding to the listening socket bindTimeout = 5 seconds - # How long a request takes until it times out - requestTimeout = null + # How long a request takes until it times out. Set to null or "infinite" to disable the timeout. + requestTimeout = infinite # Enables/disables automatic handling of HEAD requests. # If this setting is enabled the server dispatches HEAD requests as GET diff --git a/framework/src/play-akka-http-server/src/main/scala/play/core/server/AkkaHttpServer.scala b/framework/src/play-akka-http-server/src/main/scala/play/core/server/AkkaHttpServer.scala index fea926a307e..964d77d6242 100644 --- a/framework/src/play-akka-http-server/src/main/scala/play/core/server/AkkaHttpServer.scala +++ b/framework/src/play-akka-http-server/src/main/scala/play/core/server/AkkaHttpServer.scala @@ -81,16 +81,15 @@ class AkkaHttpServer( val initialSettings = ServerSettings(initialConfig) val idleTimeout = serverConfig.get[Duration](if (secure) "https.idleTimeout" else "http.idleTimeout") - val requestTimeoutOption = akkaServerConfig.getOptional[Duration]("requestTimeout") + val requestTimeout = akkaServerConfig.get[Duration]("requestTimeout") // all akka settings that are applied to the server needs to be set here - val serverSettings: ServerSettings = initialSettings.withTimeouts { - val timeouts = initialSettings.timeouts.withIdleTimeout(idleTimeout) - requestTimeoutOption match { - case Some(requestTimeout) => timeouts.withRequestTimeout(requestTimeout) - case None => timeouts - } - } + val serverSettings: ServerSettings = initialSettings + .withTimeouts( + initialSettings.timeouts + .withIdleTimeout(idleTimeout) + .withRequestTimeout(requestTimeout) + ) // Play needs these headers to fill in fields in its request model .withRawRequestUriHeader(true) .withRemoteAddressHeader(true) diff --git a/framework/src/play-filters-helpers/src/main/scala/play/filters/cors/AbstractCORSPolicy.scala b/framework/src/play-filters-helpers/src/main/scala/play/filters/cors/AbstractCORSPolicy.scala index f3a57c0bbb0..0a73183c614 100644 --- a/framework/src/play-filters-helpers/src/main/scala/play/filters/cors/AbstractCORSPolicy.scala +++ b/framework/src/play-filters-helpers/src/main/scala/play/filters/cors/AbstractCORSPolicy.scala @@ -107,7 +107,10 @@ private[cors] trait AbstractCORSPolicy { * headers and terminate this set of steps. */ if (!corsConfig.allowedOrigins(origin)) { - handleInvalidCORSRequest(request) + if (corsConfig.serveForbiddenOrigins) + next(request) + else + handleInvalidCORSRequest(request) } else { import play.core.Execution.Implicits.trampoline diff --git a/framework/src/play-filters-helpers/src/test/scala/play/filters/cors/CORSCommonSpec.scala b/framework/src/play-filters-helpers/src/test/scala/play/filters/cors/CORSCommonSpec.scala index 66d2fa46a56..a12e2a9ec2e 100644 --- a/framework/src/play-filters-helpers/src/test/scala/play/filters/cors/CORSCommonSpec.scala +++ b/framework/src/play-filters-helpers/src/test/scala/play/filters/cors/CORSCommonSpec.scala @@ -74,13 +74,22 @@ trait CORSCommonSpec extends PlaySpecification { } "forbidden" in withApplication(conf = serveForbidden) { app => val result = route(app, fakeRequest().withHeaders( - ORIGIN -> "http://www.example.com" + ORIGIN -> "http://www.notinwhitelistorhost.com" )).get status(result) must_== OK header(VARY, result) must beSome(ORIGIN) mustBeNoAccessControlResponseHeaders(result) } + "in the whitelist" in withApplication(conf = serveForbidden) { app => + val result = route(app, fakeRequest().withHeaders( + ORIGIN -> "http://example.org" + )).get + + status(result) must_== OK + header(ACCESS_CONTROL_ALLOW_ORIGIN, result) must beSome("http://example.org") + header(VARY, result) must beSome(ORIGIN) + } } "not consider sub domains to be the same origin" in withApplication() { app => diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/ActionCompositionOrderTest.java b/framework/src/play-integration-test/src/test/java/play/it/http/ActionCompositionOrderTest.java index 3dd774061be..3a072b1aa0d 100644 --- a/framework/src/play-integration-test/src/test/java/play/it/http/ActionCompositionOrderTest.java +++ b/framework/src/play-integration-test/src/test/java/play/it/http/ActionCompositionOrderTest.java @@ -7,6 +7,7 @@ import play.test.Helpers; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -57,4 +58,59 @@ public CompletionStage call(Http.Context ctx) { return delegate.call(ctx.withRequest(ctx.request().addAttr(Security.USERNAME, configuration.value()))); } } + + @With({FirstAction.class, SecondAction.class}) // let's run two actions + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(SomeRepeatable.List.class) + public static @interface SomeRepeatable { + /** + * Defines several {@code @SomeRepeatable} annotations on the same element. + */ + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface List { + SomeRepeatable[] value(); + } + } + + public static class FirstAction extends Action { + @Override + public CompletionStage call(Http.Context ctx) { + return delegate.call(ctx).thenApply(result -> { + String newContent = this.annotatedElement.getClass().getName() + "action1" + Helpers.contentAsString(result); + return Results.ok(newContent); + }); + } + } + + public static class SecondAction extends Action { + @Override + public CompletionStage call(Http.Context ctx) { + return delegate.call(ctx).thenApply(result -> { + String newContent = this.annotatedElement.getClass().getName() + "action2" + Helpers.contentAsString(result); + return Results.ok(newContent); + }); + } + } + + /** + * Could be seen as a container annotation (like SomeRepeatable.List above), however it defines @With so it's simply seen as action annotation + */ + @With(SomeActionAnnotationAction.class) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public static @interface SomeActionAnnotation { + SomeRepeatable[] value(); + } + + public static class SomeActionAnnotationAction extends Action { + @Override + public CompletionStage call(Http.Context ctx) { + return delegate.call(ctx).thenApply(result -> { + String newContent = "do_NOT_treat_me_as_container_annotation" + Helpers.contentAsString(result); + return Results.ok(newContent); + }); + } + } } diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnActionController.java new file mode 100644 index 00000000000..85f0b32a7b6 --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnActionController.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +public class MultipleRepeatableOnActionController extends MockController { + + @SomeRepeatable // runs two actions + @SomeRepeatable // plus two more + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeAndActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeAndActionController.java new file mode 100644 index 00000000000..fa03a85496d --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeAndActionController.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +@SomeRepeatable // runs two actions +@SomeRepeatable // once more, so makes it four +public class MultipleRepeatableOnTypeAndActionController extends MockController { + + @SomeRepeatable // again runs two actions + @SomeRepeatable // plus two more + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeController.java b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeController.java new file mode 100644 index 00000000000..324bac8009c --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/MultipleRepeatableOnTypeController.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +@SomeRepeatable // runs two actions +@SomeRepeatable // once more, so makes it four +public class MultipleRepeatableOnTypeController extends MockController { + + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/RepeatableBackwardCompatibilityController.java b/framework/src/play-integration-test/src/test/java/play/it/http/RepeatableBackwardCompatibilityController.java new file mode 100644 index 00000000000..84b56033ab9 --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/RepeatableBackwardCompatibilityController.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; +import play.mvc.With; + +import play.it.http.ActionCompositionOrderTest.SomeActionAnnotation; +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +/** + * Checks backward compatibility: + * Here only SomeActionAnnotation should run but the inner actions should NOT. + * We always check first if an outer annotation has @With defined before trying to unwrap it to see if it may is a container annotation. + * If SomeActionAnnotation below would not define @With it would be seen as container annotation and the the wrapped annotations would run - + * but also just because the inner annotations have @Repeatable defined; if they wouldn't be defined @Repeatable then they wouldn't run as well. + */ +public class RepeatableBackwardCompatibilityController extends MockController { + + @SomeActionAnnotation({ // -> defines @With and therefore is NOT seen as container annotation + @SomeRepeatable, // -> is defined @Repeatable and also has @With so this could be an actual action annotation that could run + @SomeRepeatable + }) + public Result action() { + return Results.ok(); + } +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnActionController.java new file mode 100644 index 00000000000..dd13fbf69ab --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnActionController.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +public class SingleRepeatableOnActionController extends MockController { + + @SomeRepeatable // runs two actions + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeAndActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeAndActionController.java new file mode 100644 index 00000000000..34a75e75f7d --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeAndActionController.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +@SomeRepeatable // runs two actions +public class SingleRepeatableOnTypeAndActionController extends MockController { + + @SomeRepeatable // again runs two actions + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeController.java b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeController.java new file mode 100644 index 00000000000..b00ccf4039d --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/SingleRepeatableOnTypeController.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; + +import play.it.http.ActionCompositionOrderTest.SomeRepeatable; + +@SomeRepeatable // runs two actions +public class SingleRepeatableOnTypeController extends MockController { + + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/WithOnActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnActionController.java new file mode 100644 index 00000000000..1e5f3adad42 --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnActionController.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; +import play.mvc.With; + +import play.it.http.ActionCompositionOrderTest.FirstAction; +import play.it.http.ActionCompositionOrderTest.SecondAction; + +public class WithOnActionController extends MockController { + + @With({FirstAction.class, SecondAction.class}) + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeAndActionController.java b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeAndActionController.java new file mode 100644 index 00000000000..d01335297cf --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeAndActionController.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; +import play.mvc.With; + +import play.it.http.ActionCompositionOrderTest.FirstAction; +import play.it.http.ActionCompositionOrderTest.SecondAction; + +@With({FirstAction.class, SecondAction.class}) +public class WithOnTypeAndActionController extends MockController { + + @With({FirstAction.class, SecondAction.class}) + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeController.java b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeController.java new file mode 100644 index 00000000000..c787f8b2cb2 --- /dev/null +++ b/framework/src/play-integration-test/src/test/java/play/it/http/WithOnTypeController.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.it.http; + +import play.mvc.Result; +import play.mvc.Results; +import play.mvc.With; + +import play.it.http.ActionCompositionOrderTest.FirstAction; +import play.it.http.ActionCompositionOrderTest.SecondAction; + +@With({FirstAction.class, SecondAction.class}) +public class WithOnTypeController extends MockController { + + public Result action() { + return Results.ok(); + } + +} diff --git a/framework/src/play-integration-test/src/test/scala/play/it/http/JavaActionCompositionSpec.scala b/framework/src/play-integration-test/src/test/scala/play/it/http/JavaActionCompositionSpec.scala index 2489480f68c..539567ace4b 100644 --- a/framework/src/play-integration-test/src/test/scala/play/it/http/JavaActionCompositionSpec.scala +++ b/framework/src/play-integration-test/src/test/scala/play/it/http/JavaActionCompositionSpec.scala @@ -52,6 +52,15 @@ class BuiltInComponentsJavaActionCompositionSpec extends JavaActionCompositionSp .addAction(classOf[ActionCompositionOrderTest.WithUsernameAction], new JSupplier[ActionCompositionOrderTest.WithUsernameAction] { override def get(): ActionCompositionOrderTest.WithUsernameAction = new ActionCompositionOrderTest.WithUsernameAction() }) + .addAction(classOf[ActionCompositionOrderTest.FirstAction], new JSupplier[ActionCompositionOrderTest.FirstAction] { + override def get(): ActionCompositionOrderTest.FirstAction = new ActionCompositionOrderTest.FirstAction() + }) + .addAction(classOf[ActionCompositionOrderTest.SecondAction], new JSupplier[ActionCompositionOrderTest.SecondAction] { + override def get(): ActionCompositionOrderTest.SecondAction = new ActionCompositionOrderTest.SecondAction() + }) + .addAction(classOf[ActionCompositionOrderTest.SomeActionAnnotationAction], new JSupplier[ActionCompositionOrderTest.SomeActionAnnotationAction] { + override def get(): ActionCompositionOrderTest.SomeActionAnnotationAction = new ActionCompositionOrderTest.SomeActionAnnotationAction() + }) } override def router(): JRouter = { @@ -159,6 +168,69 @@ trait JavaActionCompositionSpec extends PlaySpecification with WsTestClient { setCookie must contain("foo=bar") response.body must_== "foo" } + + "run a single @Repeatable annotation on a controller type" in makeRequest(new SingleRepeatableOnTypeController()) { response => + response.body must beEqualTo("""java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } + + "run a single @Repeatable annotation on a controller action" in makeRequest(new SingleRepeatableOnActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2""".stripMargin.replaceAll("\n", "")) + } + + "run multiple @Repeatable annotations on a controller type" in makeRequest(new MultipleRepeatableOnTypeController()) { response => + response.body must beEqualTo("""java.lang.Classaction1 + |java.lang.Classaction2 + |java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } + + "run multiple @Repeatable annotations on a controller action" in makeRequest(new MultipleRepeatableOnActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2 + |java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2""".stripMargin.replaceAll("\n", "")) + } + + "run single @Repeatable annotation on a controller type and a controller action" in makeRequest(new SingleRepeatableOnTypeAndActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2 + |java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } + + "run multiple @Repeatable annotations on a controller type and a controller action" in makeRequest(new MultipleRepeatableOnTypeAndActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2 + |java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2 + |java.lang.Classaction1 + |java.lang.Classaction2 + |java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } + + "run @Repeatable action composition annotations backward compatible" in makeRequest(new RepeatableBackwardCompatibilityController()) { response => + response.body must beEqualTo("do_NOT_treat_me_as_container_annotation") + } + + "run @With annotation on a controller type" in makeRequest(new WithOnTypeController()) { response => + response.body must beEqualTo("""java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } + + "run @With annotation on a controller action" in makeRequest(new WithOnActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2""".stripMargin.replaceAll("\n", "")) + } + + "run @With annotations on a controller type and a controller action" in makeRequest(new WithOnTypeAndActionController()) { response => + response.body must beEqualTo("""java.lang.reflect.Methodaction1 + |java.lang.reflect.Methodaction2 + |java.lang.Classaction1 + |java.lang.Classaction2""".stripMargin.replaceAll("\n", "")) + } } "When action composition is configured to invoke request handler action first" should { diff --git a/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/Evolutions.scala b/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/Evolutions.scala index 06c6d76ca80..21b2d38ddd0 100644 --- a/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/Evolutions.scala +++ b/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/Evolutions.scala @@ -102,11 +102,15 @@ object Evolutions { */ def fileName(db: String, revision: Int): String = s"${directoryName(db)}/${revision}.sql" + def fileName(db: String, revision: String): String = s"${directoryName(db)}/${revision}.sql" + /** * Default evolution resource name. */ def resourceName(db: String, revision: Int): String = s"evolutions/${db}/${revision}.sql" + def resourceName(db: String, revision: String): String = s"evolutions/${db}/${revision}.sql" + /** * Apply pending evolutions for the given database. */ diff --git a/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/EvolutionsApi.scala b/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/EvolutionsApi.scala index 11b8271c78d..b9a5e52735d 100644 --- a/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/EvolutionsApi.scala +++ b/framework/src/play-jdbc-evolutions/src/main/scala/play/api/db/evolutions/EvolutionsApi.scala @@ -4,6 +4,8 @@ package play.api.db.evolutions import java.io.InputStream +import java.io.File +import java.net.URL import java.sql._ import javax.inject.{ Inject, Singleton } @@ -12,6 +14,7 @@ import play.api.libs.Collections import play.api.{ Environment, Logger, PlayException } import play.utils.PlayIO +import scala.annotation.tailrec import scala.io.Codec import scala.util.control.NonFatal @@ -71,7 +74,7 @@ trait EvolutionsApi { /** * Apply pending evolutions for the given database. */ - def applyFor(dbName: String, path: java.io.File = new java.io.File("."), autocommit: Boolean = true, schema: String = ""): Unit = { + def applyFor(dbName: String, path: File = new File("."), autocommit: Boolean = true, schema: String = ""): Unit = { val scripts = this.scripts(dbName, new EnvironmentEvolutionsReader(Environment.simple(path = path)), schema) this.evolve(dbName, scripts, autocommit, schema) } @@ -491,10 +494,22 @@ abstract class ResourceEvolutionsReader extends EvolutionsReader { @Singleton class EnvironmentEvolutionsReader @Inject() (environment: Environment) extends ResourceEvolutionsReader { - def loadResource(db: String, revision: Int) = { - environment.getExistingFile(Evolutions.fileName(db, revision)).map(f => java.nio.file.Files.newInputStream(f.toPath)).orElse { - environment.resourceAsStream(Evolutions.resourceName(db, revision)) + import DefaultEvolutionsApi._ + + def loadResource(db: String, revision: Int): Option[InputStream] = { + @tailrec def findPaddedRevisionResource(paddedRevision: String, url: Option[URL]): Option[InputStream] = { + if (paddedRevision.length > 15) { + url.map(_.openStream()) // Revision string has reached max padding + } else { + val resource = environment.resource(Evolutions.resourceName(db, paddedRevision)) + for { + u <- url + r <- resource + } yield logger.warn(s"Ignoring evolution script ${new File(r.getPath()).getName()}, using ${new File(u.getPath()).getName()} instead already") + findPaddedRevisionResource("0" + paddedRevision, url.orElse(resource)) + } } + findPaddedRevisionResource(revision.toString, None) } } diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/001.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/001.sql new file mode 100644 index 00000000000..7fcd362f9cc --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/001.sql @@ -0,0 +1,7 @@ +# --- !Ups + +create table do_not_create_that_table (id bigint not null, name varchar(255)); + +# --- !Downs + +drop table if exists do_not_create_that_table; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0010.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0010.sql new file mode 100644 index 00000000000..13f1bc8bba1 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0010.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (99, 'Isabella'); + +# --- !Downs + +delete from test where name = 'Isabella'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/002.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/002.sql new file mode 100644 index 00000000000..b23fc7e0cd8 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/002.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (96, 'Benjamin'); + +# --- !Downs + +delete from test where name = 'Benjamin'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/005.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/005.sql new file mode 100644 index 00000000000..b788df5b14a --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/005.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (95, 'Charlotte'); + +# --- !Downs + +delete from test where name = 'Charlotte'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/01.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/01.sql new file mode 100644 index 00000000000..811584a16bc --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/01.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (98, 'James'); + +# --- !Downs + +delete from test where name = 'James'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/010.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/010.sql new file mode 100644 index 00000000000..3a79d9cde59 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/010.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (11, 'Mason'); + +# --- !Downs + +delete from test where name = 'Mason'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0100.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0100.sql new file mode 100644 index 00000000000..c8455b5b3ee --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/0100.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (94, 'Jacob'); + +# --- !Downs + +delete from test where name = 'Jacob'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/02.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/02.sql new file mode 100644 index 00000000000..8b98e00be92 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/02.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (97, 'Mia'); + +# --- !Downs + +delete from test where name = 'Mia'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/05.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/05.sql new file mode 100644 index 00000000000..f0a1983b178 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/05.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (6, 'Noah'); + +# --- !Downs + +delete from test where name = 'Noah'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/4.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/4.sql index 187aee98d27..54ca3008eca 100644 --- a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/4.sql +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/4.sql @@ -1 +1,7 @@ +# --- !Ups + +insert into test (id, name) values (5, 'Emma'); + # --- !Downs + +delete from test where name = 'Emma'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/6.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/6.sql new file mode 100644 index 00000000000..fc9082e3f50 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/6.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (7, 'Olivia'); + +# --- !Downs + +delete from test where name = 'Olivia'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/7.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/7.sql new file mode 100644 index 00000000000..9553eb9c8ee --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/7.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (8, 'Liam'); + +# --- !Downs + +delete from test where name = 'Liam'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/8.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/8.sql new file mode 100644 index 00000000000..9aba3d15d70 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/8.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (9, 'William'); + +# --- !Downs + +delete from test where name = 'William'; diff --git a/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/9.sql b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/9.sql new file mode 100644 index 00000000000..0c21c4e5b3e --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/resources/evolutions/test/9.sql @@ -0,0 +1,9 @@ +# --- Test data set + +# --- !Ups + +insert into test (id, name) values (10, 'Sophia'); + +# --- !Downs + +delete from test where name = 'Sophia'; diff --git a/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/EvolutionsReaderSpec.scala b/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/EvolutionsReaderSpec.scala index 2f859fd8467..57e8b2f370d 100644 --- a/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/EvolutionsReaderSpec.scala +++ b/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/EvolutionsReaderSpec.scala @@ -11,7 +11,8 @@ class EvolutionsReaderSpec extends Specification { "EnvironmentEvolutionsReader" should { - "read evolution files from classpath" in { + "read evolution files from classpath" in withLogbackCapturingAppender { + val appender = LogbackCapturingAppender[DefaultEvolutionsApi] val environment = Environment(new File("."), getClass.getClassLoader, Mode.Test) val reader = new EnvironmentEvolutionsReader(environment) @@ -19,9 +20,30 @@ class EvolutionsReaderSpec extends Specification { Evolution(1, "create table test (id bigint not null, name varchar(255));", "drop table if exists test;"), Evolution(2, "insert into test (id, name) values (1, 'alice');\ninsert into test (id, name) values (2, 'bob');", "delete from test;"), Evolution(3, "insert into test (id, name) values (3, 'charlie');\ninsert into test (id, name) values (4, 'dave');", ""), - Evolution(4, "", "") + Evolution(4, "insert into test (id, name) values (5, 'Emma');", "delete from test where name = 'Emma';"), + Evolution(5, "insert into test (id, name) values (6, 'Noah');", "delete from test where name = 'Noah';"), + Evolution(6, "insert into test (id, name) values (7, 'Olivia');", "delete from test where name = 'Olivia';"), + Evolution(7, "insert into test (id, name) values (8, 'Liam');", "delete from test where name = 'Liam';"), + Evolution(8, "insert into test (id, name) values (9, 'William');", "delete from test where name = 'William';"), + Evolution(9, "insert into test (id, name) values (10, 'Sophia');", "delete from test where name = 'Sophia';"), + Evolution(10, "insert into test (id, name) values (11, 'Mason');", "delete from test where name = 'Mason';") + // revision file 100 will not even run because revision 11 - 99 do not exist + ) + appender.events.map(_.getMessage) must_== Seq( + "Ignoring evolution script 01.sql, using 1.sql instead already", + "Ignoring evolution script 001.sql, using 1.sql instead already", + "Ignoring evolution script 02.sql, using 2.sql instead already", + "Ignoring evolution script 002.sql, using 2.sql instead already", + "Ignoring evolution script 005.sql, using 05.sql instead already", + "Ignoring evolution script 0010.sql, using 010.sql instead already" ) } } + + private def withLogbackCapturingAppender[T](block: => T): T = { + val result = block + LogbackCapturingAppender.detachAll() + result + } } diff --git a/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/LogbackCapturingAppender.scala b/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/LogbackCapturingAppender.scala new file mode 100644 index 00000000000..0d0071ed2b3 --- /dev/null +++ b/framework/src/play-jdbc-evolutions/src/test/scala/play/api/db/evolutions/LogbackCapturingAppender.scala @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.api.db.evolutions + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.classic.{ Level, Logger => LogbackLogger } +import ch.qos.logback.core.AppenderBase +import org.slf4j.{ Logger => Slf4jLogger, LoggerFactory } +import scala.reflect.ClassTag + +import scala.collection.mutable + +class LogbackCapturingAppender private (slf4jLogger: Slf4jLogger) extends AppenderBase[ILoggingEvent] { + + private val _logger: LogbackLogger = { + val logger = slf4jLogger.asInstanceOf[LogbackLogger] + logger.setLevel(Level.ALL) + logger.addAppender(this) + logger + } + + private val _events: mutable.ArrayBuffer[ILoggingEvent] = new mutable.ArrayBuffer + + /** + * Start the appender + */ + start() + + /** + * Returns the list of all captured logging events + */ + def events: Seq[ILoggingEvent] = _events.toSeq + + protected def append(event: ILoggingEvent): Unit = synchronized { + _events += event + } + + private def detach(): Unit = { + _logger.detachAppender(this) + _events.clear() + } +} + +object LogbackCapturingAppender { + private[this] val _appenders: mutable.ArrayBuffer[LogbackCapturingAppender] = new mutable.ArrayBuffer + + def apply[T](implicit ct: ClassTag[T]): LogbackCapturingAppender = + attachForLogger(LoggerFactory.getLogger(ct.runtimeClass)) + + /** + * Get a capturing appender for the given logger + */ + def attachForLogger(playLogger: play.api.Logger): LogbackCapturingAppender = attachForLogger(playLogger.logger) + + /** + * Get a capturing appender for the given logger + */ + def attachForLogger(slf4jLogger: Slf4jLogger): LogbackCapturingAppender = { + val appender = new LogbackCapturingAppender(slf4jLogger) + _appenders += appender + appender + } + + /** + * Detach all the appenders we attached + */ + def detachAll(): Unit = { + _appenders foreach (_.detach()) + _appenders.clear() + } +} diff --git a/framework/src/play-openid/src/main/resources/logback-test.xml b/framework/src/play-openid/src/test/resources/logback-test.xml similarity index 100% rename from framework/src/play-openid/src/main/resources/logback-test.xml rename to framework/src/play-openid/src/test/resources/logback-test.xml diff --git a/framework/src/play/src/main/java/play/libs/AnnotationUtils.java b/framework/src/play/src/main/java/play/libs/AnnotationUtils.java new file mode 100644 index 00000000000..4cb2935d08a --- /dev/null +++ b/framework/src/play/src/main/java/play/libs/AnnotationUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +package play.libs; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Annotation utilities. + */ +public class AnnotationUtils { + + /** + * Returns a new array whose entries do not contain container annotations anymore but the indirectly present annotation(s) a container annotation + * was wrapping instead. An annotation is considered a container annotation if its indirectly present annotation(s) are annotated with {@link Repeatable}. + * Annotations inside the given array which don't meet the above definition of a container annotations will be returned untouched. + * + * @param annotations An array of annotations to unwrap. Can contain both container and non container annotations. + * @return A new array without container annotations but the container annotations' indirectly defined annotations. + */ + public static Annotation[] unwrapContainerAnnotations(final A[] annotations) { + final List unwrappedAnnotations = new LinkedList<>(); + for (final Annotation maybeContainerAnnotation : annotations) { + final List indirectlyPresentAnnotations = getIndirectlyPresentAnnotations(maybeContainerAnnotation); + if (!indirectlyPresentAnnotations.isEmpty()) { + unwrappedAnnotations.addAll(indirectlyPresentAnnotations); + } else { + unwrappedAnnotations.add(maybeContainerAnnotation); // was not a container annotation + } + } + return unwrappedAnnotations.toArray(new Annotation[unwrappedAnnotations.size()]); + } + + /** + * If the return type of an existing {@code value()} method of the passed annotation is an {@code Annotation[]} array and the annotations inside that + * {@code Annotation[]} array are annotated with the {@link Repeatable} annotation the annotations of that array will be returned. + * If the passed annotation does not have a {@code value()} method or the above criteria are not met an empty list will be returned instead. + * + * @param maybeContainerAnnotation The annotation which {@code value()} method will be checked for other annotations + * @return The annotations defined by the {@code value()} method or an empty list. + */ + public static List getIndirectlyPresentAnnotations(final A maybeContainerAnnotation) { + try { + final Method method = maybeContainerAnnotation.annotationType().getMethod("value"); + final Object o = method.invoke(maybeContainerAnnotation); + if (Annotation[].class.isAssignableFrom(o.getClass())) { + final Annotation[] indirectAnnotations = (Annotation[])o; + if (indirectAnnotations.length > 0 && indirectAnnotations[0].annotationType().isAnnotationPresent(Repeatable.class)) { + return Arrays.asList(indirectAnnotations); + } + } + } catch (final NoSuchMethodException e) { + // That's ok, this just wasn't a container annotation -> continue + } catch (final SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + return Collections.emptyList(); + } +} diff --git a/framework/src/play/src/main/resources/reference.conf b/framework/src/play/src/main/resources/reference.conf index 280c4ba7627..fba2cc28b58 100644 --- a/framework/src/play/src/main/resources/reference.conf +++ b/framework/src/play/src/main/resources/reference.conf @@ -850,8 +850,8 @@ play { # The name of the actor system that Play creates actor-system = "application" - # How long Play should wait for Akka to shutdown before timing it. If null, waits indefinitely. - shutdown-timeout = null + # How long Play should wait for Akka to shutdown before timing it. If "infinite" or null, waits indefinitely. + shutdown-timeout = infinite # The location to read Play's Akka configuration from config = "akka" diff --git a/framework/src/play/src/main/scala/play/api/Configuration.scala b/framework/src/play/src/main/scala/play/api/Configuration.scala index ea422e42b1f..f63dd8af560 100644 --- a/framework/src/play/src/main/scala/play/api/Configuration.scala +++ b/framework/src/play/src/main/scala/play/api/Configuration.scala @@ -33,7 +33,7 @@ object Configuration { private[this] lazy val dontAllowMissingConfig = ConfigFactory.load(dontAllowMissingConfigOptions) - private[play] def load( + def load( classLoader: ClassLoader, properties: Properties, directSettings: Map[String, AnyRef], 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 4dd2455f5cd..f1cf772c7f7 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 @@ -243,6 +243,8 @@ package controllers { } object AssetsConfiguration { + private val logger = Logger(getClass) + def fromConfiguration(c: Configuration, mode: Mode = Mode.Test): AssetsConfiguration = { val assetsConfiguration = AssetsConfiguration( path = c.get[String]("play.assets.path"), @@ -265,14 +267,14 @@ package controllers { private def logAssetsConfiguration(assetsConfiguration: AssetsConfiguration): Unit = { val msg = new StringBuffer() - msg.append("Using the following cache for assets configuration:\n") - msg.append(s"\t enabledCaching = ${assetsConfiguration.enableCaching}\n") - msg.append(s"\t enabledCacheControl = ${assetsConfiguration.enableCacheControl}\n") + msg.append("Using the following cache configuration for assets:\n") + msg.append(s"\t enableCaching = ${assetsConfiguration.enableCaching}\n") + msg.append(s"\t enableCacheControl = ${assetsConfiguration.enableCacheControl}\n") msg.append(s"\t defaultCacheControl = ${assetsConfiguration.defaultCacheControl}\n") msg.append(s"\t aggressiveCacheControl = ${assetsConfiguration.aggressiveCacheControl}\n") msg.append(s"\t configuredCacheControl:") msg.append(assetsConfiguration.configuredCacheControl.map(c => s"\t\t ${c._1} = ${c._2}").mkString("\n", "\n", "\n")) - Logger.warn(msg.toString) + logger.debug(msg.toString) } private def getAssetEncodings(c: Configuration): Seq[AssetEncoding] = { diff --git a/framework/src/play/src/main/scala/play/core/j/JavaAction.scala b/framework/src/play/src/main/scala/play/core/j/JavaAction.scala index 3800da23f33..8fb03ee6944 100644 --- a/framework/src/play/src/main/scala/play/core/j/JavaAction.scala +++ b/framework/src/play/src/main/scala/play/core/j/JavaAction.scala @@ -18,8 +18,10 @@ import play.core.Execution.Implicits.trampoline import play.api.mvc._ import play.mvc.{ FileMimeTypes, Action => JAction, BodyParser => JBodyParser, Result => JResult } import play.i18n.{ Langs => JLangs, MessagesApi => JMessagesApi } +import play.libs.AnnotationUtils import play.mvc.Http.{ Context => JContext } +import scala.collection.JavaConverters._ import scala.concurrent.{ ExecutionContext, Future } /** @@ -49,6 +51,10 @@ class JavaActionAnnotations(val controller: Class[_], val method: java.lang.refl case (a: play.mvc.With, ae) => a.value.map(c => (a, c, ae)).toSeq case (a, ae) if a.annotationType.isAnnotationPresent(classOf[play.mvc.With]) => a.annotationType.getAnnotation(classOf[play.mvc.With]).value.map(c => (a, c, ae)).toSeq + case (a, ae) if !a.annotationType.isAnnotationPresent(classOf[play.mvc.With]) => + AnnotationUtils.getIndirectlyPresentAnnotations(a).asScala.filter(_.annotationType.isAnnotationPresent(classOf[play.mvc.With])).flatMap(ia => + ia.annotationType.getAnnotation(classOf[play.mvc.With]).value.map(c => (ia, c, ae)) + ) }.flatten.reverse } diff --git a/framework/version.sbt b/framework/version.sbt index bfbb3d1d18c..77cf5263c11 100644 --- a/framework/version.sbt +++ b/framework/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.6.10" +version in ThisBuild := "2.6.11"