Project Student: Maintenance Webapp (editable)
This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer, Persistence with Spring Data, Sharding Integration Test Data, Webservice Integration, JPA Criteria Queries and Maintenance Webapp (read-only).
Last time we created a simple webapp that allows us to take a quick peek into the database. It had very limited functionality – the primary goal was to knit together a system that exercised the entire stack from web browser to database. This time we add actual CRUD support.
This post is borrows heavily from the jumpstart site but there are significant differences. There’s a lot of code but it’s boilerplate that can be easily reused.
Limitations
- User authentication – no effort has been made to authenticate users.
- Encryption – no effort has been made to encrypt communications.
- Pagination – no effort has been made to support pagination. The Tapestry 5 component will give the appearance of pagination but it will always contain the same first page of data.
- Error Messages – error messages will be shown but server-side errors will be uninformative for now.
- Cross-Site Scripting (XSS) – no effort has been made to prevent XSS attacks.
- Internationalization – no effort has been made to support internationalization.
Goal
We want the standard CRUD pages.
First, we need to be able to create a new course. Our list of courses should include a link as a default message when we don’t have any data. (The first “create…” is a separate element.)
Now a creation page with several fields. A code uniquely identifies a course, e.g., CSCI 101, and name, summary and description should be self-explanatory.
After successful creation we’re taken to a review page.
And then back to an update page if we need to make a change.
At any point we can go back to the list page.
We’re prompted for confirmation before deleting a record.
And finally we’re able to show server-side errors, e.g., for duplicate values in unique fields, even if the messages are pretty useless at the moment.
We also have client-side error checking although I don’t show it here.
Index.tml
We start with the index page. It’s similar to what we saw in the last post.
Tapestry 5 has three major types of links. A pagelink is mapped to a standard HTML link. An actionlink is directly handled by the corresponding class, e.g., Index.java for the Index.tml template. Finally an eventlink injects an event into the normal event flow within the tapestry engine. All of my links go to a closely related page so I use an actionlink.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | < html t:type = "layout" title = "Course List" t:sidebarTitle = "Framework Version" xmlns:p = "tapestry:parameter" > < t:zone t:id = "zone" > < p > "Course" page </ p > < t:actionlink t:id = "create" >Create...</ t:actionlink >< br /> < t:grid source = "courses" row = "course" include = "code, name,creationdate" add = "edit,delete" > < p:codecell > < t:actionlink t:id = "view" context = "course.uuid" >${course.code}</ t:actionlink > </ p:codecell > < p:editcell > < t:actionlink t:id = "update" context = "course.uuid" >Edit</ t:actionlink > </ p:editcell > < p:deletecell > < t:actionlink t:id = "delete" context = "course.uuid" t:mixins = "Confirm" t:message = "Delete ${course.name}?" >Delete</ t:actionlink > </ p:deletecell > < p:empty > < p >There are no courses to display; you can < t:actionlink t:id = "create1" >create</ t:actionlink > one.</ p > </ p:empty > </ t:grid > </ t:zone > < p:sidebar > < p > [ < t:pagelink page = "Index" >Index</ t:pagelink > ]< br /> [ < t:pagelink page = "Course/Index" >Courses</ t:pagelink > ] </ p > </ p:sidebar > </ html > |
Confirm mixin
The Index.tml template included a ‘mixin’ on the delete actionlink. It uses a mixture of javascript and java to display a popup message to ask the user to verify that he wants to delete the course.
This code is straight from the jumpstart and Tapestry sites.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | Confirm = Class.create({ initialize: function(elementId, message) { this .message = message; Event.observe($(elementId), 'click' , this .doConfirm.bindAsEventListener( this )); }, doConfirm: function(e) { // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp) if (!confirm( this .message)) { e.stop(); } } }) // Extend the Tapestry.Initializer with a static method that instantiates a Confirm. Tapestry.Initializer.confirm = function(spec) { new Confirm(spec.elementId, spec.message); } |
The corresponding java code is
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Import (library = "confirm.js" ) public class Confirm { @Parameter (name = "message" , value = "Are you sure?" , defaultPrefix = BindingConstants.LITERAL) private String message; @Inject private JavaScriptSupport javaScriptSupport; @InjectContainer private ClientElement clientElement; @AfterRender public void afterRender() { // Tell the Tapestry.Initializer to do the initializing of a Confirm, // which it will do when the DOM has been // fully loaded. JSONObject spec = new JSONObject(); spec.put( "elementId" , clientElement.getClientId()); spec.put( "message" , message); javaScriptSupport.addInitializerCall( "confirm" , spec); } } |
Index.java
The java that supports the index template is straightforward since we only need to define a data source and provide a few action handlers.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | package com.invariantproperties.sandbox.student.maintenance.web.pages.course; public class Index { @Property @Inject @Symbol (SymbolConstants.TAPESTRY_VERSION) private String tapestryVersion; @InjectComponent private Zone zone; @Inject private CourseFinderService courseFinderService; @Inject private CourseManagerService courseManagerService; @Property private Course course; // our sibling page @InjectPage private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Editor editorPage; /** * Get the datasource containing our data. * * @return */ public GridDataSource getCourses() { return new CoursePagedDataSource(courseFinderService); } /** * Handle a delete request. This could fail, e.g., if the course has already * been deleted. * * @param courseUuid */ void onActionFromDelete(String courseUuid) { courseManagerService.deleteCourse(courseUuid, 0 ); } /** * Bring up editor page in create mode. * * @param courseUuid * @return */ Object onActionFromCreate() { editorPage.setup(Mode.CREATE, null ); return editorPage; } /** * Bring up editor page in create mode. * * @param courseUuid * @return */ Object onActionFromCreate1() { return onActionFromCreate(); } /** * Bring up editor page in review mode. * * @param courseUuid * @return */ Object onActionFromView(String courseUuid) { editorPage.setup(Mode.REVIEW, courseUuid); return editorPage; } /** * Bring up editor page in update mode. * * @param courseUuid * @return */ Object onActionFromUpdate(String courseUuid) { editorPage.setup(Mode.UPDATE, courseUuid); return editorPage; } } |
Editor.tml
The CRUD pages could be three separate pages (for create, review and update) or a single page. I’m following the pattern used by the jumpstart site – a single page. I’ll be honest – I’m not sure why he made this decision – perhaps it is because the pages are closely related and he uses event processing? In any case I’ll discuss the elements separately.
CREATE template
The “create” template is a simple form. You can see that HTML <input> elements are enhanced with some tapestry-specific attributes, plus a few additional tags like <t:errors/> and <t:submit>.
The CustomForm and CustomError are local extensions to the standard Tapestry Form and Error classes. They’re currently empty but allow us to easily add local extensions.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | < html t:type = "layout" title = "Course Editor" t:sidebarTitle = "Framework Version" < t:zone t:id = "zone" > < t:if test = "modeCreate" > < h1 >Create</ h1 > < form t:type = "form" t:id = "createForm" > < t:errors /> < table > < tr > < th >< t:label for = "code" />:</ th > < td >< input t:type = "TextField" t:id = "code" value = "course.code" t:validate = "required, maxlength=12" size = "12" /></ td > < td >(required)</ td > </ tr > < tr class = "err" > < th ></ th > < td colspan = "2" >< t:CustomError for = "code" /></ td > </ tr > < tr > < th >< t:label for = "name" />:</ th > < td >< input t:type = "TextField" t:id = "name" value = "course.name" t:validate = "required, maxlength=80" size = "45" /></ td > < td >(required)</ td > </ tr > < tr class = "err" > < th ></ th > < td colspan = "2" >< t:CustomError for = "name" /></ td > </ tr > < tr > < th >< t:label for = "summary" />:</ th > < td >< input cols = "50" rows = "4" t:type = "TextArea" t:id = "summary" value = "course.summary" t:validate = "maxlength=400" /></ td > </ tr > < tr class = "err" > < th ></ th > < td colspan = "2" >< t:CustomError for = "summary" /></ td > </ tr > < tr > < th >< t:label for = "description" />:</ th > < td >< input cols = "50" rows = "12" t:type = "TextArea" t:id = "description" value = "course.description" t:validate = "maxlength=2000" /></ td > </ tr > < tr class = "err" > < th ></ th > < td colspan = "2" >< t:CustomError for = "description" /></ td > </ tr > </ table > < div class = "buttons" > < t:submit t:event = "cancelCreate" t:context = "course.uuid" value = "Cancel" /> < input type = "submit" value = "Save" /> </ div > </ form > </ t:if > ... </ html > |
CREATE java
- The corresponding java class is straightforward. We must define a few custom events.
- The ActivationRequestParameter values are pulled from the URL query string.
- The course field contains the values to be used when creating a new object.
- The courseForm field contains the corresponding <form> on the template.
- The indexPage contains a reference to the index page.
There are four event handlers named onEventFromCreateForm, where event
can be prepare, validate, success or failure. Each event handler has very specific roles.
There is one additional event handler, onCancelCreate(). You can see the name of that event in the <t:submit> tag in the template.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | /** * This component will trigger the following events on its container (which in * this example is the page): * {@link Editor.web.components.examples.component.crud.Editor#CANCEL_CREATE} , * {@link Editor.web.components.examples.component.crud.Editor#SUCCESSFUL_CREATE} * (Long courseUuid), * {@link Editor.web.components.examples.component.crud.Editor#FAILED_CREATE} , */ // @Events is applied to a component solely to document what events it may // trigger. It is not checked at runtime. @Events ({ Editor.CANCEL_CREATE, Editor.SUCCESSFUL_CREATE, Editor.FAILED_CREATE }) public class Editor { public static final String CANCEL_CREATE = "cancelCreate" ; public static final String SUCCESSFUL_CREATE = "successfulCreate" ; public static final String FAILED_CREATE = "failedCreate" ; public enum Mode { CREATE, REVIEW, UPDATE; } // Parameters @ActivationRequestParameter @Property private Mode mode; @ActivationRequestParameter @Property private String courseUuid; // Screen fields @Property private Course course; // Work fields // This carries version through the redirect that follows a server-side // validation failure. @Persist (PersistenceConstants.FLASH) private Integer versionFlash; // Generally useful bits and pieces @Inject private CourseFinderService courseFinderService; @Inject private CourseManagerService courseManagerService; @Component private CustomForm createForm; @Inject private ComponentResources componentResources; @InjectPage private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage; // The code public void setup(Mode mode, String courseUuid) { this .mode = mode; this .courseUuid = courseUuid; } // setupRender() is called by Tapestry right before it starts rendering the // component. void setupRender() { if (mode == Mode.REVIEW) { if (courseUuid == null ) { course = null ; // Handle null course in the template. } else { if (course == null ) { try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { // Handle null course in the template. } } } } } // ///////////////////////////////////////////////////////////////////// // CREATE // ///////////////////////////////////////////////////////////////////// // Handle event "cancelCreate" Object onCancelCreate() { return indexPage; } // Component "createForm" bubbles up the PREPARE event when it is rendered // or submitted void onPrepareFromCreateForm() throws Exception { // Instantiate a Course for the form data to overlay. course = new Course(); } // Component "createForm" bubbles up the VALIDATE event when it is submitted void onValidateFromCreateForm() { if (createForm.getHasErrors()) { // We get here only if a server-side validator detected an error. return ; } try { course = courseManagerService.createCourse(course.getCode(), course.getName(), course.getSummary(), course.getDescription(), 1 ); } catch (RestClientFailureException e) { createForm.recordError( "Internal error on server." ); createForm.recordError(e.getMessage()); } catch (Exception e) { createForm.recordError(ExceptionUtil.getRootCauseMessage(e)); } } // Component "createForm" bubbles up SUCCESS or FAILURE when it is // submitted, depending on whether VALIDATE // records an error boolean onSuccessFromCreateForm() { componentResources.triggerEvent(SUCCESSFUL_CREATE, new Object[] { course.getUuid() }, null ); // We don't want "success" to bubble up, so we return true to say we've // handled it. mode = Mode.REVIEW; courseUuid = course.getUuid(); return true ; } boolean onFailureFromCreateForm() { // Rather than letting "failure" bubble up which doesn't say what you // were trying to do, we trigger new event // "failedCreate". It will bubble up because we don't have a handler // method for it. componentResources.triggerEvent(FAILED_CREATE, null , null ); // We don't want "failure" to bubble up, so we return true to say we've // handled it. return true ; } .... } |
REVIEW template
The “review” template is a simple table. It is wrapped in a form but that’s solely for the navigation buttons at the bottom of the page.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <t: if test= "modeReview" > <h1>Review</h1> <strong>Warning: no attempt is made to block XSS</strong> <form t:type= "form" t:id= "reviewForm" > <t:errors/> <t: if test= "course" > <div t:type= "if" t:test= "deleteMessage" class = "error" > ${deleteMessage} </div> <table> <tr> <th>Uuid:</th> <td>${course.uuid}</td> </tr> <tr> <th>Code:</th> <td>${course.code}</td> </tr> <tr> <th>Name:</th> <td>${course.name}</td> </tr> <tr> <th>Summary:</th> <td>${course.summary}</td> </tr> <tr> <th>Description:</th> <td>${course.description}</td> </tr> </table> <div class = "buttons" > <t:submit t:event= "toIndex" t:context= "course.uuid" value= "List" /> <t:submit t:event= "toUpdate" t:context= "course.uuid" value= "Update" /> <t:submit t:event= "delete" t:context= "course.uuid" t:mixins= "Confirm" t:message= "Delete ${course.name}?" value= "Delete" /> </div> </t: if > <t: if negate= "true" test= "course" > Course ${courseUuid} does not exist.<br/><br/> </t: if > </form> </t: if > |
REVIEW java
The java required for the review form is trivial – we just need to load the data. I would have expected the setupRender() to be enough but in practice I needed the onPrepareFromReviewForm() method.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | public class Editor { public enum Mode { CREATE, REVIEW, UPDATE; } // Parameters @ActivationRequestParameter @Property private Mode mode; @ActivationRequestParameter @Property private String courseUuid; // Screen fields @Property private Course course; // Generally useful bits and pieces @Inject private CourseFinderService courseFinderService; @Component private CustomForm reviewForm; @Inject private ComponentResources componentResources; @InjectPage private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage; // The code public void setup(Mode mode, String courseUuid) { this .mode = mode; this .courseUuid = courseUuid; } // setupRender() is called by Tapestry right before it starts rendering the // component. void setupRender() { if (mode == Mode.REVIEW) { if (courseUuid == null ) { course = null ; // Handle null course in the template. } else { if (course == null ) { try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { // Handle null course in the template. } } } } } // ///////////////////////////////////////////////////////////////////// // REVIEW // ///////////////////////////////////////////////////////////////////// void onPrepareFromReviewForm() { try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { // Handle null course in the template. } } // ///////////////////////////////////////////////////////////////////// // PAGE NAVIGATION // ///////////////////////////////////////////////////////////////////// // Handle event "toUpdate" boolean onToUpdate(String courseUuid) { mode = Mode.UPDATE; return false ; } // Handle event "toIndex" Object onToIndex() { return indexPage; } .... } |
UPDATE template
Finally, the “update” template looks similar to the “create” template.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <t: if test= "modeUpdate" > <h1>Update</h1> <strong>Warning: no attempt is made to block XSS</strong> <form t:type= "form" t:id= "updateForm" > <t:errors/> <t: if test= "course" > <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. --> <!-- <t:hidden value= "course.version" /> --> <table> <tr> <th><t:label for = "updCode" />:</th> <td><input t:type= "TextField" t:id= "updCode" value= "course.code" t:disabled= "true" size= "12" /></td> <td>(read-only)</td> </tr> <tr class = "err" > <th></th> <td colspan= "2" ><t:CustomError for = "updName" /></td> </tr> <tr> <th><t:label for = "updName" />:</th> <td><input t:type= "TextField" t:id= "updName" value= "course.name" t:validate= "required, maxlength=80" size= "45" /></td> <td>(required)</td> </tr> <tr class = "err" > <th></th> <td colspan= "2" ><t:CustomError for = "updSummary" /></td> </tr> <tr> <th><t:label for = "updSummary" />:</th> <td><input cols= "50" rows= "4" t:type= "TextArea" t:id= "updSummary" value= "course.summary" t:validate= "maxlength=400" /></td> </tr> <tr class = "err" > <th></th> <td colspan= "2" ><t:CustomError for = "updSummary" /></td> </tr> <tr> <th><t:label for = "updDescription" />:</th> <td><input cols= "50" rows= "12" t:type= "TextArea" t:id= "updDescription" value= "course.description" t:validate= "maxlength=50" /></td> </tr> <tr class = "err" > <th></th> <td colspan= "2" ><t:CustomError for = "updDescription" /></td> </tr> </table> <div class = "buttons" > <t:submit t:event= "toIndex" t:context= "course.uuid" value= "List" /> <t:submit t:event= "cancelUpdate" t:context= "course.uuid" value= "Cancel" /> <input t:type= "submit" value= "Save" /> </div> </t: if > <t: if negate= "true" test= "course" > Course ${courseUuid} does not exist.<br/><br/> </t: if > </form> </t: if > |
UPDATE java
Likewise the “update” java code looks a lot like the “create” java code. The biggest difference is that we have to be able to handle a race condition where a course has been deleted before we attempt to update the database.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | @Events ({ Editor.TO_UPDATE, Editor.CANCEL_UPDATE, Editor.SUCCESSFUL_UPDATE, Editor.FAILED_UPDATE }) public class Editor { public static final String TO_UPDATE = "toUpdate" ; public static final String CANCEL_UPDATE = "cancelUpdate" ; public static final String SUCCESSFUL_UPDATE = "successfulUpdate" ; public static final String FAILED_UPDATE = "failedUpdate" ; public enum Mode { CREATE, REVIEW, UPDATE; } // Parameters @ActivationRequestParameter @Property private Mode mode; @ActivationRequestParameter @Property private String courseUuid; // Screen fields @Property private Course course; @Property @Persist (PersistenceConstants.FLASH) private String deleteMessage; // Work fields // This carries version through the redirect that follows a server-side // validation failure. @Persist (PersistenceConstants.FLASH) private Integer versionFlash; // Generally useful bits and pieces @Inject private CourseFinderService courseFinderService; @Inject private CourseManagerService courseManagerService; @Component private CustomForm updateForm; @Inject private ComponentResources componentResources; @InjectPage private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage; // The code public void setup(Mode mode, String courseUuid) { this .mode = mode; this .courseUuid = courseUuid; } // setupRender() is called by Tapestry right before it starts rendering the // component. void setupRender() { if (mode == Mode.REVIEW) { if (courseUuid == null ) { course = null ; // Handle null course in the template. } else { if (course == null ) { try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { // Handle null course in the template. } } } } } // ///////////////////////////////////////////////////////////////////// // UPDATE // ///////////////////////////////////////////////////////////////////// // Handle event "cancelUpdate" Object onCancelUpdate(String courseUuid) { return indexPage; } // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during // form render void onPrepareForRenderFromUpdateForm() { try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { // Handle null course in the template. } // If the form has errors then we're redisplaying after a redirect. // Form will restore your input values but it's up to us to restore // Hidden values. if (updateForm.getHasErrors()) { if (course != null ) { course.setVersion(versionFlash); } } } // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event during for // submission void onPrepareForSubmitFromUpdateForm() { // Get objects for the form fields to overlay. try { course = courseFinderService.findCourseByUuid(courseUuid); } catch (ObjectNotFoundException e) { course = new Course(); updateForm.recordError( "Course has been deleted by another process." ); } } // Component "updateForm" bubbles up the VALIDATE event when it is submitted void onValidateFromUpdateForm() { if (updateForm.getHasErrors()) { // We get here only if a server-side validator detected an error. return ; } try { courseManagerService .updateCourse(course, course.getName(), course.getSummary(), course.getDescription(), 1 ); } catch (RestClientFailureException e) { updateForm.recordError( "Internal error on server." ); updateForm.recordError(e.getMessage()); } catch (Exception e) { // Display the cause. In a real system we would try harder to get a // user-friendly message. updateForm.recordError(ExceptionUtil.getRootCauseMessage(e)); } } // Component "updateForm" bubbles up SUCCESS or FAILURE when it is // submitted, depending on whether VALIDATE // records an error boolean onSuccessFromUpdateForm() { // We want to tell our containing page explicitly what course we've // updated, so we trigger new event // "successfulUpdate" with a parameter. It will bubble up because we // don't have a handler method for it. componentResources.triggerEvent(SUCCESSFUL_UPDATE, new Object[] { courseUuid }, null ); // We don't want "success" to bubble up, so we return true to say we've // handled it. mode = Mode.REVIEW; return true ; } boolean onFailureFromUpdateForm() { versionFlash = course.getVersion(); // Rather than letting "failure" bubble up which doesn't say what you // were trying to do, we trigger new event // "failedUpdate". It will bubble up because we don't have a handler // method for it. componentResources.triggerEvent(FAILED_UPDATE, new Object[] { courseUuid }, null ); // We don't want "failure" to bubble up, so we return true to say we've // handled it. return true ; } } |
DELETE template and java
The editor doesn’t have an explicit “delete” mode but it does support deleting the current object on the review and update pages.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // ///////////////////////////////////////////////////////////////////// // DELETE // ///////////////////////////////////////////////////////////////////// // Handle event "delete" Object onDelete(String courseUuid) { this .courseUuid = courseUuid; int courseVersion = 0 ; try { courseManagerService.deleteCourse(courseUuid, courseVersion); } catch (ObjectNotFoundException e) { // the object's already deleted } catch (RestClientFailureException e) { createForm.recordError( "Internal error on server." ); createForm.recordError(e.getMessage()); // Display the cause. In a real system we would try harder to get a // user-friendly message. deleteMessage = ExceptionUtil.getRootCauseMessage(e); // Trigger new event "failedDelete" which will bubble up. componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null ); // We don't want "delete" to bubble up, so we return true to say // we've handled it. return true ; } catch (Exception e) { // Display the cause. In a real system we would try harder to get a // user-friendly message. deleteMessage = ExceptionUtil.getRootCauseMessage(e); // Trigger new event "failedDelete" which will bubble up. componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null ); // We don't want "delete" to bubble up, so we return true to say // we've handled it. return true ; } // Trigger new event "successfulDelete" which will bubble up. componentResources.triggerEvent(SUCCESFUL_DELETE, new Object[] { courseUuid }, null ); // We don't want "delete" to bubble up, so we return true to say we've // handled it. return indexPage; } |
Next Steps
The obvious next steps are improving the error messages, adding support for pagination, support, and one-to-many and many-to-many relationships. All will require revising the REST payloads. I have a few additional items in the pipeline, e.g., an ExceptionService, to say nothing of the security issues.
Source Code
- The source code is available at http://code.google.com/p/invariant-properties-blog/source/browse/student.