#{extends '../main.html'/}# #{set title:'Webpieces QuickStart'/}# #{set tab:'management'/}# #{renderTagArgs 'docHome.html'/}# #{renderTagArgs 'quickStartList.html'/}#
Last updated: Sept 14th, 2020
It's always best to start with basic html and keep things simple to start with when learning so this is the pure html, no javascript version of list all my Accounts, create an account, edit an account, and delete an account. The basic CRUD flow is so common, it deserves it's own example. If you want the advanced AJAX version, skip to the next page
First, create a new package called crudexamples in your org.webpieces.helloworld/web directory.
Next, let's create a new RouteId file called ExampleRouteId like so:
package org.webpieces.helloworld.web.crudexamples;
import org.webpieces.router.api.routes.RouteId;
public enum ExampleRouteId implements RouteId {
//list
LIST_ACCOUNTS,
//add/edit
GET_ADD_ACCOUNT_FORM, GET_EDIT_ACCOUNT_FORM, POST_ACCOUNT_FORM,
//delete
CONFIRM_DELETE_ACCOUNT, POST_DELETE_ACCOUNT
}
Next, let's create a RouteModule class called "ExampleRoutes." In here, there is a special method for a CRUD operation like so:
package org.webpieces.helloworld.web.crudexamples;
import org.webpieces.ctx.api.HttpMethod;
import org.webpieces.router.api.routebldr.DomainRouteBuilder;
import org.webpieces.router.api.routebldr.RouteBuilder;
import org.webpieces.router.api.routes.Ports;
import org.webpieces.router.api.routes.Routes;
import org.webpieces.router.api.routes.CrudRouteIds;
public class ExampleRoutes implements Routes {
@Override
protected void configure(DomainRouteBuilder bldr) {
RouteBuilder b = bldr.getAllDomainsRouteBuilder();
CrudRouteIds routeIds = new CrudRouteIds(
ExampleRouteId.LIST_ACCOUNTS, ExampleRouteId.GET_ADD_ACCOUNT_FORM,
ExampleRouteId.GET_EDIT_ACCOUNT_FORM, ExampleRouteId.POST_ACCOUNT_FORM,
ExampleRouteId.CONFIRM_DELETE_ACCOUNT, ExampleRouteId.POST_DELETE_ACCOUNT);
b.addCrud(Port.BOTH, "account", "ExampleAccountController", routeIds);
}
}
The "account" we pass in becomes part of the 'standard url pattern' we happen to use in this addCrud method but feel free to create your own 'addCrud' method that your whole app can use to stay consistent. This method adds all these routes for you but more importantly keeps your urls consistent for CRUD operations:
Notice, that it names the url /account/*** and the methods accountList and accountAddEdit and all post methods then start with postXXXX keeping consistency so every GET request invokes normal methods but POST methods always start with postXXX keeping your app consistent. Next, let's create the AccountDbo.java entity bean like so:
As with before, let's add this RouteModule file to ProdServerMeta.java (org.webpieces.helloworld.base) to make those routes accessible:
*[@Override
public List getRouteModules() {
return Lists.newArrayList(
new LoginRoutes("/org/webpieces/helloworld/web/login/AppLoginController", "/secure/.*", "password"),
new CrudRoutes(),
new AjaxCrudRoutes(),
new JsonRoutes(),
new MyMainRoutes(),
new ExampleRoutes()
);
}]*
Next we create the database object for our accounts inside the db package (recall that Hibernate only scans a portion of your code for database objects!):
*[package org.webpieces.helloworld.db;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Query;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name="ACCOUNT")
@NamedQueries({
@NamedQuery(name = "findAllAccounts", query = "select u from AccountDbo u"),
})
public class AccountDbo {
@Id
@SequenceGenerator(name="account_id_gen",sequenceName="account_sequence" ,initialValue=1,allocationSize=10)
@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="account_id_gen")
private Integer id;
@Column(unique = true)
private String name;
private String contactsName;
private int priority;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContactsName() {
return contactsName;
}
public void setContactsName(String contactsName) {
this.contactsName = contactsName;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
@SuppressWarnings("unchecked")
public static List findAll(EntityManager mgr) {
Query query = mgr.createNamedQuery("findAllAccounts");
return query.getResultList();
}
}]*
Then, we can now create ExampleAccountController.java with our first method accountList like so:
*[package org.webpieces.helloworld.web.crudexamples;
import java.util.List;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import org.webpieces.plugins.hibernate.Em;
import org.webpieces.router.api.controller.actions.Action;
import org.webpieces.router.api.controller.actions.Actions;
@Singleton
public class ExampleAccountController {
public Action accountList() {
EntityManager mgr = Em.get();
List accounts = AccountDbo.findAll(mgr);
return Actions.renderThis("accounts", accounts);
}
}]*
Then, of course, we need an html page called accountList.html since renderThis is being called:
*[#{extends '../mainTemplate.html'/}#
#{set title:'Awesome CRUD'/}#
#{set tab:'none'/}#
Accounts
Account Name
Contacts Name
Priority
#{list items:accounts, as:'entity'}#
${entity.name}$
${entity.contactsName}$
${entity.priority}$
&{'Edit', 'link.edit'}&
&{'Delete', 'link.delete'}&
#{/list}#
#{else}#
There are no accounts, Add one now please.
#{/else}#
Add Account
]*
Finally, boot up your DevelopmentServer.java main class (if it's not already running!) and open a browser and go to http://localhost:8080/account/list or go to https://localhost:8443/account/list
We can now go over this template piece by piece:
Now, go to http://localhost:8080/account/list to view your work of art. You will notice you have no accounts so next it might be a good idea to create the controller method and the HTML form for creating some accounts. Let's start with our ExampleAccountController:
*[public Action accountAddEdit(Integer id) {
if(id == null) {
return Actions.renderThis("entity", new AccountDbo());
}
AccountDbo account = Em.get().find(AccountDbo.class, id);
if(account == null)
throw new NotFoundException("Account is not found");
return Actions.renderThis("entity", account);
}]*
This method accepts the id (or you can use some other unique identifier) and if it is null, then it assumes a create and supplies back an empty Account to render the html page with. If however, there is an id, we look up the account and if it is not found, we throw NotFound which will in turn invoke our NotFound route to return a 404 and a nice looking page with the 404 to the browser. If the account is found, we render the same create form with all the fields filled in with the account data. The html for this create or edit page is as follows:
*[#{extends '../mainTemplate.html'/}#
#{set title:'Add/Edit'/}#
#{set tab:'none'/}#
#{form action:@[POST_ACCOUNT_FORM]@, class:'form-horizontal', style:'min-width:500px;max-width:800px;margin: 0 auto'}#
Account
#{field 'entity.name', label:'Account Name'}##{/field}#
#{field 'entity.contactsName', label:"Contact''s Name"}##{/field}#
#{field 'entity.priority', label:'Priority'}##{/field}#
#{/form}#]*
The first 3 lines are the same as the accountList.html we wrote about above(hopefully you were reading and not spaced out). Then, we use the form tag along with the POST_ACCOUNT_FORM url lookup. Next, we have *[]* which is the special html for saving if this is an edit or a creation. If we are editing a value, entity.id on posting will be filled in and if we are creating an account, entity.id will be a 0 length string.
Finally, we reach the real meat, and yes, I spent shit loads of time perfecting this tag to work with arrays, and all sorts of things that you may run into saving your ass. You can thank me later(jk, but monetary donations accepted). The field tag works with *[ ]* or all sorts of input type html. It accepts the entity.name which is the groovy text identifying the bean's get/set methods to call in this case AccountDbo.getName and AccountDbo.setName. The label is what shown in the gui unless i18n overrides it using 'entity.name' as the key for you into the i18n properties.
#{renderTagArgs 'fieldTag.html'/}#Next is the bottom of the page with the standard input element button for submitting in html. That is just standard html so nothing special there other than the i18n *[&{'Save', 'link.save'}&]* tokens to do translations. Lastly, there is a simple cancel button which literally tells the browser to do a GET request on the LIST_ACCOUNTS url. Cancel is simply asking to load a different page.
Now, you can click the Add Account button and the page for creating an account will be shown. Of course, we have not created the post method yet so you cannot save anything just yet. So, next let's create our post method in our controller like so
*[public Redirect postSaveAccount(AccountDbo entity) {
if(entity.getName() == null) {
Current.validation().addError("entity.name", "password is required");
} else if(entity.getName().length() < 3) {
Current.validation().addError("entity.name", "Value is too short");
}
if(entity.getContactsName() == null) {
Current.validation().addError("entity.contactsName", "First name is required");
} else if(entity.getContactsName().length() < 3) {
Current.validation().addError("entity.contactsName", "First name must be more than 2 characters");
}
//all errors are grouped and now if there are errors redirect AND fill in
//the form with what the user typed in along with errors
if(Current.validation().hasErrors()) {
FlashAndRedirect redirect = new FlashAndRedirect(Current.getContext(), "Errors in form below");
redirect.setIdFieldAndValue("id", entity.getId());
return Actions.redirectFlashAll(AccountRouteId.GET_ADD_ACCOUNT_FORM, AccountRouteId.GET_EDIT_ACCOUNT_FORM, redirect);
}
Current.flash().setMessage("Account successfully saved");
Current.flash().keep(true);
Current.validation().keep(true);
Em.get().merge(entity);
Em.get().flush();
return Actions.redirect(AccountRouteId.LIST_ACCOUNTS);
}]*
The first steps on a post method are to do your validation. Each time, you add an error like Current.validation().addError("entity.name", "my error") using the field tag name 'entity.name' from the html. As it goes through validation, it accumulates all the user errors it can. Then, you check if there were any using Current.validation.hasErrors(). At this point, you need to store everything in flash scope to follow the PRG pattern. Basically, on the GET request all the information is available from flash scope and blown away once we return a response to the GET request. In this case, we need to redirect to the add account route or the get account route since this post call could be from either editing or creating an account.
As noted in your first hibernate entity(previous tutorial), calling Em.get().flush() is critical as no persist/delete/merge(update) will modify the database. This is a special, VERY nice feature of hibernate such that you can modify tons of entities and delay the decision of persisting them all or not until later
If there were no errors in validation however, we set a global message that is rendered in mainTemplate.html so no matter what page we land on, as long as that page extends mainTemplate.html "Account successfully saved" will be rendered again using flash scope Current.flash().setMessage("Account successfully saved"); Flash scope is not always saved, so we need to tell webpieces to keep the flash scope for after the redirect by calling Current.flash().keep(true); Finally, we redirect back to the list accounts page where your new account should show up.
Now for a little awesome demonstration of the kick-ass power of PRG:
Notice that you are NEVER getting this image. We don't allow you to break your customer experience on webpieces:
Next, go to http://localhost:8080/account/list and click edit on your Account and you will notice that you can already just edit your account and save it just fine.
Now, the only one thing we have left is the delete which we don't have a page for yet. Let's create the confirmDeleteAccount() method that will render our html page
*[public Render confirmDeleteAccount(int id) {
AccountDbo account = Em.get().find(AccountDbo.class, id);
return Actions.renderThis("entity", account);
}]*
Pretty simple in that we just lookup the entity and pass it to the html template which is also simple:
*[#{extends '../mainTemplate.html'/}#
#{set title:'Confirm Delete'/}#
#{set tab:'none'/}#
#{form action:@[POST_DELETE_ACCOUNT, id:entity.id]@, class:'form-horizontal', style:'min-width:500px;max-width:800px;margin: 0 auto'}#
Delete Account?
Are you sure you want to delete Account ${entity.name}$?
#{/form}#]*
This looks much like the POST for add/edit account except one minor difference in that we use the reverse route into url tokens with the entities id *[@[POST_DELETE_ACCOUNT, id:entity.id]@]* and pass the entity.id in there so the POST url ends up having the id. We could have instead done a hidden field as well like the add/edit. The choice is up to you. We could use a GET request to delete an entity but we prefer to use POST for deletes/creates/modifications and use GET for read with no side-affects to the database. There are definite exceptions to this like a confirmation email may contain a GET request link which would modify your database state of the email to be confirmed. So while, some developers stay in the theoretical world, there always seem to be exceptions since you can't do a POST from an email which we can redirect to a GET.
Now, if you really want, from an email, you can do a GET, then redirect to a POST and then redirect back to a GET to stay compliant with GET requests are only reads, not writes. Ideally, when you do a GET, it only reads but this type of GET/Redirect/POST/Redirect will still be a link that tries modify the database a second time (to the same value of course the second time)
Next, let's create the post method in our controller to actually delete the entity
*[public Redirect postDeleteAccount(int id) {
AccountDbo ref = Em.get().find(AccountDbo.class, id);
Em.get().remove(ref);
Em.get().flush();
Current.flash().setMessage("Account deleted");
Current.flash().keep(true);
return Actions.redirect(AccountRouteId.LIST_ACCOUNTS);
}]*
Now, go ahead and create and delete entities to your hearts content to see the flow of the standard CRUD code for webpieces. We hope to have a plugin to generate all this on fly in the future for you. Basically follow a wizard, type in your fields and we generate the list page, add/edit/delete pages, controllers, routes, etc. all in one tight little package.
In the next tutorial let's do an ajax CRUD in webpieces which is the same amount of code ironically.
Next Ajax CRUD