Это часть проекта Студент . Другими публикациями являются клиент Webservice с Джерси , сервер Webservice с Джерси , бизнес-уровень , постоянство с данными Spring , тестовые данные интеграции Sharding, интеграция с Webservice , запросы JPA Criteria и обслуживание Webapp (только для чтения) .
В прошлый раз мы создали простое веб-приложение, которое позволяет нам быстро взглянуть на базу данных. У него была очень ограниченная функциональность — основная цель состояла в том, чтобы собрать систему, которая использовала весь стек от веб-браузера до базы данных. На этот раз мы добавили актуальную поддержку CRUD.
Этот пост заимствован из сайта Jumpstart, но есть существенные различия. Там много кода, но это шаблон, который можно легко использовать повторно.
Ограничения
- Аутентификация пользователя — не было предпринято никаких усилий для аутентификации пользователей.
- Шифрование — не было предпринято никаких усилий для шифрования сообщений.
- Нумерация страниц — не было предпринято никаких усилий для поддержки нумерации страниц. Компонент Tapestry 5 создаст вид пагинации, но он всегда будет содержать одну и ту же первую страницу данных.
- Сообщения об ошибках — будут отображаться сообщения об ошибках, но ошибки на стороне сервера пока будут неинформативными.
- Межсайтовый скриптинг (XSS) — не было предпринято никаких усилий для предотвращения XSS-атак.
- Интернационализация — не было предпринято никаких усилий для поддержки интернационализации.
Цель
Мы хотим, чтобы стандартные страницы CRUD.
Во-первых, нам нужно создать новый курс. Наш список курсов должен включать ссылку в качестве сообщения по умолчанию, когда у нас нет никаких данных. (Первое «создать…» — это отдельный элемент.)
Теперь страница создания с несколькими полями. Код однозначно идентифицирует курс, например, CSCI 101, а название, краткое изложение и описание должны быть самоочевидными.
После успешного создания мы попадаем на страницу обзора.
А затем вернитесь на страницу обновления, если нам нужно внести изменения.
В любой момент мы можем вернуться на страницу списка.
Перед удалением записи у нас запрашивается подтверждение.
И, наконец, мы можем показать ошибки на стороне сервера, например, для дублированных значений в уникальных полях, даже если сообщения в настоящее время довольно бесполезны.
У нас также есть проверка ошибок на стороне клиента, хотя я ее здесь не показываю.
Index.tml
Начнем со страницы индекса. Это похоже на то, что мы видели в последнем посте.
Гобелен 5 имеет три основных типа ссылок. Ссылка на страницу сопоставляется со стандартной ссылкой HTML. Ссылка на действие напрямую обрабатывается соответствующим классом, например, Index.java для шаблона Index.tml . Наконец, ссылка на событие внедряет событие в обычный поток событий в механизме гобелена. Все мои ссылки идут на тесно связанную страницу, поэтому я использую ссылку для действий.
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 > |
Подтвердите миксин
Шаблон Index.tml включает в себя «mixin» в ссылке для удаления действия. Он использует смесь javascript и java для отображения всплывающего сообщения, чтобы попросить пользователя подтвердить, что он хочет удалить курс.
Этот код прямо с сайтов Jumpstart и Tapestry.
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); } |
Соответствующий код Java
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
Java, который поддерживает шаблон индекса, прост, поскольку нам нужно только определить источник данных и предоставить несколько обработчиков действий.
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
Страницы CRUD могут быть тремя отдельными страницами (для создания, просмотра и обновления) или одной страницей. Я следую схеме, используемой сайтом Jumpstart — одной странице. Я буду честен — я не уверен, почему он принял это решение — возможно, это потому, что страницы тесно связаны, и он использует обработку событий? В любом случае я буду обсуждать элементы отдельно.
СОЗДАТЬ шаблон
Шаблон «создать» — это простая форма. Вы можете видеть, что HTML-элементы <input> дополнены некоторыми специфическими для гобелена атрибутами, а также несколькими дополнительными тегами, такими как <t: errors /> и <t: submit>.
CustomForm и CustomError являются локальными расширениями стандартных форм Tapestry Form и Error . В настоящее время они пусты, но позволяют легко добавлять локальные расширения.
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 > |
СОЗДАТЬ Java
- Соответствующий класс Java прост. Мы должны определить несколько пользовательских событий.
- Значения ActivationRequestParameter извлекаются из строки запроса URL.
- Поле курса содержит значения, которые будут использоваться при создании нового объекта.
- Поле courseForm содержит соответствующий <form> в шаблоне.
- IndexPage содержит ссылку на страницу индекса.
Существует четыре обработчика событий с именем onEventFromCreateForm , где событие
можно подготовить, подтвердить, успех или неудача. Каждый обработчик событий имеет очень определенные роли.
Есть еще один обработчик события, onCancelCreate () . Вы можете увидеть название этого события в теге <t: submit> в шаблоне.
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 ; } .... } |
ОБЗОР шаблон
Шаблон «обзор» представляет собой простую таблицу. Он обернут в форму, но это только для кнопок навигации в нижней части страницы.
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 > |
ОБЗОР ЯВА
Java, необходимый для формы обзора, тривиален — нам просто нужно загрузить данные. Я ожидал бы, что setupRender () будет достаточно, но на практике мне понадобился метод onPrepareFromReviewForm () .
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» выглядит аналогично шаблону «create».
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 > |
ОБНОВЛЕНИЕ Java
Аналогично, «обновляемый» Java-код очень похож на «создающий» Java-код. Самое большое отличие состоит в том, что мы должны быть в состоянии обработать состояние гонки, когда курс был удален, прежде чем мы попытаемся обновить базу данных.
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 ; } } |
УДАЛИТЬ шаблон и Java
Редактор не имеет явного режима «удаления», но поддерживает удаление текущего объекта на страницах просмотра и обновления.
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; } |
Следующие шаги
Следующими очевидными шагами являются улучшение сообщений об ошибках, добавление поддержки разбиения на страницы, поддержки и отношений «один ко многим» и «многие ко многим». Все это потребует пересмотра полезных нагрузок REST. У меня есть несколько дополнительных элементов в конвейере, например, ExceptionService, не говоря уже о проблемах безопасности.
Исходный код
- Исходный код доступен по адресу http://code.google.com/p/invariant-properties-blog/source/browse/student .