What’s the preferred way to handle 404 errors with Play 2.0 and show a nice templated view?
You can override the onHandlerNotFound method on your Global object, e.g.:
object Global extends GlobalSettings {
override def onHandlerNotFound(request: RequestHeader): Result = {
NotFound(views.html.notFound(request))
}
}
Please note that there are really two different problems to solve:
Showing a custom 404 page when there is "no handler found", e.g. when the user goes to an invalid URL, and
Showing a custom 404 (NotFound) page as a valid outcome of an existing handler.
I think the OP was referring to #2 but answers referred to #1.
"No Handler Found" Scenario
In the first scenario, for "no handler found" (i.e. invalid URL), the other answers have it right but to be more detailed, per the Play 2.1 documentation as:
Step 1: add a custom Global object:
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
object Global extends GlobalSettings {
override def onHandlerNotFound(request: RequestHeader): Result = {
NotFound(
views.html.notFoundPage(request.path)
)
}
}
Step 2: add the template. Here's mine:
#(path: String)
<html>
<body>
<h1>Uh-oh. That wasn't found.</h1>
<p>#path</p>
</body>
</html>
Step 3: tweak your conf/application.conf to refer to your new "Global". I put it in the controllers package but it doesn't have to be:
...
application.global=controllers.Global
Step 4: restart and go to an invalid URL.
"Real Handler can't find object" Scenario
In the second scenario an existing handler wants to show a custom 404. For example, the user asked for object "1234" but no such object exists. The good news is that doing this is deceptively easy:
Instead of Ok(), surround your response with NotFound()
For example:
object FruitController extends Controller {
def showFruit(uuidString: String) = Action {
Fruits.find(uuidString) match {
case Some(fruit) => Ok(views.html.showFruit(fruit))
// NOTE THE USE OF "NotFound" BELOW!
case None => NotFound(views.html.noSuchFruit(s"No such fruit: $uuidString"))
}
}
}
What I like about this is the clean separation of the status code (200 vs 404) from the HTML returned (showFruit vs noSuchFruit).
HTH
Andrew
If you want to do the same using Java instead of Scala you can do it in this way (this works for play framework 2.0.3):
Global.java:
import play.GlobalSettings;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Http.RequestHeader;
public class Global extends GlobalSettings {
#Override
public Result onHandlerNotFound(RequestHeader request) {
return Results.notFound(views.html.error404.render());
}
}
Asumming that your 404 error template is views.html.error404 (i.e. views/error404.scala.html).
Please note that Play development team are making lots of efforts to move away from global state in Play, and hence GlobalSettings and the application Global object have been deprecated since version 2.4.
HttpErrorHandler.onClientError should be used instead of
GlobalSettings.onHandlerNotFound. Basically create a class that inherits from HttpErrorHandler, and provide an implementation for onClientError method.
In order to find out type of error (404 in your case) you need to read status code, which is passed as a one of the method arguments e.g.
if(statusCode == play.mvc.Http.Status.NOT_FOUND) {
// your code to handle 'page not found' situation
// e.g. return custom implementation of 404 page
}
In order to let Play know what handler to use, you can place your error handler in the root package or configure it in application.conf using play.http.errorHandler configuration key e.g.
play.http.errorHandler = "my.library.MyErrorHandler"
You can find more details on handling errors here: for Scala or Java.
This works in 2.2.1. In Global.java:
public Promise<SimpleResult> onHandlerNotFound(RequestHeader request) {
return Promise.<SimpleResult>pure(notFound(
views.html.throw404.render()
));
}
Ensure that you have a view called /views/throw404.scala.html
This works in 2.2.3 Play - Java
public Promise<SimpleResult> onHandlerNotFound(RequestHeader request) {
return Promise<SimpleResult>pure(Results.notFound(views.html.notFound404.render()));
}
html should be within /views/notFound404.scala.html
Dont forget to add Results.notFounf() and import play.mvc.Results;
For Java, if you want to just redirect to main page, I solved it by this.
#Override
public Promise<Result> onHandlerNotFound(RequestHeader request) {
return Promise.pure(redirect("/"));
}
Related
Scenario: (AEM 6.3.2) I'm requesting a page with the selector "test1", like this:
http://localhost:4502/content/myapp/home.test1.html
This page have a parsys where I have drop a component "slider", so the component's path is: "/content/myapp/home/jcr:content/parsys/slider"
At the "slider" component level, how can I access to the "test1" selector?
I've tried different ways (SlingModel, WCMUsePojo, the "request" HTL Global Object...), but always get the same problem: the "request" I can access is the GET request of the component (GET "/content/myapp/home/jcr:content/parsys/slider.html") where the selector is not present.
You should use the method SlingHttpServletRequest##getPathInfo inherited from HttpServletRequest
In your example, if you make a request to:
http://localhost:4502/content/myapp/home.test1.html
Then in your component's Class (Use/SlingModel) you can call request.getPathInfo() which will return: /content/myapp/home.test1.html
Then you can parse that path using: com.day.cq.commons.PathInfo
Here is an example sling model:
package com.mycom.core.models;
import com.day.cq.commons.PathInfo;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
#Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SampleModel {
#Self
SlingHttpServletRequest request;
public PathInfo getPathInfo() {
return new PathInfo(request.getPathInfo());
}
}
then in your HTML you can do:
<sly data-sly-use.sample="com.mycom.core.models.SampleModel"/>
<div>${sample.pathInfo.selectors # join=', '}</div>
An that will output: (based on your example path)
<div>test1</div>
Just checked the exact same component/code on another AEM instance (same version) and it's working... will check what can be causing the wrong behavior, but I guess the problem is solved!
Is there a way to reconfigure the Grails 3 Link Generator to create Restful links, i.e. localhost:8080/book/{id} rather than the old style that includes the action in the URL, localhost:8080/book/show/{id}?
I'd like to have restful URLs in the location headers of the responses to save actions.
I've been using this Grails Restful Link Generator as a workaround. I'm not perfectly happy with it, but it's the best I've been able to come up with thus far.
1. Create a trait in src/main/groovy that removes the superfluous action from the URL
import grails.web.mapping.LinkGenerator
trait RestfulLinkGeneratorTrait {
LinkGenerator grailsLinkGenerator
String generateLink(Map map) {
map.controller = map.controller ?: this.controllerName
map.absolute = map.absolute ?: true
map.action = map.action ?: "show"
grailsLinkGenerator.link(map).replace("/$map.action", "")
}
}
2. Implement the RestfulLinkGenerator on your controller(s) and call generateLink(id: obj.id) to generate links.
#Secured('ROLE_USER')
class BookController extends RestfulController implements RestfulLinkGeneratorTrait {
//... other methods ...//
#Transactional
def save() {
// ... save you resource ... //
response.addHeader(HttpHeaders.LOCATION, generateLink(id: book.id))
respond book, [status: CREATED, view: 'show']
}
//... other methods ...//
}
The following is the code generated by SWTBot Recorder.
public class UserInterfaceTester extends SWTBotEclipseTestCase {
#Test
public void TableTest() {
bot.tree().getTreeItem("wtrt").select();
bot.contextMenu("Expand All").click();
bot.tree().getTreeItem("wtrt").getNode("erwtesg(3)").getNode("esrgg").select();
bot.contextMenu("Open Application Metadata File").click();
bot.text().setText("9.5");
bot.text().setText("Synopsys");
bot.text().setText("3.2");
}
}
But when I try to put that in my Test case to run inside my project it shows error in bot.contextMenu. It says "The method contextMenu(String) is undefined for the type SWTEclipseBot".
Extending SWTBotEclipseTestCase automatically gives me bot object which is
protected SWTEclipseBot bot = new SWTEclipseBot();
But it says it is a deprecated version. It says "Deprecated. use SWTWorkbenchBot. This will be removed from future releases"
Hence I tried
SWTWorkbenchBot bot = new SWTWorkbenchBot(); // by removing extends SWTBotEclipseTestCase
that to did not work. What is the issue? Can some one help?
The ContextMenuHelper class should help with this, and it works round some bugs with dynamic context menus. try:
SWTBotMenu menu =
new SWTBotMenu(ContextMenuHelper.contextMenu(bot.tree(), "Expand All"));
menu.click();
I have the following template code:
views/Login.scala.html:
#(loginForm: Form[views.Data])
#import mytemplates.loginform
#Main("") {
email:#loginform(loginForm("email"))
#*email:#loginForm("email").value.getOrElse("xyz")*#
}
views/mytemplates/loginform.scala.html:
#(emailField: Field)
#emailField.value.getOrElse("xyz")
views/Main.scala.html:
#(page: String)(content: Html)
<!DOCTYPE html>
<html>
<body>
#content
</html>
views/Data.java:
package views;
import play.data.validation.ValidationError;
import java.util.List;
public class Data {
public String email = "";
public Data() { }
public List<ValidationError> validate() {
return null;
}
}
Compiling the above is successful. But if line #*email:#loginForm("email").value.getOrElse("xyz")*# in Login.scala.html is uncommented compiling produces an value getOrElse is not a member of String error.
Why does this happen? I'd like to exclude the template mytemplates.loginform but can't get it to work.
edit: Following estmatic's advice I get the following:
views/Login.scala.html:
#loginForm("email").getClass: class play.data.Form$Field
#loginForm("email").valueOr("").getClass: class java.lang.String
views/mytemplates/loginform.scala.html:
#emailField.getClass: class play.core.j.PlayMagicForJava$$anon$1
#emailField.value.getClass: class scala.None$
I had to use valueOr("") in Login.scala.html otherwise a NullPointer execution exception would be produced. Clearly they are all different classes. I haven't used Play framework much and am not sure what this means.
Since it looks like you have a Java project, the framework is going to do some automatic conversions here and there between the Java classes and their Scala equivalent.
Try this out:
#loginForm("email").getClass()
#loginForm("email").value.getClass()
Make this change on both Login.scala.html and loginform.scala.html and you'll see that you are dealing with different classes in each scenario.
When you go through the loginform template your field.value will be wrapped in a scala.Some object, which is why .getOrElse compiles in that case. When you do it directly in the main view you never leave Java-class-world, so your field.value is returned directly as a String.
If you are using the latest version of Play then you should be able to use the Field.valueOr method instead of getOrElse.
#loginForm("email").valueOr("xyz")
I've declared an object which gets instantiated on application start. I want to access it inside a controller, which is part of a plugin. I want to be able to use that plugin, but I can't seem to get past the first part -- finding the MyWebsocketConnection object. None of the examples show how to do this. I don't want to inject into the controller because I'm writing a plugin (I saw static examples of how to do that somewhere).
Global.scala, plugin application \app\Global.scala
object Global extends GlobalSettings {
object MyWebsocketConnection {
val logger = // return something that gets instantiated once, like websocket object for logging to ui
}
class MyWebsocketConnection {
import MyWebsocketConnection.logger
}
override def onStart(app: Application) {
Logger.info("Application has started...");
}
}
My custom logging plugin controller:
MyLogger.Scala, plugin application \app\controllers\MyLogger.scala
object MyLogger {
def info(message: String) = {
// THIS CAN'T BE FOUND ?
// MyWebsocketConnection.logger.send(message)
}
}
So, from the Play! 2.0 app that references the plugin, I would (probably) do something like below, but I can't even get past the part before this:
MyFutureController.scala, another Play! application \app\controllers\MyFutureController.scala
object MyFutureController extends Controller {
def someRandomMethod = Action {
// Custom logging
MyLogger.info("Here's my log message!");
Ok("This documentation stinks!")
}
}
There is also workaround #3: move your Global class to a package and specify its fully qualified name in application.conf file, like so:
global= my.packaged.Global
The problem is that your Global objects resides in default package. And in Java, classes from default package can't be referenced from other packages, they are accessible only within the same package (default).
I see two workarounds of this problem.
Move MyWebsocketConnection to some named package (say config) so it can be accessible in your application.
object MyLogger {
def info(message: String) = {
config.MyWebsocketConnection.logger.send(message)
}
}
Move your whole application into single package (but it is a lot of pain)
foo
|--controllers
|--models
|--views
|--Global.scala
Then Global object will resides in foo package and will be accessible within application.