#{extends '../main.html'/}# #{set title:'Controllers'/}# #{set tab:'management'/}# #{renderTagArgs 'docHome.html'/}#
You will notice very quickly if you have not already that webpieces forces all your Controllers to have the @Singleton annotation. We believe in a stateless server meaning only one instance of the Controller is necessary plus it saves on performance so we don't have to construct it every time like J2EE sometimes does which is a pure waste of resources in a ton of cases where you are stateless anyways.
Every Controller method returns some sort of Action where the type heirarchy looks like so
This means your Controller must return one of these types. If you are a post method, then you must always return a Redirect and webpieces will complain to you if you don't. All Actions are created via the Actions.java class. A typical GET method in a controller looks like so:
*[public Render easyUpgrade() {
return Actions.renderThis("menu", menuCreator.getMenu());
}]*
Notice that I could change the return type to Action if the GET method does a Redirect or a Render like so:
*[public Action easyUpgrade() {
if(something)
return Actions.redirect(MyRouteId.LOGIN);
return Actions.renderThis("menu", menuCreator.getMenu());
}]*
A POST method which is forced into returning a redirect to be compliant with PRG may look like so:
*[public Redirect postDeleteUser(int id) {
UserDbo ref = Em.get().find(UserDbo.class, id);
Em.get().remove(ref);
Em.get().flush();
Current.flash().setMessage("User deleted");
Current.flash().keep();
return Actions.redirect(CrudUserRouteId.LIST_USERS);
}]*
Now, AjaxRedirect created by Actions.ajaxRedirect is a special case. Because you can't return http code 303 Redirect to javascript components as they will then redirect internally and load that html, they need to be told the server wants you to redirect to some page. This is where AjaxRedirect comes into play. He is translated to a http 287 code so your javascript can check for this code and redirect on the javascript side.
AjaxRedirect is wired into some common components already such that it will redirect when necessary
In typical api development, you typically want to KISS and send errors back on the first issue you find with a request. In GUIs however, you want to give a phenomenal user experience and give as many correctable errors as you can in the first go. For this reason there is a validation like so:
*[if(password == null) {
Current.validation().addError("password", "password is required");
}
if(entity.getFirstName() == null) {
Current.validation().addError("entity.firstName", "First name is required");
}]*
Finally, once you have an added an error to each field, you can check if there are any errors to display a global message or global error:
*[if(Current.validation().hasErrors()) {
Current.flash().setMessage("You have errors below");
Current.flash().setError("You have errors below");
}]*
But 'wait' you say, why in the world is there 2 ways of setting a message or error. Good question! Typically, you will have something like this in your template that is at the top of every page:
*[#{if _flash.message}#
${_flash.message}$
#{/if}#]*
This reads from the message and so if you are doing basic boring straight up html, you can use the message inside the flash scope for any previous page to display a 1 time message like 'User Successfully Saved' or 'You have errors in your form below'.
If however you are doing AJAX add/edit popup, the message is on the main page so instead in your ajax add/edit popup, you may have code like this to read the global error:
*[#{if _flash.error}#
Oops....
${_flash.error}$
#{/if}#]*
Now, let's look at adding an error to an entity. In this next example, we are adding an error to 'entity.firstName':
*[Current.validation().addError("entity.firstName", "First name is required");]*
Now, in our page, we can access the error like so:
*[_ctx.validation.getError('entity.firstName')]*
Instead, however, we have created the field tag which is used like so:
*[#{field 'entity.firstName', label:'First Name'}##{/field}#]*
and thankfully FieldTag.java sets up a Map variable called 'field' so that the actual field.tag file can use a variable *[${field.error}$]*:
*[${field.error}$]*
As a final note, the validation bean is a flash scoped cookie held by the browser. When you set something on the validation bean, a cookie is sent to your browser with the redirect(remember we force posts to send a redirect) and then the browser sends a GET with your validation cookie, we render the page and then unless you call validation.keep(), we clear out the validation bean. This is done after the html is rendered though so your html has time to use the error messages in the page.
You can access the session like so
*[Current.session().put("key", "something");
Current.session().remove("key");
Current.session().get("key");
]*
Whatever you put in the session stays there until you remove it. The session is secured with a message digest hash that is compared every time webpieces receives a request. If it is invalid, we reject and clear their cookie completely assuming it is either a hacker or a bug.
To access the session from the html, you can do something like this:
*[#{if _session.get('userId')}#
Logged In User: ${_session.get('userId')}$
#{/if}#]*
However, let's say you are booking plane tickets in two different tabs. Session is definitely not where you want to be saving the user experience. For that situation, you would want to save it into some sort of tab state. Of course, for some reason, the larger community has not created a spec for a tab cookie that only goes back and forth for one single tab in a browser :( so tab state has to be simulated within the url query params.
Flash can be accessed like so in a java controller:
*[Current.flash().put("someKey", "someValue");
Current.flash().setMessage("User successfully saved");
Current.flash().keep();]*
setMessage is a special method that just does the same thing as .put("_message", "User Successfully saved"); so another words the _message key is reserved for him. keep() must be called or none of the state will be saved. Also, if you receive a flash data from a previous post and add more data and call keep() yet again, all the previous data from the previous flash will also be saved over yet again.
In html, you can also access the flash with _flash variable like so:
*[#{if _flash.message}#
${_flash.message}$
#{/if}#]*
Or access a specific key like so:
*[${_flash.get('flashData')}$]*
Tab Scope exists so when you have a user that can book tickets in multiple tabs, session scope would be horrible and would conflict. Instead Tab Scope exists for ease of developing these types of applications
To enable tab scope or from your controller, using something like Current.tabState().get("key") or from your html, using something like *[${_tab.get('key')}$]*, you need to define when a conversation name and when a conversation starts and ends
When a conversation starts, we will tack on a conversation name and a tab id(tid) onto the end of every url like http://somedomain.com/some/account?tid=conversation1-234. We will also write out a cookie with the conversationName-tid so that every time we receive a request with a tid, we can lookup and setup the Current.tabState() for you such that you can use it
These cookies can be left dangling and add up so to prevent that, when creating a conversation name and when it starts and ends, you also define a timeout for that conversation and where we will redirect the user when that specific conversationName times out which can allow them to start over on the conversation
Up until now, Current.flash(), Current.session(), Current.validation() have all been shortcuts because all of those same instances are included in the RequestContext. You can access the RequestContext like so:
*[RequestContext ctx = Current.getContext()]*
Then, the RequestContext contains all context surrounding the request. It contains the Session which spans all requests for that session. It contains Flash which spans a few requests(the POST/GET cycle), and the RouterRequest itself which is just for this request and is independent of http protocol. You will notice that the webpieces router has no compile-time dependencies on http2 nor http1 objects actually and therefore is a very independent webpiece.
The RouterRequest can be accessed like so:
*[Current.request();]*
or like so:
*[Current.getContext().getRequest();]*
We heavily documented the RouterRequest so please check out the comments and you can also look who uses each variable as we tried to make things really easy to trace so we tried not to over encapsulate which is a frequent error of developers killing off trace-ability
Our router depends on a core http-shared which has no http and only has RouterRequest. Our templating engine depends on that same http-shared as it needed access to certain things. This kept our dependencies light so http-router can easily be used to create a different webserver with a different templating engine. Or http-templating can be used as a templating engine to be used with a completely different router.
#{renderTagArgs 'plugins/useQuery.html'/}#Well, I have been using scala Futures for sometime now(3+ years). Man, we really fucked up developer productivity. Sure, we made it so a server doesn't have to hold up a thread which seemed pretty cool for my first 2 years of using it. Having owned a business previously though and watching the cost sky rocket(and developer costs were way more than computers before anyways), I have come full circle and almost always prefer the synchronous environment. I mean, I see bugs that used to take 5 minutes taking hours now. 3 hours/5 minutes = 36x the cost right there. I am dead serious as the stack traces screamed the location of the issue immediately and you could jump in and fix in literally 5 minutes. In today's world, this has caused much slower times
Ok, OFF MY RANT. Webpieces still supports asynchronous development so you can kill yourself if you like. Just create a method like so and webpieces will know it is asynchronous:
*[public CompletableFuture[Action] postCar(CarDbo car)]*
or if you know the specific return value, you can make it more specific
*[public CompletableFuture[Render] renderCarPage()]*
Now, there is some tricks to keep this working regarding the RequestContext. Java futures are not as good as the twitter scala ones that can thread some global state per request down all the thenApply and thenCompose methods(which is very annoying as I can't get the MDC of logback working for async methods in java like I can in scala). Hit me up directly if you want more info on this and I can vent ;). Soooo, if you are going to use thenApply and thenCompose on the future, you must manually call Current.getContext BEFORE those methods and pass in the context to the next function so that you can access the session, flash, validation, etc.
Ok, last but not least is overriding http headers. We allow the registration of callbacks so if any plugins, or filters muck with headers, the last callback added(generally the controller) wins. We are hoping filters add their callbacks on the request going down path and not the response coming back path. In this way, the Controller can have last say to override headers when it really needs to. Here is an example adding some headers from a controller method
*[public Action postSomething() {
Current.getContext().addModifyResponse(resp -> addCacheHeaders(resp));
}
private Object addCacheHeaders(Object response) {
Http2Headers resp = (Http2Headers) response;
//http://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers
//This forces the browser back button to re-request the page as it would never have the page
//and is good to use to hide banking information type pages
//resp.addHeader(new Header(KnownHeaderName.CACHE_CONTROL, "no-store"));
resp.addHeader(new Http2Header(Http2HeaderName.CACHE_CONTROL, "no-cache, no-store, must-revalidate"));
resp.addHeader(new Http2Header(Http2HeaderName.PRAGMA, "no-cache"));
resp.addHeader(new Http2Header(Http2HeaderName.EXPIRES, "0"));
return resp;
}]*
This is of course for webpieces webserver only. If another webserver uses the Router, they may stuff a netty http request in or whatever they like and you would need to know about that implementation. Perhaps, in the future, we can have you set header names and values independent of which library is used so the platform can translate for you instead of you depending on the http parser like this example. For now though, this works and I decided KISS here in that at least the router jar doesn't depend on any http parser.