Это часть проекта Студент . Другими публикациями являются клиент 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 | <htmlt:type="layout"title="Course List"      t:sidebarTitle="Framework Version"      xmlns:p="tapestry:parameter">    <t:zonet:id="zone">           <p>            "Course" page        </p>        <t:actionlinkt:id="create">Create...</t:actionlink><br/>        <t:gridsource="courses"row="course"include="code, name,creationdate"add="edit,delete">            <p:codecell>                <t:actionlinkt:id="view"context="course.uuid">${course.code}</t:actionlink>            </p:codecell>            <p:editcell>                <t:actionlinkt:id="update"context="course.uuid">Edit</t:actionlink>            </p:editcell>            <p:deletecell>                <t:actionlinkt: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:actionlinkt:id="create1">create</t:actionlink> one.</p>            </p:empty>        </t:grid>    </t:zone>    <p:sidebar>        <p>            [            <t:pagelinkpage="Index">Index</t:pagelink>            ]<br/>            [            <t:pagelinkpage="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) {    newConfirm(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")publicclassConfirm {    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)    privateString message;    @Inject    privateJavaScriptSupport javaScriptSupport;    @InjectContainer    privateClientElement clientElement;    @AfterRender    publicvoidafterRender() {        // Tell the Tapestry.Initializer to do the initializing of a Confirm,        // which it will do when the DOM has been        // fully loaded.        JSONObject spec = newJSONObject();        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 | packagecom.invariantproperties.sandbox.student.maintenance.web.pages.course;publicclassIndex {    @Property    @Inject    @Symbol(SymbolConstants.TAPESTRY_VERSION)    privateString tapestryVersion;    @InjectComponent    privateZone zone;    @Inject    privateCourseFinderService courseFinderService;    @Inject    privateCourseManagerService courseManagerService;    @Property    privateCourse course;    // our sibling page    @InjectPage    privatecom.invariantproperties.sandbox.student.maintenance.web.pages.course.Editor editorPage;    /**     * Get the datasource containing our data.     *      * @return     */    publicGridDataSource getCourses() {        returnnewCoursePagedDataSource(courseFinderService);    }    /**     * Handle a delete request. This could fail, e.g., if the course has already     * been deleted.     *      * @param courseUuid     */    voidonActionFromDelete(String courseUuid) {        courseManagerService.deleteCourse(courseUuid, 0);    }    /**     * Bring up editor page in create mode.     *      * @param courseUuid     * @return     */    Object onActionFromCreate() {        editorPage.setup(Mode.CREATE, null);        returneditorPage;    }    /**     * Bring up editor page in create mode.     *      * @param courseUuid     * @return     */    Object onActionFromCreate1() {        returnonActionFromCreate();    }    /**     * Bring up editor page in review mode.     *      * @param courseUuid     * @return     */    Object onActionFromView(String courseUuid) {        editorPage.setup(Mode.REVIEW, courseUuid);        returneditorPage;    }    /**     * Bring up editor page in update mode.     *      * @param courseUuid     * @return     */    Object onActionFromUpdate(String courseUuid) {        editorPage.setup(Mode.UPDATE, courseUuid);        returneditorPage;    }} | 
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 | <htmlt:type="layout"title="Course Editor"      t:sidebarTitle="Framework Version"    <t:zonet:id="zone">       <t:iftest="modeCreate">        <h1>Create</h1>        <formt:type="form"t:id="createForm">            <t:errors/>            <table>                <tr>                    <th><t:labelfor="code"/>:</th>                    <td><inputt:type="TextField"t:id="code"value="course.code"t:validate="required, maxlength=12"size="12"/></td>                    <td>(required)</td>                </tr>                <trclass="err">                    <th></th>                    <tdcolspan="2"><t:CustomErrorfor="code"/></td>                </tr>                <tr>                    <th><t:labelfor="name"/>:</th>                    <td><inputt:type="TextField"t:id="name"value="course.name"t:validate="required, maxlength=80"size="45"/></td>                    <td>(required)</td>                </tr>                <trclass="err">                    <th></th>                    <tdcolspan="2"><t:CustomErrorfor="name"/></td>                </tr>                <tr>                    <th><t:labelfor="summary"/>:</th>                    <td><inputcols="50"rows="4"t:type="TextArea"t:id="summary"value="course.summary"t:validate="maxlength=400"/></td>                </tr>                <trclass="err">                    <th></th>                    <tdcolspan="2"><t:CustomErrorfor="summary"/></td>                </tr>                <tr>                    <th><t:labelfor="description"/>:</th>                    <td><inputcols="50"rows="12"t:type="TextArea"t:id="description"value="course.description"t:validate="maxlength=2000"/></td>                </tr>                <trclass="err">                    <th></th>                    <tdcolspan="2"><t:CustomErrorfor="description"/></td>                </tr>            </table>            <divclass="buttons">                <t:submitt:event="cancelCreate"t:context="course.uuid"value="Cancel"/>                <inputtype="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 })publicclassEditor {    publicstaticfinalString CANCEL_CREATE = "cancelCreate";    publicstaticfinalString SUCCESSFUL_CREATE = "successfulCreate";    publicstaticfinalString FAILED_CREATE = "failedCreate";    publicenumMode {        CREATE, REVIEW, UPDATE;    }    // Parameters    @ActivationRequestParameter    @Property    privateMode mode;    @ActivationRequestParameter    @Property    privateString courseUuid;    // Screen fields    @Property    privateCourse course;    // Work fields    // This carries version through the redirect that follows a server-side    // validation failure.    @Persist(PersistenceConstants.FLASH)    privateInteger versionFlash;    // Generally useful bits and pieces    @Inject    privateCourseFinderService courseFinderService;    @Inject    privateCourseManagerService courseManagerService;    @Component    privateCustomForm createForm;    @Inject    privateComponentResources componentResources;    @InjectPage    privatecom.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;    // The code    publicvoidsetup(Mode mode, String courseUuid) {        this.mode = mode;        this.courseUuid = courseUuid;    }    // setupRender() is called by Tapestry right before it starts rendering the    // component.    voidsetupRender() {        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() {        returnindexPage;    }    // Component "createForm" bubbles up the PREPARE event when it is rendered    // or submitted    voidonPrepareFromCreateForm() throwsException {        // Instantiate a Course for the form data to overlay.        course = newCourse();    }    // Component "createForm" bubbles up the VALIDATE event when it is submitted    voidonValidateFromCreateForm() {        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    booleanonSuccessFromCreateForm() {        componentResources.triggerEvent(SUCCESSFUL_CREATE, newObject[] { 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();        returntrue;    }    booleanonFailureFromCreateForm() {        // 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.        returntrue;    }    ....} | 
ОБЗОР шаблон
Шаблон «обзор» представляет собой простую таблицу. Он обернут в форму, но это только для кнопок навигации в нижней части страницы.
| 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:iftest="modeReview">        <h1>Review</h1>        <strong>Warning: no attempt is made to block XSS</strong>        <form t:type="form"t:id="reviewForm">            <t:errors/>        <t:iftest="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:ifnegate="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 | publicclassEditor {    publicenumMode {        CREATE, REVIEW, UPDATE;    }    // Parameters    @ActivationRequestParameter    @Property    privateMode mode;    @ActivationRequestParameter    @Property    privateString courseUuid;    // Screen fields    @Property    privateCourse course;    // Generally useful bits and pieces    @Inject    privateCourseFinderService courseFinderService;    @Component    privateCustomForm reviewForm;    @Inject    privateComponentResources componentResources;    @InjectPage    privatecom.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;    // The code    publicvoidsetup(Mode mode, String courseUuid) {        this.mode = mode;        this.courseUuid = courseUuid;    }    // setupRender() is called by Tapestry right before it starts rendering the    // component.    voidsetupRender() {        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    // /////////////////////////////////////////////////////////////////////    voidonPrepareFromReviewForm() {        try{            course = courseFinderService.findCourseByUuid(courseUuid);        } catch(ObjectNotFoundException e) {            // Handle null course in the template.        }    }    // /////////////////////////////////////////////////////////////////////    // PAGE NAVIGATION    // /////////////////////////////////////////////////////////////////////    // Handle event "toUpdate"    booleanonToUpdate(String courseUuid) {        mode = Mode.UPDATE;        returnfalse;    }    // Handle event "toIndex"    Object onToIndex() {        returnindexPage;    }    ....} | 
ОБНОВЛЕНИЕ шаблона
Наконец, шаблон «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:iftest="modeUpdate">        <h1>Update</h1>        <strong>Warning: no attempt is made to block XSS</strong>        <form t:type="form"t:id="updateForm">            <t:errors/>            <t:iftest="course">                <!-- If optimistic locking is not needed then comment out thisnext 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:ifnegate="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 })publicclassEditor {    publicstaticfinalString TO_UPDATE = "toUpdate";    publicstaticfinalString CANCEL_UPDATE = "cancelUpdate";    publicstaticfinalString SUCCESSFUL_UPDATE = "successfulUpdate";    publicstaticfinalString FAILED_UPDATE = "failedUpdate";    publicenumMode {        CREATE, REVIEW, UPDATE;    }    // Parameters    @ActivationRequestParameter    @Property    privateMode mode;    @ActivationRequestParameter    @Property    privateString courseUuid;    // Screen fields    @Property    privateCourse course;    @Property    @Persist(PersistenceConstants.FLASH)    privateString deleteMessage;    // Work fields    // This carries version through the redirect that follows a server-side    // validation failure.    @Persist(PersistenceConstants.FLASH)    privateInteger versionFlash;    // Generally useful bits and pieces    @Inject    privateCourseFinderService courseFinderService;    @Inject    privateCourseManagerService courseManagerService;    @Component    privateCustomForm updateForm;    @Inject    privateComponentResources componentResources;    @InjectPage    privatecom.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;    // The code    publicvoidsetup(Mode mode, String courseUuid) {        this.mode = mode;        this.courseUuid = courseUuid;    }    // setupRender() is called by Tapestry right before it starts rendering the    // component.    voidsetupRender() {        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) {        returnindexPage;    }    // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during    // form render    voidonPrepareForRenderFromUpdateForm() {        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    voidonPrepareForSubmitFromUpdateForm() {        // Get objects for the form fields to overlay.        try{            course = courseFinderService.findCourseByUuid(courseUuid);        } catch(ObjectNotFoundException e) {            course = newCourse();            updateForm.recordError("Course has been deleted by another process.");        }    }    // Component "updateForm" bubbles up the VALIDATE event when it is submitted    voidonValidateFromUpdateForm() {        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    booleanonSuccessFromUpdateForm() {        // 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, newObject[] { courseUuid }, null);        // We don't want "success" to bubble up, so we return true to say we've        // handled it.        mode = Mode.REVIEW;        returntrue;    }    booleanonFailureFromUpdateForm() {        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, newObject[] { courseUuid }, null);        // We don't want "failure" to bubble up, so we return true to say we've        // handled it.        returntrue;    }} | 
УДАЛИТЬ шаблон и 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;        intcourseVersion = 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, newObject[] { courseUuid }, null);            // We don't want "delete" to bubble up, so we return true to say            // we've handled it.            returntrue;        } 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, newObject[] { courseUuid }, null);            // We don't want "delete" to bubble up, so we return true to say            // we've handled it.            returntrue;        }        // Trigger new event "successfulDelete" which will bubble up.        componentResources.triggerEvent(SUCCESFUL_DELETE, newObject[] { courseUuid }, null);        // We don't want "delete" to bubble up, so we return true to say we've        // handled it.        returnindexPage;    } | 
Следующие шаги
Следующими очевидными шагами являются улучшение сообщений об ошибках, добавление поддержки разбиения на страницы, поддержки и отношений «один ко многим» и «многие ко многим». Все это потребует пересмотра полезных нагрузок REST. У меня есть несколько дополнительных элементов в конвейере, например, ExceptionService, не говоря уже о проблемах безопасности.
Исходный код
- Исходный код доступен по адресу http://code.google.com/p/invariant-properties-blog/source/browse/student .






