Enterprise Java

Project Student: Maintenance Webapp (editable)

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer, Persistence with Spring Data, Sharding Integration Test Data, Webservice Integration, JPA Criteria Queries and Maintenance Webapp (read-only).

Last time we created a simple webapp that allows us to take a quick peek into the database. It had very limited functionality – the primary goal was to knit together a system that exercised the entire stack from web browser to database. This time we add actual CRUD support.

This post is borrows heavily from the jumpstart site but there are significant differences. There’s a lot of code but it’s boilerplate that can be easily reused.

Limitations

  • User authentication – no effort has been made to authenticate users.
  • Encryption – no effort has been made to encrypt communications.
  • Pagination – no effort has been made to support pagination. The Tapestry 5 component will give the appearance of pagination but it will always contain the same first page of data.
  • Error Messages – error messages will be shown but server-side errors will be uninformative for now.
  • Cross-Site Scripting (XSS) – no effort has been made to prevent XSS attacks.
  • Internationalization – no effort has been made to support internationalization.

Goal

We want the standard CRUD pages.

First, we need to be able to create a new course. Our list of courses should include a link as a default message when we don’t have any data. (The first “create…” is a separate element.)

Course-List-Chromium_008

Now a creation page with several fields. A code uniquely identifies a course, e.g., CSCI 101, and name, summary and description should be self-explanatory.

Course-Editor-Chromium_006

After successful creation we’re taken to a review page.

Course-Editor-Chromium_002

And then back to an update page if we need to make a change.

Course-Editor-Chromium_004

At any point we can go back to the list page.

Course-List-Chromium_001

We’re prompted for confirmation before deleting a record.

Course-List-Chromium_005

And finally we’re able to show server-side errors, e.g., for duplicate values in unique fields, even if the messages are pretty useless at the moment.

Course-Editor-Chromium_007

We also have client-side error checking although I don’t show it here.

Index.tml

We start with the index page. It’s similar to what we saw in the last post.

Tapestry 5 has three major types of links. A pagelink is mapped to a standard HTML link. An actionlink is directly handled by the corresponding class, e.g., Index.java for the Index.tml template. Finally an eventlink injects an event into the normal event flow within the tapestry engine. All of my links go to a closely related page so I use an actionlink.

<html t:type="layout" title="Course List"
      t:sidebarTitle="Framework Version"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter">

    <t:zone t:id="zone">   
        <p>
            "Course" page
        </p>

        <t:actionlink t:id="create">Create...</t:actionlink><br/>

        <t:grid source="courses" row="course" include="code, name,creationdate" add="edit,delete">
            <p:codecell>
                <t:actionlink t:id="view" context="course.uuid">${course.code}</t:actionlink>
            </p:codecell>
            <p:editcell>
                <t:actionlink t:id="update" context="course.uuid">Edit</t:actionlink>
            </p:editcell>
            <p:deletecell>
                <t:actionlink t:id="delete" context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?">Delete</t:actionlink>
            </p:deletecell>
            <p:empty>
              <p>There are no courses to display; you can <t:actionlink t:id="create1">create</t:actionlink> one.</p>
            </p:empty>
        </t:grid>
    </t:zone>

    <p:sidebar>
        <p>
            [
            <t:pagelink page="Index">Index</t:pagelink>
            ]<br/>
            [
            <t:pagelink page="Course/Index">Courses</t:pagelink>
            ]
        </p>
    </p:sidebar>

</html>

Confirm mixin

The Index.tml template included a ‘mixin’ on the delete actionlink. It uses a mixture of javascript and java to display a popup message to ask the user to verify that he wants to delete the course.

This code is straight from the jumpstart and Tapestry sites.

// from http://jumpstart.doublenegative.com.au/
Confirm = Class.create({

    initialize: function(elementId, message) {
        this.message = message;
        Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
    },

    doConfirm: function(e) {

        // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp)

        if (!confirm(this.message)) {
                e.stop();
        }
    }
})

// Extend the Tapestry.Initializer with a static method that instantiates a Confirm.

Tapestry.Initializer.confirm = function(spec) {
    new Confirm(spec.elementId, spec.message);
}

The corresponding java code is

// from http://jumpstart.doublenegative.com.au/
@Import(library = "confirm.js")
public class Confirm {

    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
    private String message;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @InjectContainer
    private ClientElement clientElement;

    @AfterRender
    public void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of a Confirm,
        // which it will do when the DOM has been
        // fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("message", message);
        javaScriptSupport.addInitializerCall("confirm", spec);
    }
}

Index.java

The java that supports the index template is straightforward since we only need to define a data source and provide a few action handlers.

package com.invariantproperties.sandbox.student.maintenance.web.pages.course;

public class Index {
    @Property
    @Inject
    @Symbol(SymbolConstants.TAPESTRY_VERSION)
    private String tapestryVersion;

    @InjectComponent
    private Zone zone;

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Property
    private Course course;

    // our sibling page
    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Editor editorPage;

    /**
     * Get the datasource containing our data.
     * 
     * @return
     */
    public GridDataSource getCourses() {
        return new CoursePagedDataSource(courseFinderService);
    }

    /**
     * Handle a delete request. This could fail, e.g., if the course has already
     * been deleted.
     * 
     * @param courseUuid
     */
    void onActionFromDelete(String courseUuid) {
        courseManagerService.deleteCourse(courseUuid, 0);
    }

    /**
     * Bring up editor page in create mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromCreate() {
        editorPage.setup(Mode.CREATE, null);
        return editorPage;
    }

    /**
     * Bring up editor page in create mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromCreate1() {
        return onActionFromCreate();
    }

    /**
     * Bring up editor page in review mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromView(String courseUuid) {
        editorPage.setup(Mode.REVIEW, courseUuid);
        return editorPage;
    }

    /**
     * Bring up editor page in update mode.
     * 
     * @param courseUuid
     * @return
     */
    Object onActionFromUpdate(String courseUuid) {
        editorPage.setup(Mode.UPDATE, courseUuid);
        return editorPage;
    }
}

Editor.tml

The CRUD pages could be three separate pages (for create, review and update) or a single page. I’m following the pattern used by the jumpstart site – a single page. I’ll be honest – I’m not sure why he made this decision – perhaps it is because the pages are closely related and he uses event processing? In any case I’ll discuss the elements separately.

CREATE template

The “create” template is a simple form. You can see that HTML <input> elements are enhanced with some tapestry-specific attributes, plus a few additional tags like <t:errors/> and <t:submit>.

The CustomForm and CustomError are local extensions to the standard Tapestry Form and Error classes. They’re currently empty but allow us to easily add local extensions.

<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>

CREATE java

  • The corresponding java class is straightforward. We must define a few custom events.
  • The ActivationRequestParameter values are pulled from the URL query string.
  • The course field contains the values to be used when creating a new object.
  • The courseForm field contains the corresponding <form> on the template.
  • The indexPage contains a reference to the index page.

There are four event handlers named onEventFromCreateForm, where event
can be prepare, validate, success or failure. Each event handler has very specific roles.

There is one additional event handler, onCancelCreate(). You can see the name of that event in the <t:submit> tag in the template.

/**
 * This component will trigger the following events on its container (which in
 * this example is the page):
 * {@link Editor.web.components.examples.component.crud.Editor#CANCEL_CREATE} ,
 * {@link Editor.web.components.examples.component.crud.Editor#SUCCESSFUL_CREATE}
 * (Long courseUuid),
 * {@link Editor.web.components.examples.component.crud.Editor#FAILED_CREATE} ,
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ Editor.CANCEL_CREATE, Editor.SUCCESSFUL_CREATE, Editor.FAILED_CREATE })
public class Editor {
    public static final String CANCEL_CREATE = "cancelCreate";
    public static final String SUCCESSFUL_CREATE = "successfulCreate";
    public static final String FAILED_CREATE = "failedCreate";

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    // Work fields

    // This carries version through the redirect that follows a server-side
    // validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Component
    private CustomForm createForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelCreate"

    Object onCancelCreate() {
        return indexPage;
    }

    // Component "createForm" bubbles up the PREPARE event when it is rendered
    // or submitted

    void onPrepareFromCreateForm() throws Exception {
        // Instantiate a Course for the form data to overlay.
        course = new Course();
    }

    // Component "createForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromCreateForm() {

        if (createForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            course = courseManagerService.createCourse(course.getCode(), course.getName(), course.getSummary(),
                    course.getDescription(), 1);
        } catch (RestClientFailureException e) {
            createForm.recordError("Internal error on server.");
            createForm.recordError(e.getMessage());
        } catch (Exception e) {
            createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "createForm" bubbles up SUCCESS or FAILURE when it is
    // submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromCreateForm() {
        componentResources.triggerEvent(SUCCESSFUL_CREATE, new Object[] { course.getUuid() }, null);

        // We don't want "success" to bubble up, so we return true to say we've
        // handled it.
        mode = Mode.REVIEW;
        courseUuid = course.getUuid();
        return true;
    }

    boolean onFailureFromCreateForm() {
        // Rather than letting "failure" bubble up which doesn't say what you
        // were trying to do, we trigger new event
        // "failedCreate". It will bubble up because we don't have a handler
        // method for it.
        componentResources.triggerEvent(FAILED_CREATE, null, null);

        // We don't want "failure" to bubble up, so we return true to say we've
        // handled it.
        return true;
    }

    ....
}

REVIEW template

The “review” template is a simple table. It is wrapped in a form but that’s solely for the navigation buttons at the bottom of the page.

<t:if test="modeReview">
        <h1>Review</h1>

        <strong>Warning: no attempt is made to block XSS</strong>

        <form t:type="form" t:id="reviewForm">
            <t:errors/>

        <t:if test="course">
            <div t:type="if" t:test="deleteMessage" class="error">
                ${deleteMessage}
            </div>

            <table>
                <tr>
                    <th>Uuid:</th>
                    <td>${course.uuid}</td>
                </tr>
                <tr>
                    <th>Code:</th>
                    <td>${course.code}</td>
                </tr>
                <tr>
                    <th>Name:</th>
                    <td>${course.name}</td>
                </tr>
                <tr>
                    <th>Summary:</th>
                    <td>${course.summary}</td>
                </tr>
                <tr>
                    <th>Description:</th>
                    <td>${course.description}</td>
                </tr>
            </table>

            <div class="buttons">
                <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
                <t:submit t:event="toUpdate" t:context="course.uuid" value="Update"/>
                <t:submit t:event="delete" t:context="course.uuid" t:mixins="Confirm" t:message="Delete ${course.name}?" value="Delete"/>
            </div>

        </t:if>
        <t:if negate="true" test="course">
            Course ${courseUuid} does not exist.<br/><br/>
        </t:if>
        </form>
    </t:if>

REVIEW java

The java required for the review form is trivial – we just need to load the data. I would have expected the setupRender() to be enough but in practice I needed the onPrepareFromReviewForm() method.

public class Editor {

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Component
    private CustomForm reviewForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    void onPrepareFromReviewForm() {
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            // Handle null course in the template.
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // PAGE NAVIGATION
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toUpdate"

    boolean onToUpdate(String courseUuid) {
        mode = Mode.UPDATE;
        return false;
    }

    // Handle event "toIndex"

    Object onToIndex() {
        return indexPage;
    }

    ....
}

UPDATE template

Finally, the “update” template looks similar to the “create” template.

<t:if test="modeUpdate">
        <h1>Update</h1>

        <strong>Warning: no attempt is made to block XSS</strong>

        <form t:type="form" t:id="updateForm">
            <t:errors/>

            <t:if test="course">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <!-- <t:hidden value="course.version"/> -->

                <table>
                    <tr>
                        <th><t:label for="updCode"/>:</th>
                        <td><input t:type="TextField" t:id="updCode" value="course.code" t:disabled="true" size="12"/></td>
                        <td>(read-only)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updName"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updName"/>:</th>
                        <td><input t:type="TextField" t:id="updName" value="course.name" t:validate="required, maxlength=80" size="45"/></td>
                        <td>(required)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updSummary"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updSummary"/>:</th>
                        <td><input cols="50" rows="4" t:type="TextArea" t:id="updSummary" value="course.summary" t:validate="maxlength=400"/></td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updSummary"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updDescription"/>:</th>
                        <td><input cols="50" rows="12" t:type="TextArea" t:id="updDescription" value="course.description" t:validate="maxlength=50"/></td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updDescription"/></td>
                    </tr>
                </table>

                <div class="buttons">
                    <t:submit t:event="toIndex" t:context="course.uuid" value="List"/>
                    <t:submit t:event="cancelUpdate" t:context="course.uuid" value="Cancel"/>
                    <input t:type="submit" value="Save"/>
                </div>
            </t:if>
            <t:if negate="true" test="course">
                Course ${courseUuid} does not exist.<br/><br/>
            </t:if>
        </form>    
    </t:if>

UPDATE java

Likewise the “update” java code looks a lot like the “create” java code. The biggest difference is that we have to be able to handle a race condition where a course has been deleted before we attempt to update the database.

@Events({ Editor.TO_UPDATE, Editor.CANCEL_UPDATE,
        Editor.SUCCESSFUL_UPDATE, Editor.FAILED_UPDATE })
public class Editor {
    public static final String TO_UPDATE = "toUpdate";
    public static final String CANCEL_UPDATE = "cancelUpdate";
    public static final String SUCCESSFUL_UPDATE = "successfulUpdate";
    public static final String FAILED_UPDATE = "failedUpdate";

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @ActivationRequestParameter
    @Property
    private Mode mode;

    @ActivationRequestParameter
    @Property
    private String courseUuid;

    // Screen fields

    @Property
    private Course course;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String deleteMessage;

    // Work fields

    // This carries version through the redirect that follows a server-side
    // validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @Inject
    private CourseManagerService courseManagerService;

    @Component
    private CustomForm updateForm;

    @Inject
    private ComponentResources componentResources;

    @InjectPage
    private com.invariantproperties.sandbox.student.maintenance.web.pages.course.Index indexPage;

    // The code

    public void setup(Mode mode, String courseUuid) {
        this.mode = mode;
        this.courseUuid = courseUuid;
    }

    // setupRender() is called by Tapestry right before it starts rendering the
    // component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (courseUuid == null) {
                course = null;
                // Handle null course in the template.
            } else {
                if (course == null) {
                    try {
                        course = courseFinderService.findCourseByUuid(courseUuid);
                    } catch (ObjectNotFoundException e) {
                        // Handle null course in the template.
                    }
                }
            }
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelUpdate"

    Object onCancelUpdate(String courseUuid) {
        return indexPage;
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during
    // form render

    void onPrepareForRenderFromUpdateForm() {
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            // Handle null course in the template.
        }

        // If the form has errors then we're redisplaying after a redirect.
        // Form will restore your input values but it's up to us to restore
        // Hidden values.

        if (updateForm.getHasErrors()) {
            if (course != null) {
                course.setVersion(versionFlash);
            }
        }
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event during for
    // submission

    void onPrepareForSubmitFromUpdateForm() {
        // Get objects for the form fields to overlay.
        try {
            course = courseFinderService.findCourseByUuid(courseUuid);
        } catch (ObjectNotFoundException e) {
            course = new Course();
            updateForm.recordError("Course has been deleted by another process.");
        }
    }

    // Component "updateForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromUpdateForm() {

        if (updateForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            courseManagerService
                    .updateCourse(course, course.getName(), course.getSummary(), course.getDescription(), 1);
        } catch (RestClientFailureException e) {
            updateForm.recordError("Internal error on server.");
            updateForm.recordError(e.getMessage());
        } catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "updateForm" bubbles up SUCCESS or FAILURE when it is
    // submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromUpdateForm() {
        // We want to tell our containing page explicitly what course we've
        // updated, so we trigger new event
        // "successfulUpdate" with a parameter. It will bubble up because we
        // don't have a handler method for it.
        componentResources.triggerEvent(SUCCESSFUL_UPDATE, new Object[] { courseUuid }, null);

        // We don't want "success" to bubble up, so we return true to say we've
        // handled it.
        mode = Mode.REVIEW;
        return true;
    }

    boolean onFailureFromUpdateForm() {
        versionFlash = course.getVersion();

        // Rather than letting "failure" bubble up which doesn't say what you
        // were trying to do, we trigger new event
        // "failedUpdate". It will bubble up because we don't have a handler
        // method for it.
        componentResources.triggerEvent(FAILED_UPDATE, new Object[] { courseUuid }, null);
        // We don't want "failure" to bubble up, so we return true to say we've
        // handled it.
        return true;
    }
}

DELETE template and java

The editor doesn’t have an explicit “delete” mode but it does support deleting the current object on the review and update pages.

// /////////////////////////////////////////////////////////////////////
    // DELETE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "delete"

    Object onDelete(String courseUuid) {
        this.courseUuid = courseUuid;
        int courseVersion = 0;

        try {
            courseManagerService.deleteCourse(courseUuid, courseVersion);
        } catch (ObjectNotFoundException e) {
            // the object's already deleted
        } catch (RestClientFailureException e) {
            createForm.recordError("Internal error on server.");
            createForm.recordError(e.getMessage());

            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            deleteMessage = ExceptionUtil.getRootCauseMessage(e);

            // Trigger new event "failedDelete" which will bubble up.
            componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
            // We don't want "delete" to bubble up, so we return true to say
            // we've handled it.
            return true;
        } catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            deleteMessage = ExceptionUtil.getRootCauseMessage(e);

            // Trigger new event "failedDelete" which will bubble up.
            componentResources.triggerEvent(FAILED_DELETE, new Object[] { courseUuid }, null);
            // We don't want "delete" to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

        // Trigger new event "successfulDelete" which will bubble up.
        componentResources.triggerEvent(SUCCESFUL_DELETE, new Object[] { courseUuid }, null);
        // We don't want "delete" to bubble up, so we return true to say we've
        // handled it.
        return indexPage;
    }

Next Steps

The obvious next steps are improving the error messages, adding support for pagination, support, and one-to-many and many-to-many relationships. All will require revising the REST payloads. I have a few additional items in the pipeline, e.g., an ExceptionService, to say nothing of the security issues.

Source Code

 

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button