Статьи

Студент проекта: Техническое обслуживание Webapp (редактируемый)

Это часть проекта Студент . Другими публикациями являются клиент Webservice с Джерси , сервер Webservice с Джерси , бизнес-уровень , постоянство с данными Spring , тестовые данные интеграции Sharding, интеграция с Webservice , запросы JPA Criteria и обслуживание Webapp (только для чтения) .

В прошлый раз мы создали простое веб-приложение, которое позволяет нам быстро взглянуть на базу данных. У него была очень ограниченная функциональность — основная цель состояла в том, чтобы собрать систему, которая использовала весь стек от веб-браузера до базы данных. На этот раз мы добавили актуальную поддержку CRUD.

Этот пост заимствован из сайта Jumpstart, но есть существенные различия. Там много кода, но это шаблон, который можно легко использовать повторно.

Ограничения

  • Аутентификация пользователя — не было предпринято никаких усилий для аутентификации пользователей.
  • Шифрование — не было предпринято никаких усилий для шифрования сообщений.
  • Нумерация страниц — не было предпринято никаких усилий для поддержки нумерации страниц. Компонент Tapestry 5 создаст вид пагинации, но он всегда будет содержать одну и ту же первую страницу данных.
  • Сообщения об ошибках — будут отображаться сообщения об ошибках, но ошибки на стороне сервера пока будут неинформативными.
  • Межсайтовый скриптинг (XSS) — не было предпринято никаких усилий для предотвращения XSS-атак.
  • Интернационализация — не было предпринято никаких усилий для поддержки интернационализации.

Цель

Мы хотим, чтобы стандартные страницы CRUD.

Во-первых, нам нужно создать новый курс. Наш список курсов должен включать ссылку в качестве сообщения по умолчанию, когда у нас нет никаких данных. (Первое «создать…» — это отдельный элемент.)

Курс-List-Chromium_008

Теперь страница создания с несколькими полями. Код однозначно идентифицирует курс, например, CSCI 101, а название, краткое изложение и описание должны быть самоочевидными.

Курс-редактор Chromium_006

После успешного создания мы попадаем на страницу обзора.

Курс-редактор Chromium_002

А затем вернитесь на страницу обновления, если нам нужно внести изменения.

Курс-редактор Chromium_004

В любой момент мы можем вернуться на страницу списка.

Курс-List-Chromium_001

Перед удалением записи у нас запрашивается подтверждение.

Курс-List-Chromium_005

И, наконец, мы можем показать ошибки на стороне сервера, например, для дублированных значений в уникальных полях, даже если сообщения в настоящее время довольно бесполезны.

Курс-редактор Chromium_007

У нас также есть проверка ошибок на стороне клиента, хотя я ее здесь не показываю.

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"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
 
    <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, не говоря уже о проблемах безопасности.

Исходный код