Invariant Properties

  • rss
  • Home

Project Student: Maintenance Webapp (read-only)

Bear Giles | December 30, 2013

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 and JPA Criteria Queries.

When I started this project I had four goals. In no particular order they were to:

  • learn about jQuery and other AJAX technologies. For that I needed a REST server I understood,
  • capture recently acquired knowledge about jersey and tapestry,
  • create a framework I could use to learn about other technologies (e.g., spring MVC, restlet, netty), and
  • have something to discuss in job interviews

If it was useful to others – great! That’s why it’s available under the Apache license.

(It should go without saying that acceptable uses does not include turning “Project Student” into a student project without proper attribution!)

The problem with learning AJAX is that I’ll initially be uncertain where the problem lies. Is it bad jQuery? A bad REST service? Something else? The extensive unit and integration tests are a good start but there will always be some uncertainty.

The other consideration is that in the real world we’ll often need a basic view into the database. It won’t be for public consumption – it’s for internal use when we hit a WTF moment. It can also be used to maintain information that we don’t want to manage via a public interface, e.g., values in pulldown menus.

A small twist on this can be used to provide a modest level of scalability. Use hefty servers for your database and REST service, then have N front-end servers that run conventional webapps that act as an intermediary between the user and the REST service. The front-end servers can be fairly lightweight and spun up on an as-needed basis. Bonus points for putting a caching server between the front ends and the REST server since the overwhelming fraction of hits will be reads.

This approach won’t scale to Amazon or Facebook scales but it will be good enough for many sites.

Maintenance Webapp

This brings us to the optional layers of the webapp onion – a conventional webapp that acts as a frontend to the REST service. For various reasons I’m using Tapestry 5 for the application but it’s an arbitrary decision and I won’t spend much time delving into Tapestry-specific code.

You can create a new tapestry project with

  1. $ mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org
$ mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org

I’ve found the examples at http://jumpstart.doublenegative.com.au/jumpstart/examples/ invaluable. I’ve kept in attribution where appropriate.

Later I will also create the second optional layer of the webapp – functional and regression tests with Selenium and WebDriver (that is, Selenium 2.0).

Limitations

Read-only – spinning up the webapp takes a lot of work so the initial version will only provide read-only access of simple tables. No updates, no one-to-many mappings.

User Authentication – no effort has been made to authenticate users.

Encryption – no effort has been made to encrypt communications.

Database Locks – we’re using opportunistic locking with hibernate versions instead of explicit database locks. Security note: under the principle of least disclosure we don’t want to make the version visible unless it’s required. A good rule is that you’ll see if it you request a specific object but not see it in lists.

REST Client – the default GET handler for each type is very crude – it just returns a list of objects. We need a more sophisticated response (e.g., the number of records, the start- and end-index, a status code, etc.) and will let the UI drive it. For now the only thing we really need is a count and we can just request the list and count the number of elements.

Goal

We want a page that lists all courses in the database. It does not need to worry about pagination, sorting, etc. It should have links (possibly inactive) for editing and deleting a record. It does not need to have a link to add a new course.

The page should look something like this:

project-maintenance

Course template

The Tapestry page listing courses is straightforward – it’s basically just a decorated grid of values.

(See the tapestry archetype for Layout.tml, etc.)

  1. <html t:type="layout" title="Course List"
  2.      t:sidebarTitle="Framework Version"
  3.      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
  4.      xmlns:p="tapestry:parameter">
  5.         <!-- Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml -->
  6.  
  7.     <t:zone t:id="zone">  
  8.         <p>
  9.             "Course" page
  10.         </p>
  11.    
  12.         <t:grid source="courses" row="course" include="uuid,name,creationdate" add="edit,delete">
  13.             <p:name>
  14.                 <t:pagelink page="CourseEditor" context="course.uuid">${course.name}</t:pagelink>
  15.             </p:name>
  16.             <p:editcell>
  17.                 <t:actionlink t:id="edit" context="course.uuid">Edit</t:actionlink>
  18.             </p:editcell>
  19.             <p:deletecell>
  20.                 <t:actionlink t:id="delete" context="course.uuid">Delete</t:actionlink>
  21.             </p:deletecell>
  22.             <p:empty>
  23.               <p>There are no courses to display; you can <t:pagelink page="Course/Editor" parameters="{ 'mode':'create', 'courseUuid':null }">add some</t:pagelink>.</p>
  24.             </p:empty>
  25.         </t:grid>
  26.     </t:zone>
  27.  
  28.     <p:sidebar>
  29.         <p>
  30.             [
  31.             <t:pagelink page="Index">Index</t:pagelink>
  32.             ]<br/>
  33.             [
  34.             <t:pagelink page="Course/List">Courses</t:pagelink>
  35.             ]
  36.         </p>
  37.     </p:sidebar>
  38. </html>
<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">
        <!-- Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml -->

    <t:zone t:id="zone">   
        <p>
            "Course" page
        </p>
   
        <t:grid source="courses" row="course" include="uuid,name,creationdate" add="edit,delete">
            <p:name>
                <t:pagelink page="CourseEditor" context="course.uuid">${course.name}</t:pagelink>
            </p:name>
            <p:editcell>
                <t:actionlink t:id="edit" context="course.uuid">Edit</t:actionlink>
            </p:editcell>
            <p:deletecell>
                <t:actionlink t:id="delete" context="course.uuid">Delete</t:actionlink>
            </p:deletecell>
            <p:empty>
              <p>There are no courses to display; you can <t:pagelink page="Course/Editor" parameters="{ 'mode':'create', 'courseUuid':null }">add some</t:pagelink>.</p>
            </p:empty>
        </t:grid>
    </t:zone>

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

with a properties file of

  1. title=Courses
  2. delete-course=Delete course?
title=Courses
delete-course=Delete course?

I’ve included the actionlinks for edit and delete but they’re nonfunctional.

GridDataSources

Our page needs a source for the values to display. This requires two classes. The first defines the courses property used above.

  1. package com.invariantproperties.sandbox.student.maintenance.web.tables;
  2.  
  3. import com.invariantproperties.sandbox.student.business.CourseFinderService;
  4.  
  5. public class GridDataSources {
  6.     // Screen fields
  7.  
  8.     @Property
  9.     private GridDataSource courses;
  10.  
  11.     // Generally useful bits and pieces
  12.  
  13.     @Inject
  14.     private CourseFinderService courseFinderService;
  15.  
  16.     @InjectComponent
  17.     private Grid grid;
  18.  
  19.     // The code
  20.  
  21.     void setupRender() {
  22.         courses = new CoursePagedDataSource(courseFinderService);
  23.     }
  24. }
package com.invariantproperties.sandbox.student.maintenance.web.tables;

import com.invariantproperties.sandbox.student.business.CourseFinderService;

public class GridDataSources {
    // Screen fields

    @Property
    private GridDataSource courses;

    // Generally useful bits and pieces

    @Inject
    private CourseFinderService courseFinderService;

    @InjectComponent
    private Grid grid;

    // The code

    void setupRender() {
        courses = new CoursePagedDataSource(courseFinderService);
    }
}

and the actual implementation is

  1. package com.invariantproperties.sandbox.student.maintenance.web.tables;
  2.  
  3. import com.invariantproperties.sandbox.student.business.CourseFinderService;
  4. import com.invariantproperties.sandbox.student.domain.Course;
  5. import com.invariantproperties.sandbox.student.maintenance.query.SortCriterion;
  6. import com.invariantproperties.sandbox.student.maintenance.query.SortDirection;
  7.  
  8. public class CoursePagedDataSource implements GridDataSource {
  9.  
  10.     private int startIndex;
  11.     private List<Course> preparedResults;
  12.  
  13.     private final CourseFinderService courseFinderService;
  14.  
  15.     public CoursePagedDataSource(CourseFinderService courseFinderService) {
  16.         this.courseFinderService = courseFinderService;
  17.     }
  18.  
  19.     @Override
  20.     public int getAvailableRows() {
  21.         long count = courseFinderService.count();
  22.         return (int) count;
  23.     }
  24.  
  25.     @Override
  26.     public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
  27.  
  28.         // Get a page of courses - ask business service to find them (from the
  29.         // database)
  30.         // List<SortCriterion> sortCriteria = toSortCriteria(sortConstraints);
  31.         // preparedResults = courseFinderService.findCourses(startIndex,
  32.         // endIndex - startIndex + 1, sortCriteria);
  33.         preparedResults = courseFinderService.findAllCourses();
  34.  
  35.         this.startIndex = startIndex;
  36.     }
  37.  
  38.     @Override
  39.     public Object getRowValue(final int index) {
  40.         return preparedResults.get(index - startIndex);
  41.     }
  42.  
  43.     @Override
  44.     public Class<Course> getRowType() {
  45.         return Course.class;
  46.     }
  47.  
  48.     /**
  49.      * Converts a list of Tapestry's SortConstraint to a list of our business
  50.      * tier's SortCriterion. The business tier does not use SortConstraint
  51.      * because that would create a dependency on Tapestry.
  52.      */
  53.     private List<SortCriterion> toSortCriteria(List<SortConstraint> sortConstraints) {
  54.         List<SortCriterion> sortCriteria = new ArrayList<>();
  55.  
  56.         for (SortConstraint sortConstraint : sortConstraints) {
  57.  
  58.             String propertyName = sortConstraint.getPropertyModel().getPropertyName();
  59.             SortDirection sortDirection = SortDirection.UNSORTED;
  60.  
  61.             switch (sortConstraint.getColumnSort()) {
  62.             case ASCENDING:
  63.                 sortDirection = SortDirection.ASCENDING;
  64.                 break;
  65.             case DESCENDING:
  66.                 sortDirection = SortDirection.DESCENDING;
  67.                 break;
  68.             default:
  69.             }
  70.  
  71.             SortCriterion sortCriterion = new SortCriterion(propertyName, sortDirection);
  72.             sortCriteria.add(sortCriterion);
  73.         }
  74.  
  75.         return sortCriteria;
  76.     }
  77. }
package com.invariantproperties.sandbox.student.maintenance.web.tables;

import com.invariantproperties.sandbox.student.business.CourseFinderService;
import com.invariantproperties.sandbox.student.domain.Course;
import com.invariantproperties.sandbox.student.maintenance.query.SortCriterion;
import com.invariantproperties.sandbox.student.maintenance.query.SortDirection;

public class CoursePagedDataSource implements GridDataSource {

    private int startIndex;
    private List<Course> preparedResults;

    private final CourseFinderService courseFinderService;

    public CoursePagedDataSource(CourseFinderService courseFinderService) {
        this.courseFinderService = courseFinderService;
    }

    @Override
    public int getAvailableRows() {
        long count = courseFinderService.count();
        return (int) count;
    }

    @Override
    public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {

        // Get a page of courses - ask business service to find them (from the
        // database)
        // List<SortCriterion> sortCriteria = toSortCriteria(sortConstraints);
        // preparedResults = courseFinderService.findCourses(startIndex,
        // endIndex - startIndex + 1, sortCriteria);
        preparedResults = courseFinderService.findAllCourses();

        this.startIndex = startIndex;
    }

    @Override
    public Object getRowValue(final int index) {
        return preparedResults.get(index - startIndex);
    }

    @Override
    public Class<Course> getRowType() {
        return Course.class;
    }

    /**
     * Converts a list of Tapestry's SortConstraint to a list of our business
     * tier's SortCriterion. The business tier does not use SortConstraint
     * because that would create a dependency on Tapestry.
     */
    private List<SortCriterion> toSortCriteria(List<SortConstraint> sortConstraints) {
        List<SortCriterion> sortCriteria = new ArrayList<>();

        for (SortConstraint sortConstraint : sortConstraints) {

            String propertyName = sortConstraint.getPropertyModel().getPropertyName();
            SortDirection sortDirection = SortDirection.UNSORTED;

            switch (sortConstraint.getColumnSort()) {
            case ASCENDING:
                sortDirection = SortDirection.ASCENDING;
                break;
            case DESCENDING:
                sortDirection = SortDirection.DESCENDING;
                break;
            default:
            }

            SortCriterion sortCriterion = new SortCriterion(propertyName, sortDirection);
            sortCriteria.add(sortCriterion);
        }

        return sortCriteria;
    }
}

AppModule

Now that we have a GridDataSource we can see what it needs – a CourseFinderService. While there is a Tapestry-Spring integration we want to keep the maintenance webapp as thin as possible so for now we use standard Tapestry injection.

  1. package com.invariantproperties.sandbox.student.maintenance.web.services;
  2.  
  3. import com.invariantproperties.sandbox.student.business.CourseFinderService;
  4. import com.invariantproperties.sandbox.student.business.CourseManagerService;
  5. import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseFinderServiceTapestryImpl;
  6. import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseManagerServiceTapestryImpl;
  7.  
  8. /**
  9.  * This module is automatically included as part of the Tapestry IoC Registry,
  10.  * it's a good place to configure and extend Tapestry, or to place your own
  11.  * service definitions.
  12.  */
  13. public class AppModule {
  14.     public static void bind(ServiceBinder binder) {
  15.         binder.bind(CourseFinderService.class, CourseFinderServiceTapestryImpl.class);
  16.         binder.bind(CourseManagerService.class, CourseManagerServiceTapestryImpl.class);
  17.     }
  18.  
  19.     ....
  20. }
package com.invariantproperties.sandbox.student.maintenance.web.services;

import com.invariantproperties.sandbox.student.business.CourseFinderService;
import com.invariantproperties.sandbox.student.business.CourseManagerService;
import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseFinderServiceTapestryImpl;
import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseManagerServiceTapestryImpl;

/**
 * This module is automatically included as part of the Tapestry IoC Registry,
 * it's a good place to configure and extend Tapestry, or to place your own
 * service definitions.
 */
public class AppModule {
    public static void bind(ServiceBinder binder) {
        binder.bind(CourseFinderService.class, CourseFinderServiceTapestryImpl.class);
        binder.bind(CourseManagerService.class, CourseManagerServiceTapestryImpl.class);
    }

    ....
}

Note that we’re using the standard CourseFinderService interface with the tapestry-specific implementation. This means we can use the standard implementation directly with nothing more than a small change to the configuration files!

CourseFinderServiceTapestryImpl

The local implementation of the CourseFinderService interface must use the REST client instead of the Spring Data implementation. Using the outside-in approach used earlier the needs of the Tapestry template should drive the needs of the Service implementation and that, in turn, drives the needs of the REST client and server.

  1. package com.invariantproperties.sandbox.student.maintenance.service.impl;
  2.  
  3. public class CourseFinderServiceTapestryImpl implements CourseFinderService {
  4.     private final CourseFinderRestClient finder;
  5.  
  6.     public CourseFinderServiceTapestryImpl() {
  7.         // resource should be loaded as tapestry resource
  8.         final String resource = "http://localhost:8080/student-ws-webapp/rest/course/";
  9.         finder = new CourseFinderRestClientImpl(resource);
  10.  
  11.         // load some initial data
  12.         initCache(new CourseManagerRestClientImpl(resource));
  13.     }
  14.  
  15.     @Override
  16.     public long count() {
  17.         // FIXME: grossly inefficient but good enough for now.
  18.         return finder.getAllCourses().length;
  19.     }
  20.  
  21.     @Override
  22.     public long countByTestRun(TestRun testRun) {
  23.         // FIXME: grossly inefficient but good enough for now.
  24.         return finder.getAllCourses().length;
  25.     }
  26.  
  27.     @Override
  28.     public Course findCourseById(Integer id) {
  29.         // unsupported operation!
  30.         throw new ObjectNotFoundException(id);
  31.     }
  32.  
  33.     @Override
  34.     public Course findCourseByUuid(String uuid) {
  35.         return finder.getCourse(uuid);
  36.     }
  37.  
  38.     @Override
  39.     public List<Course> findAllCourses() {
  40.         return Arrays.asList(finder.getAllCourses());
  41.     }
  42.  
  43.     @Override
  44.     public List<Course> findCoursesByTestRun(TestRun testRun) {
  45.         return Collections.emptyList();
  46.     }
  47.  
  48.     // method to load some test data into the database.
  49.     private void initCache(CourseManagerRestClient manager) {
  50.         manager.createCourse("physics 101");
  51.         manager.createCourse("physics 201");
  52.         manager.createCourse("physics 202");
  53.     }
  54. }
package com.invariantproperties.sandbox.student.maintenance.service.impl;

public class CourseFinderServiceTapestryImpl implements CourseFinderService {
    private final CourseFinderRestClient finder;

    public CourseFinderServiceTapestryImpl() {
        // resource should be loaded as tapestry resource
        final String resource = "http://localhost:8080/student-ws-webapp/rest/course/";
        finder = new CourseFinderRestClientImpl(resource);

        // load some initial data
        initCache(new CourseManagerRestClientImpl(resource));
    }

    @Override
    public long count() {
        // FIXME: grossly inefficient but good enough for now.
        return finder.getAllCourses().length;
    }

    @Override
    public long countByTestRun(TestRun testRun) {
        // FIXME: grossly inefficient but good enough for now.
        return finder.getAllCourses().length;
    }

    @Override
    public Course findCourseById(Integer id) {
        // unsupported operation!
        throw new ObjectNotFoundException(id);
    }

    @Override
    public Course findCourseByUuid(String uuid) {
        return finder.getCourse(uuid);
    }

    @Override
    public List<Course> findAllCourses() {
        return Arrays.asList(finder.getAllCourses());
    }

    @Override
    public List<Course> findCoursesByTestRun(TestRun testRun) {
        return Collections.emptyList();
    }

    // method to load some test data into the database.
    private void initCache(CourseManagerRestClient manager) {
        manager.createCourse("physics 101");
        manager.createCourse("physics 201");
        manager.createCourse("physics 202");
    }
}

Our JPA Criteria query can give us a quick count but our REST client doesn’t support that yet.

Wrapping up

After we finish the grunt work we’ll have a maintenance .war file. We can deploy it with the webservice .war on our appserver – or not. There’s no reason the two .war files have to be on the same system other than the temporarily hardcoded URL for the webservice.

We should first go to http://localhost:8080/student-maintenance-webapp/course/list. We should see a short list of courses as shown above. (In that case I had restarted the webapp three times so each entry is duplicated three-fold.)

Now we should go to our webservice webapp at http://localhost:8080/student-ws-webapp/rest/course and verify that we can get data via the browser there as well. After a bit of cleanup we should see:

  1. {"course":
  2.  [
  3.    {
  4.      "creationDate":"2013-12-28T14:40:21.369-07:00",
  5.      "uuid":"500069e4-444d-49bc-80f0-4894c2d13f6a",
  6.      "version":"0",
  7.      "name":"physics 101"
  8.    },
  9.    {
  10.      "creationDate":"2013-12-28T14:40:21.777-07:00",
  11.      "uuid":"54001b2a-abbb-4a75-a289-e1f09173fa04",
  12.      "version":"0",
  13.      "name":"physics 201"
  14.    },
  15.    {
  16.      "creationDate":"2013-12-28T14:40:21.938-07:00",
  17.      "uuid":"cfaf892b-7ead-4d64-8659-8f87756bed62",
  18.      "version":"0",
  19.      "name":"physics 202"
  20.    },
  21.    {
  22.      "creationDate":"2013-12-28T16:17:54.608-07:00",
  23.      "uuid":"d29735ff-f614-4979-a0de-e1d134e859f4",
  24.      "version":"0",
  25.      "name":"physics 101"
  26.    },
  27.    ....
  28.  ]
  29. }
{"course":
 [
   {
     "creationDate":"2013-12-28T14:40:21.369-07:00",
     "uuid":"500069e4-444d-49bc-80f0-4894c2d13f6a",
     "version":"0",
     "name":"physics 101"
   },
   {
     "creationDate":"2013-12-28T14:40:21.777-07:00",
     "uuid":"54001b2a-abbb-4a75-a289-e1f09173fa04",
     "version":"0",
     "name":"physics 201"
   },
   {
     "creationDate":"2013-12-28T14:40:21.938-07:00",
     "uuid":"cfaf892b-7ead-4d64-8659-8f87756bed62",
     "version":"0",
     "name":"physics 202"
   },
   {
     "creationDate":"2013-12-28T16:17:54.608-07:00",
     "uuid":"d29735ff-f614-4979-a0de-e1d134e859f4",
     "version":"0",
     "name":"physics 101"
   },
   ....
 ]
}

Source Code

The source code is at https://github.com/beargiles/project-student [github] and http://beargiles.github.io/project-student/ [github pages].

Categories
java
Comments rss
Comments rss
Trackback
Trackback

« Project Student: JPA Criteria Queries Project Student: Maintenance Webapp (editable) »

Leave a Reply

Click here to cancel reply.

You must be logged in to post a comment.

Archives

  • May 2020 (1)
  • March 2019 (1)
  • August 2018 (1)
  • May 2018 (1)
  • February 2018 (1)
  • November 2017 (4)
  • January 2017 (3)
  • June 2016 (1)
  • May 2016 (1)
  • April 2016 (2)
  • March 2016 (1)
  • February 2016 (3)
  • January 2016 (6)
  • December 2015 (2)
  • November 2015 (3)
  • October 2015 (2)
  • August 2015 (4)
  • July 2015 (2)
  • June 2015 (2)
  • January 2015 (1)
  • December 2014 (6)
  • October 2014 (1)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (2)
  • May 2014 (2)
  • April 2014 (1)
  • March 2014 (1)
  • February 2014 (3)
  • January 2014 (6)
  • December 2013 (13)
  • November 2013 (6)
  • October 2013 (3)
  • September 2013 (2)
  • August 2013 (5)
  • June 2013 (1)
  • May 2013 (2)
  • March 2013 (1)
  • November 2012 (1)
  • October 2012 (3)
  • September 2012 (2)
  • May 2012 (6)
  • January 2012 (2)
  • December 2011 (12)
  • July 2011 (1)
  • June 2011 (2)
  • May 2011 (5)
  • April 2011 (6)
  • March 2011 (4)
  • February 2011 (3)
  • October 2010 (6)
  • September 2010 (8)

Recent Posts

  • 8-bit Breadboard Computer: Good Encapsulation!
  • Where are all the posts?
  • Better Ad Blocking Through Pi-Hole and Local Caching
  • The difference between APIs and SPIs
  • Hadoop: User Impersonation with Kerberos Authentication

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org

Pages

  • About Me
  • Notebook: Common XML Tasks
  • Notebook: Database/Webapp Security
  • Notebook: Development Tips

Syndication

Java Code Geeks

Know Your Rights

Support Bloggers' Rights
Demand Your dotRIGHTS

Security

  • Dark Reading
  • Krebs On Security Krebs On Security
  • Naked Security Naked Security
  • Schneier on Security Schneier on Security
  • TaoSecurity TaoSecurity

Politics

  • ACLU ACLU
  • EFF EFF

News

  • Ars technica Ars technica
  • Kevin Drum at Mother Jones Kevin Drum at Mother Jones
  • Raw Story Raw Story
  • Tech Dirt Tech Dirt
  • Vice Vice

Spam Blocked

53,313 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox