From 496e9dc69ee28091426d852ce1f11d79ccdbf24b Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Wed, 8 Feb 2017 20:04:02 +0200 Subject: [PATCH] Fixes #1095 add marker interface for play controllers it would allow users to use custom implementations of base controller See https://github.com/playframework/play1/issues/1095 --- framework/build.xml | 4 + framework/src/play/mvc/ActionInvoker.java | 33 +-- framework/src/play/mvc/Controller.java | 8 +- framework/src/play/mvc/Http.java | 4 +- framework/src/play/mvc/PlayController.java | 10 + .../app/controllers/Application.java | 23 ++ .../app/views/Application/index.html | 4 + .../nonstatic-app/app/views/main.html | 13 + .../nonstatic-app/conf/application.conf | 240 ++++++++++++++++++ .../nonstatic-app/conf/dependencies.yml | 4 + samples-and-tests/nonstatic-app/conf/messages | 3 + samples-and-tests/nonstatic-app/conf/routes | 13 + .../nonstatic-app/test/ApplicationTest.java | 24 ++ 13 files changed, 361 insertions(+), 22 deletions(-) create mode 100644 framework/src/play/mvc/PlayController.java create mode 100644 samples-and-tests/nonstatic-app/app/controllers/Application.java create mode 100644 samples-and-tests/nonstatic-app/app/views/Application/index.html create mode 100644 samples-and-tests/nonstatic-app/app/views/main.html create mode 100644 samples-and-tests/nonstatic-app/conf/application.conf create mode 100644 samples-and-tests/nonstatic-app/conf/dependencies.yml create mode 100644 samples-and-tests/nonstatic-app/conf/messages create mode 100644 samples-and-tests/nonstatic-app/conf/routes create mode 100644 samples-and-tests/nonstatic-app/test/ApplicationTest.java diff --git a/framework/build.xml b/framework/build.xml index 50d0275817..1d917b0fa8 100644 --- a/framework/build.xml +++ b/framework/build.xml @@ -290,6 +290,10 @@ + + + + diff --git a/framework/src/play/mvc/ActionInvoker.java b/framework/src/play/mvc/ActionInvoker.java index d758174932..ee5ca3fd66 100644 --- a/framework/src/play/mvc/ActionInvoker.java +++ b/framework/src/play/mvc/ActionInvoker.java @@ -218,7 +218,7 @@ public static void invoke(Http.Request request, Http.Response response) { private static void invokeControllerCatchMethods(Throwable throwable) throws Exception { // @Catch Object[] args = new Object[] {throwable}; - List catches = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Catch.class); + List catches = Java.findAllAnnotatedMethods(getControllerClass(), Catch.class); ControllerInstrumentation.stopActionCall(); for (Method mCatch : catches) { Class[] exceptions = mCatch.getAnnotation(Catch.class).value(); @@ -268,7 +268,7 @@ public static Method findActionMethod(String name, Class clazz) { } private static void handleBefores(Http.Request request) throws Exception { - List befores = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Before.class); + List befores = Java.findAllAnnotatedMethods(getControllerClass(), Before.class); ControllerInstrumentation.stopActionCall(); for (Method before : befores) { String[] unless = before.getAnnotation(Before.class).unless(); @@ -302,7 +302,7 @@ private static void handleBefores(Http.Request request) throws Exception { } private static void handleAfters(Http.Request request) throws Exception { - List afters = Java.findAllAnnotatedMethods(Controller.getControllerClass(), After.class); + List afters = Java.findAllAnnotatedMethods(getControllerClass(), After.class); ControllerInstrumentation.stopActionCall(); for (Method after : afters) { String[] unless = after.getAnnotation(After.class).unless(); @@ -348,13 +348,13 @@ private static void handleAfters(Http.Request request) throws Exception { */ static void handleFinallies(Http.Request request, Throwable caughtException) throws PlayException { - if (Controller.getControllerClass() == null) { + if (getControllerClass() == null) { // skip it return; } try { - List allFinally = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Finally.class); + List allFinally = Java.findAllAnnotatedMethods(Request.current().controllerClass, Finally.class); ControllerInstrumentation.stopActionCall(); for (Method aFinally : allFinally) { String[] unless = aFinally.getAnnotation(Finally.class).unless(); @@ -455,9 +455,7 @@ public static Object invokeControllerMethod(Method method, Object[] forceArgs) t if (declaringClassName.endsWith("$class")) { args[0] = scalaInstance; // Scala trait method } else { - request.controllerInstance = (Controller) scalaInstance; // Scala - // object - // method + request.controllerInstance = (PlayController) scalaInstance; // Scala object method } } catch (NoSuchFieldException e) { // not Scala @@ -500,11 +498,11 @@ static Object invoke(Method method, Object instance, Object ... realArgs) throws static Object invokeWithContinuation(Method method, Object instance, Object[] realArgs) throws Exception { // Callback case - if (Http.Request.current().args.containsKey(A)) { + if (Request.current().args.containsKey(A)) { // Action0 - instance = Http.Request.current().args.get(A); - Future f = (Future) Http.Request.current().args.get(F); + instance = Request.current().args.get(A); + Future f = (Future) Request.current().args.get(F); Scope.RenderArgs renderArgs = (Scope.RenderArgs) Request.current().args.remove(ActionInvoker.CONTINUATIONS_STORE_RENDER_ARGS); Scope.RenderArgs.current.set(renderArgs); if (f == null) { @@ -520,7 +518,7 @@ static Object invokeWithContinuation(Method method, Object instance, Object[] re } // Continuations case - Continuation continuation = (Continuation) Http.Request.current().args.get(C); + Continuation continuation = (Continuation) Request.current().args.get(C); if (continuation == null) { continuation = new Continuation(new StackRecorder((Runnable) null)); } @@ -541,7 +539,7 @@ static Object invokeWithContinuation(Method method, Object instance, Object[] re } Object trigger = pStackRecorder.value; Continuation nextContinuation = new Continuation(pStackRecorder); - Http.Request.current().args.put(C, nextContinuation); + Request.current().args.put(C, nextContinuation); if (trigger instanceof Long) { throw new Suspend((Long) trigger); @@ -555,7 +553,7 @@ static Object invokeWithContinuation(Method method, Object instance, Object[] re throw new UnexpectedException("Unexpected continuation trigger -> " + trigger); } else { - Http.Request.current().args.remove(C); + Request.current().args.remove(C); } } finally { pStackRecorder.deregisterThread(old); @@ -577,10 +575,10 @@ public static Object[] getActionMethod(String fullAction) { if (controllerClass == null) { throw new ActionNotFoundException(fullAction, new Exception("Controller " + controller + " not found")); } - if (!ControllerSupport.class.isAssignableFrom(controllerClass)) { + if (!PlayController.class.isAssignableFrom(controllerClass)) { // Try the scala way controllerClass = Play.classloader.getClassIgnoreCase(controller + "$"); - if (!ControllerSupport.class.isAssignableFrom(controllerClass)) { + if (!PlayController.class.isAssignableFrom(controllerClass)) { throw new ActionNotFoundException(fullAction, new Exception("class " + controller + " does not extend play.mvc.Controller")); } @@ -636,4 +634,7 @@ public static Object[] getActionMethodArgs(Method method, Object o) throws Excep return rArgs; } + private static Class getControllerClass() { + return Http.Request.current().controllerClass; + } } diff --git a/framework/src/play/mvc/Controller.java b/framework/src/play/mvc/Controller.java index e230f30b4d..5f81c9d122 100644 --- a/framework/src/play/mvc/Controller.java +++ b/framework/src/play/mvc/Controller.java @@ -69,10 +69,9 @@ * Application controller support: The controller receives input and initiates a * response by making calls on model objects. * - * This is the class that your controllers should extend. - * + * This is the class that your controllers should extend in most cases. */ -public class Controller implements ControllerSupport, LocalVariablesSupport { +public class Controller implements PlayController, ControllerSupport, LocalVariablesSupport { /** * The current HTTP request: the message sent by the client to the server. @@ -963,8 +962,9 @@ protected static T getControllerInheritedAnnotation(Class * * @return Annotation object or null if not found */ + @SuppressWarnings("unchecked") protected static Class getControllerClass() { - return Http.Request.current().controllerClass; + return (Class) Http.Request.current().controllerClass; } /** diff --git a/framework/src/play/mvc/Http.java b/framework/src/play/mvc/Http.java index 3bb9f87941..63b8ff07b8 100644 --- a/framework/src/play/mvc/Http.java +++ b/framework/src/play/mvc/Http.java @@ -265,11 +265,11 @@ public static class Request implements Serializable { /** * The invoked controller class */ - public transient Class controllerClass; + public transient Class controllerClass; /** * The instance of invoked controller in case it uses non-static action methods. */ - public transient Controller controllerInstance; + public transient PlayController controllerInstance; /** * Free space to store your request specific data */ diff --git a/framework/src/play/mvc/PlayController.java b/framework/src/play/mvc/PlayController.java new file mode 100644 index 0000000000..69d7029684 --- /dev/null +++ b/framework/src/play/mvc/PlayController.java @@ -0,0 +1,10 @@ +package play.mvc; + +/** + * Marker interface for play controllers + * + * This is the class that your controllers should implement. + * In most cases, you can extend play.mvc.Controller that contains all needed methods for controllers. + */ +public interface PlayController { +} diff --git a/samples-and-tests/nonstatic-app/app/controllers/Application.java b/samples-and-tests/nonstatic-app/app/controllers/Application.java new file mode 100644 index 0000000000..ffd470260f --- /dev/null +++ b/samples-and-tests/nonstatic-app/app/controllers/Application.java @@ -0,0 +1,23 @@ +package controllers; + +import play.mvc.PlayController; +import play.mvc.results.RenderTemplate; +import play.mvc.results.RenderText; +import play.mvc.results.Result; +import play.templates.Template; +import play.templates.TemplateLoader; + +import static java.util.Collections.emptyMap; + +public class Application implements PlayController { + + public Result index() { + Template template = TemplateLoader.load("Application/index.html"); + return new RenderTemplate(template, emptyMap()); + } + + public Result hello() { + return new RenderText("Hello world!"); + } + +} \ No newline at end of file diff --git a/samples-and-tests/nonstatic-app/app/views/Application/index.html b/samples-and-tests/nonstatic-app/app/views/Application/index.html new file mode 100644 index 0000000000..16f814b355 --- /dev/null +++ b/samples-and-tests/nonstatic-app/app/views/Application/index.html @@ -0,0 +1,4 @@ +#{extends 'main.html' /} +#{set title:'Home' /} + +

Welcome to the non-static world!

\ No newline at end of file diff --git a/samples-and-tests/nonstatic-app/app/views/main.html b/samples-and-tests/nonstatic-app/app/views/main.html new file mode 100644 index 0000000000..8456f2096d --- /dev/null +++ b/samples-and-tests/nonstatic-app/app/views/main.html @@ -0,0 +1,13 @@ + + + + + #{get 'title' /} + + #{get 'moreStyles' /} + + + #{doLayout /} + #{get 'moreScripts' /} + + diff --git a/samples-and-tests/nonstatic-app/conf/application.conf b/samples-and-tests/nonstatic-app/conf/application.conf new file mode 100644 index 0000000000..81a9b3db1d --- /dev/null +++ b/samples-and-tests/nonstatic-app/conf/application.conf @@ -0,0 +1,240 @@ +# This is the main configuration file for the application. +# ~~~~~ +application.name=nonstatic-app + +# Application mode +# ~~~~~ +# Set to dev to enable instant reloading and other development help. +# Otherwise set to prod. +application.mode=dev +%prod.application.mode=prod + +# Secret key +# ~~~~~ +# The secret key is used to secure cryptographics functions +# If you deploy your application to several instances be sure to use the same key ! +application.secret=LGhGcdAqQYyEG5GX7sauvQxeZXQBoQNpFflbeHT0mGM9u7oRxzShTrRYZNIc4tvR + +# i18n +# ~~~~~ +# Define locales used by your application. +# You can then place localized messages in conf/messages.{locale} files +# application.langs=fr,en,ja + +# Date format +# ~~~~~ +date.format=yyyy-MM-dd +# date.format.fr=dd/MM/yyyy + +# Server configuration +# ~~~~~ +# If you need to change the HTTP port, uncomment this (default is set to 9000) +# http.port=9000 +# +# By default the server listen for HTTP on the wildcard address. +# You can restrict this. +# http.address=127.0.0.1 +# +# Use this if you don't host your Play application at the root of the domain +# you're serving it from. This parameter has no effect when deployed as a +# war, because the path will be handled by the application server. +# http.path=/ + +# Session configuration +# ~~~~~~~~~~~~~~~~~~~~~~ +# By default, session will be written to the transient PLAY_SESSION cookie. +# The cookies are not secured by default, only set it to true +# if you're serving your pages through https. +# application.session.cookie=PLAY +# application.session.maxAge=1h +# application.session.secure=false + +# Session/Cookie sharing between subdomain +# ~~~~~~~~~~~~~~~~~~~~~~ +# By default a cookie is only valid for a specific domain. By setting +# application.defaultCookieDomain to '.example.com', the cookies +# will be valid for all domains ending with '.example.com', ie: +# foo.example.com and bar.example.com +# application.defaultCookieDomain=.example.com + +# JVM configuration +# ~~~~~ +# Define which port is used by JPDA when application is in debug mode (default is set to 8000) +# jpda.port=8000 +# +# Java source level => 1.8 +# java.source=1.8 + +# Log level +# ~~~~~ +# Specify log level for your application. +# If you want a very customized log, create a log4j.properties file in the conf directory +# application.log=INFO +# +# More logging configuration +# application.log.path=/log4j.properties +# application.log.system.out=off + +# Database configuration +# ~~~~~ +# Enable a database engine if needed. +# +# To quickly set up a development database, use either: +# - mem : for a transient in memory database (H2 in memory) +# - fs : for a simple file written database (H2 file stored) +# db.default=mem +# +# To connect to a local MySQL5 database, use: +# db.default=mysql://user:pwd@host/database +# +# To connect to a local PostgreSQL9 database, use: +# db=postgres://user:pwd@host/database +# +# If you need a full JDBC configuration use the following : +# db.default.url=jdbc:postgresql:database_name +# db.default.driver=org.postgresql.Driver +# db.default.user=root +# db.default.pass=secret +# +# Connections pool configuration : +# db.default.pool.timeout=1000 +# db.default.pool.maxSize=30 +# db.default.pool.minSize=10 +# +# If you want to reuse an existing Datasource from your application server, use: +# db.default=java:/comp/env/jdbc/myDatasource +# +# When using an existing Datasource, it's sometimes needed to destroy it when +# the application is stopped. Depending on the datasource, you can define a +# generic "destroy" method : +# db.default.destroyMethod=close + +# JPA Configuration (Hibernate) +# ~~~~~ +# +# Specify the custom JPA dialect to use here (default to guess): +# jpa.default.dialect=org.hibernate.dialect.PostgreSQLDialect +# +# Specify the ddl generation pattern to use. Set to none to disable it +# (default to update in DEV mode, and none in PROD mode): +# jpa.default.ddl=update +# +# Debug SQL statements (logged using DEBUG level): +# jpa.default.debugSQL=true +# +# You can even specify additional hibernate properties here: +# default.hibernate.use_sql_comments=true +# ... +# +# Store path for Blob content +attachments.path=data/attachments + +# Memcached configuration +# ~~~~~ +# Enable memcached if needed. Otherwise a local cache is used. +# memcached=enabled +# +# Specify memcached host (default to 127.0.0.1:11211) +# memcached.host=127.0.0.1:11211 +# +# Or you can specify multiple host to build a distributed cache +# memcached.1.host=127.0.0.1:11211 +# memcached.2.host=127.0.0.1:11212 +# +# Use plain SASL to authenticate for memcached +# memcached.user= +# memcached.password= + +# HTTP Response headers control for static files +# ~~~~~ +# Set the default max-age, telling the user's browser how long it should cache the page. +# Default is 3600 (one hour). Set it to 0 to send no-cache. +# This is only read in prod mode, in dev mode the cache is disabled. +# http.cacheControl=3600 + +# If enabled, Play will generate entity tags automatically and send a 304 when needed. +# Default is true, set it to false to deactivate use of entity tags. +# http.useETag=true + +# Custom mime types +# mimetype.xpi=application/x-xpinstall + +# WS configuration +# ~~~~~ +# Default engine is Async Http Client, uncomment to use +# the JDK's internal implementation +# webservice = urlfetch +# If you need to set proxy params for WS requests +# http.proxyHost = localhost +# http.proxyPort = 3128 +# http.proxyUser = jojo +# http.proxyPassword = jojo + +# Mail configuration +# ~~~~~ +# Default is to use a mock Mailer +mail.smtp=mock + +# Or, specify mail host configuration +# mail.smtp.host=127.0.0.1 +# mail.smtp.user=admin +# mail.smtp.pass= +# mail.smtp.channel=ssl + +# Url-resolving in Jobs +# ~~~~~~ +# When rendering templates with reverse-url-resoling (@@{..}) in Jobs (which do not have an inbound Http.Request), +# ie if sending a HtmlMail, Play need to know which url your users use when accessing your app. +# %test.application.baseUrl=http://localhost:9000/ +# %prod.application.baseUrl=http://www.yourdomain.com/ + +# Templates additional precompilation +# ~~~~~~ +# yaml files can use template expressions and are located in the conf folder. +# The default precompile task will not pickup those files. +# You can manually add some files with the following property +# (default: system property play.templates.compile, fallback system environment PLAY_TEMPLATES_COMPILE) +# play.templates.compile=conf/initialdata.yml;{module:reporting}test/reporting/unittests/export.yml +# +# Override the path separator if you want to use the Play!-module notation like {module:modname} on unix. +# (default: system property path.separator) +# play.templates.compile.path.separator=; + +# Jobs executor +# ~~~~~~ +# Size of the Jobs pool +# play.jobs.pool=10 + +# Execution pool +# ~~~~~ +# Default to 1 thread in DEV mode or (nb processors + 1) threads in PROD mode. +# Try to keep a low as possible. 1 thread will serialize all requests (very useful for debugging purpose) +# play.pool=3 + +# Netty pipeline configuration (advanced settings) +# You can default netty settings by overriding the following line. Each handler must be comma separated. +# The last value must be the PlayHandler class (or your own that extends PlayHandler) +# Default values are +# play.netty.pipeline = play.server.FlashPolicyHandler,org.jboss.netty.handler.codec.http.HttpRequestDecoder,play.server.StreamChunkAggregator,org.jboss.netty.handler.codec.http.HttpResponseEncoder,org.jboss.netty.handler.stream.ChunkedWriteHandler,play.server.PlayHandler +# For example, to enable Netty response compression +# play.netty.pipeline = play.server.FlashPolicyHandler,org.jboss.netty.handler.codec.http.HttpRequestDecoder,play.server.StreamChunkAggregator,org.jboss.netty.handler.codec.http.HttpResponseEncoder,org.jboss.netty.handler.codec.http.HttpContentCompressor,org.jboss.netty.handler.stream.ChunkedWriteHandler,play.server.PlayHandler +# For SSL, use the play.ssl.netty.pipeline property +# play.ssl.netty.pipeline = play.server.FlashPolicyHandler,org.jboss.netty.handler.codec.http.HttpRequestDecoder,play.server.StreamChunkAggregator,org.jboss.netty.handler.codec.http.HttpResponseEncoder,org.jboss.netty.handler.codec.http.HttpContentCompressor,org.jboss.netty.handler.stream.ChunkedWriteHandler,play.server.ssl.SslPlayHandler + + +# Open file from errors pages +# ~~~~~ +# If your text editor supports opening files by URL, Play! will +# dynamically link error pages to files +# +# Example, for textmate: +# play.editor=txmt://open?url=file://%s&line=%s + +# Testing. Set up a custom configuration for test mode +# ~~~~~ +#%test.module.cobertura=${play.path}/modules/cobertura +%test.application.mode=dev +%test.db.url=jdbc:h2:mem:play;MODE=MYSQL;LOCK_MODE=0 +%test.jpa.ddl=create +%test.mail.smtp=mock + diff --git a/samples-and-tests/nonstatic-app/conf/dependencies.yml b/samples-and-tests/nonstatic-app/conf/dependencies.yml new file mode 100644 index 0000000000..85373765a1 --- /dev/null +++ b/samples-and-tests/nonstatic-app/conf/dependencies.yml @@ -0,0 +1,4 @@ +# Application dependencies + +require: + - play diff --git a/samples-and-tests/nonstatic-app/conf/messages b/samples-and-tests/nonstatic-app/conf/messages new file mode 100644 index 0000000000..b51f29459d --- /dev/null +++ b/samples-and-tests/nonstatic-app/conf/messages @@ -0,0 +1,3 @@ +# You can specialize this file for each language. +# For example, for French create a messages.fr file +# diff --git a/samples-and-tests/nonstatic-app/conf/routes b/samples-and-tests/nonstatic-app/conf/routes new file mode 100644 index 0000000000..fcd1dbf193 --- /dev/null +++ b/samples-and-tests/nonstatic-app/conf/routes @@ -0,0 +1,13 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +# Home page +GET / Application.index +GET /hello Application.hello + +# Ignore favicon requests +GET /favicon.ico 404 + +# Map static resources from the /app/public folder to the /public path +GET /public/ staticDir:public diff --git a/samples-and-tests/nonstatic-app/test/ApplicationTest.java b/samples-and-tests/nonstatic-app/test/ApplicationTest.java new file mode 100644 index 0000000000..55da40b5ed --- /dev/null +++ b/samples-and-tests/nonstatic-app/test/ApplicationTest.java @@ -0,0 +1,24 @@ +import org.junit.Test; +import play.mvc.Http.Response; +import play.test.FunctionalTest; + +public class ApplicationTest extends FunctionalTest { + + @Test + public void indexPage() { + Response response = GET("/"); + assertIsOk(response); + assertContentType("text/html", response); + assertCharset(play.Play.defaultWebEncoding, response); + assertContentMatch("Welcome to the non-static world!", response); + } + + @Test + public void helloPage() { + Response response = GET("/hello"); + assertIsOk(response); + assertContentType("text/plain", response); + assertCharset(play.Play.defaultWebEncoding, response); + assertContentMatch("Hello world!", response); + } +} \ No newline at end of file