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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: JPA Criteria Queries

Bear Giles | December 29, 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 and Webservice Integration.

We’ve covered basic CRUD operations but that doesn’t take us very far. Spring Data makes it easy to include basic searches but it’s important to have other standard options. One of the most important is JPA criteria queries.

A good introduction to this material is Spring Data JPA tutorial – JPA Criteria Queries [http://www.petrikainulainen.net].

Design Decisions

JPA criteria – I’m using JPA criteria searches instead of querydsl. I’ll come back to querydsl later.

Limitations

Violation of Encapsulation – this design requires breaking the architectural goal of making each layer completely unaware of the implementation details of the other layers. This is a very small violation – only the JPA specifications – and we still have to deal with pagination. Put those together and I feel it’s premature optimization to worry about this too much at this time.

Webservices – the webservice client and server are not updated. Again we still have to deal with pagination and anything we do now will have to be changed anyway.

Refactoring Prior Work

I have three changes to prior work.

findCoursesByTestRun()

I had defined the method

  1. List findCoursesByTestRun(TestRun testRun);
List findCoursesByTestRun(TestRun testRun);

in the Course repository. That doesn’t have the intended effect. What I needed was

  1. List findCoursesByTestRun_Uuid(String uuid);
List findCoursesByTestRun_Uuid(String uuid);

with the appropriate changes to the calling code. Or you can just use the JPA criteria query discussed below.

FinderService and ManagerService

This comes from research on the Jumpstart site. The author splits the standard service interface into two pieces:

  • FinderService – read-only operations (searches)
  • ManagerService – read-write operations (create, update, delete)

This makes a lot of sense, e.g., it will be a lot easier to add behavior via AOP when we can operate at the class vs. method level. I’ve made the appropriate changes in the existing code.

FindBugs

I’ve fixed a number of issues identified by FindBugs.

Metadata

We begin by enabling access to metadata access to our persistent objects. This allows us to create queries that can be optimized by the JPA implementation.

  1. import javax.persistence.metamodel.SingularAttribute;
  2. import javax.persistence.metamodel.StaticMetamodel;
  3.  
  4. @StaticMetamodel(Course.class)
  5. public class Course_ {
  6.     public static volatile SingularAttribute<Course, TestRun> testRun;
  7. }
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@StaticMetamodel(Course.class)
public class Course_ {
    public static volatile SingularAttribute<Course, TestRun> testRun;
}

and

  1. @StaticMetamodel(TestRun.class)
  2. public class TestRun_ {
  3.     public static volatile SingularAttribute<TestRun, String> uuid;
  4. }
@StaticMetamodel(TestRun.class)
public class TestRun_ {
    public static volatile SingularAttribute<TestRun, String> uuid;
}

The name of the class is required due to convention over configuration.

For a discussion of this feature see Static metadata [jboss.org].

Specifications

We can now create the query specification using the metadata now available. This is a slightly more complex query since we need to drill into the structure. (This shouldn’t require an actual join since the testrun uuid is used as the foreign key.)

  1. public class CourseSpecifications {
  2.  
  3.     /**
  4.      * Creates a specification used to find courses with the specified testUuid.
  5.      *
  6.      * @param testRun
  7.      * @return
  8.      */
  9.     public static Specification<Course> testRunIs(final TestRun testRun) {
  10.  
  11.         return new Specification<Course>() {
  12.             @Override
  13.             public Predicate toPredicate(Root<Course> courseRoot, CriteriaQuery<?> query, CriteriaBuilder cb) {
  14.                 Predicate p = null;
  15.                 if (testRun == null || testRun.getUuid() == null) {
  16.                     p = cb.isNull(courseRoot.<Course_> get("testRun"));
  17.                 } else {
  18.                     p = cb.equal(courseRoot.<Course_> get("testRun").<TestRun_> get("uuid"), testRun.getUuid());
  19.                 }
  20.                 return p;
  21.             }
  22.         };
  23.     }
  24. }
public class CourseSpecifications {

    /**
     * Creates a specification used to find courses with the specified testUuid.
     * 
     * @param testRun
     * @return
     */
    public static Specification<Course> testRunIs(final TestRun testRun) {

        return new Specification<Course>() {
            @Override
            public Predicate toPredicate(Root<Course> courseRoot, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate p = null;
                if (testRun == null || testRun.getUuid() == null) {
                    p = cb.isNull(courseRoot.<Course_> get("testRun"));
                } else {
                    p = cb.equal(courseRoot.<Course_> get("testRun").<TestRun_> get("uuid"), testRun.getUuid());
                }
                return p;
            }
        };
    }
}

Some documentation suggests I could use get(Course_.testRun) instead of get(“testRun”) but eclipse was flagging it as a type violation on the get() method. Your mileage may vary.

Spring Data Repository

We must tell Spring Data that we’re using JPA Criteria queries. This is done by extending the JpaSpecificationExecutor interface.

  1. @Repository
  2. public interface CourseRepository extends JpaRepository<Course, Integer>,
  3.         JpaSpecificationExecutor<Course> {
  4.    
  5.     Course findCourseByUuid(String uuid);
  6.  
  7.     List findCoursesByTestRunUuid(String uuid);
  8. }
@Repository
public interface CourseRepository extends JpaRepository<Course, Integer>,
        JpaSpecificationExecutor<Course> {
    
    Course findCourseByUuid(String uuid);

    List findCoursesByTestRunUuid(String uuid);
}

FinderService Implementation

We can now use the JPA specification in our service implementations. As mentioned above using the JPA Criteria Specification violates encapsulation.

  1. import static com.invariantproperties.sandbox.student.specification.CourseSpecifications.testRunIs;
  2.  
  3. @Service
  4. public class CourseFinderServiceImpl implements CourseFinderService {
  5.     @Resource
  6.     private CourseRepository courseRepository;
  7.  
  8.     /**
  9.      * @see com.invariantproperties.sandbox.student.business.FinderService#
  10.      *      count()
  11.      */
  12.     @Transactional(readOnly = true)
  13.     @Override
  14.     public long count() {
  15.         return countByTestRun(null);
  16.     }
  17.  
  18.     /**
  19.      * @see com.invariantproperties.sandbox.student.business.FinderService#
  20.      *      countByTestRun(com.invariantproperties.sandbox.student.domain.TestRun)
  21.      */
  22.     @Transactional(readOnly = true)
  23.     @Override
  24.     public long countByTestRun(TestRun testRun) {
  25.         long count = 0;
  26.         try {
  27.             count = courseRepository.count(testRunIs(testRun));
  28.         } catch (DataAccessException e) {
  29.             if (!(e instanceof UnitTestException)) {
  30.                 log.info("internal error retrieving classroom count by " + testRun, e);
  31.             }
  32.             throw new PersistenceException("unable to count classrooms by " + testRun, e, 0);
  33.         }
  34.  
  35.         return count;
  36.     }
  37.  
  38.     /**
  39.      * @see com.invariantproperties.sandbox.student.business.CourseFinderService#
  40.      *      findAllCourses()
  41.      */
  42.     @Transactional(readOnly = true)
  43.     @Override
  44.     public List<Course> findAllCourses() {
  45.         return findCoursesByTestRun(null);
  46.     }
  47.  
  48.     /**
  49.      * @see com.invariantproperties.sandbox.student.business.CourseFinderService#
  50.      *      findCoursesByTestRun(java.lang.String)
  51.      */
  52.     @Transactional(readOnly = true)
  53.     @Override
  54.     public List<Course> findCoursesByTestRun(TestRun testRun) {
  55.         List<Course> courses = null;
  56.  
  57.         try {
  58.             courses = courseRepository.findAll(testRunIs(testRun));
  59.         } catch (DataAccessException e) {
  60.             if (!(e instanceof UnitTestException)) {
  61.                 log.info("error loading list of courses: " + e.getMessage(), e);
  62.             }
  63.             throw new PersistenceException("unable to get list of courses.", e);
  64.         }
  65.  
  66.         return courses;
  67.     }
  68.  
  69.     ....
  70. }
import static com.invariantproperties.sandbox.student.specification.CourseSpecifications.testRunIs;

@Service
public class CourseFinderServiceImpl implements CourseFinderService {
    @Resource
    private CourseRepository courseRepository;

    /**
     * @see com.invariantproperties.sandbox.student.business.FinderService#
     *      count()
     */
    @Transactional(readOnly = true)
    @Override
    public long count() {
        return countByTestRun(null);
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.FinderService#
     *      countByTestRun(com.invariantproperties.sandbox.student.domain.TestRun)
     */
    @Transactional(readOnly = true)
    @Override
    public long countByTestRun(TestRun testRun) {
        long count = 0;
        try {
            count = courseRepository.count(testRunIs(testRun));
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving classroom count by " + testRun, e);
            }
            throw new PersistenceException("unable to count classrooms by " + testRun, e, 0);
        }

        return count;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseFinderService#
     *      findAllCourses()
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCourses() {
        return findCoursesByTestRun(null);
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseFinderService#
     *      findCoursesByTestRun(java.lang.String)
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findCoursesByTestRun(TestRun testRun) {
        List<Course> courses = null;

        try {
            courses = courseRepository.findAll(testRunIs(testRun));
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }

        return courses;
    }

    ....
}

Unit Testing

Our unit tests require a small change to use a Specification.

  1. public class CourseFinderServiceImplTest {
  2.     private final Class<Specification<Course>> sClass = null;
  3.  
  4.     @Test
  5.     public void testCount() {
  6.         final long expected = 3;
  7.  
  8.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  9.         when(repository.count(any(sClass))).thenReturn(expected);
  10.  
  11.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  12.         final long actual = service.count();
  13.  
  14.         assertEquals(expected, actual);
  15.     }
  16.  
  17.     @Test
  18.     public void testCountByTestRun() {
  19.         final long expected = 3;
  20.         final TestRun testRun = new TestRun();
  21.  
  22.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  23.         when(repository.count(any(sClass))).thenReturn(expected);
  24.  
  25.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  26.         final long actual = service.countByTestRun(testRun);
  27.  
  28.         assertEquals(expected, actual);
  29.     }
  30.  
  31.     @Test(expected = PersistenceException.class)
  32.     public void testCountError() {
  33.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  34.         when(repository.count(any(sClass))).thenThrow(new UnitTestException());
  35.  
  36.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  37.         service.count();
  38.     }
  39.  
  40.     @Test
  41.     public void testFindAllCourses() {
  42.         final List<Course> expected = Collections.emptyList();
  43.  
  44.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  45.         when(repository.findAll(any(sClass))).thenReturn(expected);
  46.  
  47.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  48.         final List<Course> actual = service.findAllCourses();
  49.  
  50.         assertEquals(expected, actual);
  51.     }
  52.  
  53.     @Test(expected = PersistenceException.class)
  54.     public void testFindAllCoursesError() {
  55.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  56.         final Class<Specification<Course>> sClass = null;
  57.         when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());
  58.  
  59.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  60.         service.findAllCourses();
  61.     }
  62.  
  63.     @Test
  64.     public void testFindCourseByTestUuid() {
  65.         final TestRun testRun = new TestRun();
  66.         final Course course = new Course();
  67.         final List<Course> expected = Collections.singletonList(course);
  68.  
  69.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  70.         when(repository.findAll(any(sClass))).thenReturn(expected);
  71.  
  72.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  73.         final List actual = service.findCoursesByTestRun(testRun);
  74.  
  75.         assertEquals(expected, actual);
  76.     }
  77.  
  78.     @Test(expected = PersistenceException.class)
  79.     public void testFindCourseByTestUuidError() {
  80.         final TestRun testRun = new TestRun();
  81.  
  82.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  83.         when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());
  84.  
  85.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  86.         service.findCoursesByTestRun(testRun);
  87.     }
  88.  
  89.     @Test
  90.     public void testFindCoursesByTestUuid() {
  91.         final TestRun testRun = new TestRun();
  92.         final Course course = new Course();
  93.         final List<Course> expected = Collections.singletonList(course);
  94.  
  95.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  96.         when(repository.findAll(any(sClass))).thenReturn(expected);
  97.  
  98.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  99.         final List<Course> actual = service.findCoursesByTestRun(testRun);
  100.  
  101.         assertEquals(expected, actual);
  102.     }
  103.  
  104.     @Test(expected = PersistenceException.class)
  105.     public void testFindCoursesByTestUuidError() {
  106.         final TestRun testRun = new TestRun();
  107.  
  108.         final CourseRepository repository = Mockito.mock(CourseRepository.class);
  109.         when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());
  110.  
  111.         final CourseFinderService service = new CourseFinderServiceImpl(repository);
  112.         service.findCoursesByTestRun(testRun);
  113.     }
  114.  
  115.     ....
  116. }
public class CourseFinderServiceImplTest {
    private final Class<Specification<Course>> sClass = null;

    @Test
    public void testCount() {
        final long expected = 3;

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.count(any(sClass))).thenReturn(expected);

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        final long actual = service.count();

        assertEquals(expected, actual);
    }

    @Test
    public void testCountByTestRun() {
        final long expected = 3;
        final TestRun testRun = new TestRun();

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.count(any(sClass))).thenReturn(expected);

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        final long actual = service.countByTestRun(testRun);

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testCountError() {
        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.count(any(sClass))).thenThrow(new UnitTestException());

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        service.count();
    }

    @Test
    public void testFindAllCourses() {
        final List<Course> expected = Collections.emptyList();

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.findAll(any(sClass))).thenReturn(expected);

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        final List<Course> actual = service.findAllCourses();

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testFindAllCoursesError() {
        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        final Class<Specification<Course>> sClass = null;
        when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        service.findAllCourses();
    }
 
    @Test
    public void testFindCourseByTestUuid() {
        final TestRun testRun = new TestRun();
        final Course course = new Course();
        final List<Course> expected = Collections.singletonList(course);

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.findAll(any(sClass))).thenReturn(expected);

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        final List actual = service.findCoursesByTestRun(testRun);

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testFindCourseByTestUuidError() {
        final TestRun testRun = new TestRun();

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        service.findCoursesByTestRun(testRun);
    }

    @Test
    public void testFindCoursesByTestUuid() {
        final TestRun testRun = new TestRun();
        final Course course = new Course();
        final List<Course> expected = Collections.singletonList(course);

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.findAll(any(sClass))).thenReturn(expected);

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        final List<Course> actual = service.findCoursesByTestRun(testRun);

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testFindCoursesByTestUuidError() {
        final TestRun testRun = new TestRun();

        final CourseRepository repository = Mockito.mock(CourseRepository.class);
        when(repository.findAll(any(sClass))).thenThrow(new UnitTestException());

        final CourseFinderService service = new CourseFinderServiceImpl(repository);
        service.findCoursesByTestRun(testRun);
    }

    ....
}

I could eliminate a lot of duplicate code by using a @Begin method but decided against it to support parallel testing.

Integration Testing

We finally come to our integration tests. We know we did something right because the only is one line to test additional functionality – counting the number of courses in the database.

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
  3.         TestPersistenceJpaConfig.class })
  4. @Transactional
  5. @TransactionConfiguration(defaultRollback = true)
  6. public class CourseServiceIntegrationTest {
  7.  
  8.     @Resource
  9.     private CourseFinderService fdao;
  10.  
  11.     @Resource
  12.     private CourseManagerService mdao;
  13.  
  14.     @Resource
  15.     private TestRunService testService;
  16.  
  17.     @Test
  18.     public void testCourseLifecycle() throws Exception {
  19.         final TestRun testRun = testService.createTestRun();
  20.  
  21.         final String name = "Calculus 101 : " + testRun.getUuid();
  22.  
  23.         final Course expected = new Course();
  24.         expected.setName(name);
  25.  
  26.         assertNull(expected.getId());
  27.  
  28.         // create course
  29.         Course actual = mdao.createCourseForTesting(name, testRun);
  30.         expected.setId(actual.getId());
  31.         expected.setUuid(actual.getUuid());
  32.         expected.setCreationDate(actual.getCreationDate());
  33.  
  34.         assertThat(expected, equalTo(actual));
  35.         assertNotNull(actual.getUuid());
  36.         assertNotNull(actual.getCreationDate());
  37.  
  38.         // get course by id
  39.         actual = fdao.findCourseById(expected.getId());
  40.         assertThat(expected, equalTo(actual));
  41.  
  42.         // get course by uuid
  43.         actual = fdao.findCourseByUuid(expected.getUuid());
  44.         assertThat(expected, equalTo(actual));
  45.  
  46.         // get all courses
  47.         final List<Course> courses = fdao.findCoursesByTestRun(testRun);
  48.         assertTrue(courses.contains(actual));
  49.  
  50.         // count courses
  51.         final long count = fdao.countByTestRun(testRun);
  52.         assertTrue(count > 0);
  53.  
  54.         // update course
  55.         expected.setName("Calculus 102 : " + testRun.getUuid());
  56.         actual = mdao.updateCourse(actual, expected.getName());
  57.         assertThat(expected, equalTo(actual));
  58.  
  59.         // delete Course
  60.         mdao.deleteCourse(expected.getUuid(), 0);
  61.         try {
  62.             fdao.findCourseByUuid(expected.getUuid());
  63.             fail("exception expected");
  64.         } catch (ObjectNotFoundException e) {
  65.             // expected
  66.         }
  67.  
  68.         testService.deleteTestRun(testRun.getUuid());
  69.     }
  70. }
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
        TestPersistenceJpaConfig.class })
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class CourseServiceIntegrationTest {

    @Resource
    private CourseFinderService fdao;

    @Resource
    private CourseManagerService mdao;

    @Resource
    private TestRunService testService;

    @Test
    public void testCourseLifecycle() throws Exception {
        final TestRun testRun = testService.createTestRun();

        final String name = "Calculus 101 : " + testRun.getUuid();

        final Course expected = new Course();
        expected.setName(name);

        assertNull(expected.getId());

        // create course
        Course actual = mdao.createCourseForTesting(name, testRun);
        expected.setId(actual.getId());
        expected.setUuid(actual.getUuid());
        expected.setCreationDate(actual.getCreationDate());

        assertThat(expected, equalTo(actual));
        assertNotNull(actual.getUuid());
        assertNotNull(actual.getCreationDate());

        // get course by id
        actual = fdao.findCourseById(expected.getId());
        assertThat(expected, equalTo(actual));

        // get course by uuid
        actual = fdao.findCourseByUuid(expected.getUuid());
        assertThat(expected, equalTo(actual));

        // get all courses
        final List<Course> courses = fdao.findCoursesByTestRun(testRun);
        assertTrue(courses.contains(actual));

        // count courses
        final long count = fdao.countByTestRun(testRun);
        assertTrue(count > 0);

        // update course
        expected.setName("Calculus 102 : " + testRun.getUuid());
        actual = mdao.updateCourse(actual, expected.getName());
        assertThat(expected, equalTo(actual));

        // delete Course
        mdao.deleteCourse(expected.getUuid(), 0);
        try {
            fdao.findCourseByUuid(expected.getUuid());
            fail("exception expected");
        } catch (ObjectNotFoundException e) {
            // expected
        }

        testService.deleteTestRun(testRun.getUuid());
    }
}

Source Code

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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

How Many Lines Of Code Do You Write Every Day?

Bear Giles | December 27, 2013

I was recently asked a bizarre question in an interview – “how many lines of code do you write every day?”

I’ve been thinking about that question for a week and realized that it makes sense since she was a C person before migrating to other work a few years ago, not a contemporary java person.

To explain, I can fold my career into three major stages.

Ancient – computers were resource-limited (memory < 1 MB) and no third-party libraries to speak of. Our "internet" was journals (esp. "Programming Pearls"), magazines (e.g., "Dr. Dobbs") and books. You wrote everything you used and you needed to use efficient algorithms and data structures if you didn’t want it to crawl. SLOC could be considered a fair measure of productivity, subject to various well-known limitations. (E.g., you needed to be comparing comparable work using the same definition of SLOC, etc.)

(Sidenote: on several occasions I improved performance of existing code by several orders of magnitude – literally hours to minutes or minutes to seconds – by simply recognizing that the code was now running on computers with more memory and I could cache values instead of constantly rereading files or the database, recomputing values, etc.)

Mature C – computers had decent resources (memory < 100 MB) and a wide variety of quality third-party libraries. We could get those libraries via the internet but it was years before Google or blogs and we still depended on journals, magazines and books. SLOC was starting to lose its value since you could have one person who still wrote everything himself (including bugs) and another person who took less time and wrote fewer SLOC but had better results since he used a third-party library. Knowing good third-party libraries was important but not critical and you still spent most of your time writing code (excluding meetings, etc.).

Mature Java – computers have ample resources (memory > 4 GB, multicore processors) and a number of quality deep libraries. The standard API, Apache, Spring Framework, etc. We can get libraries by adding a few lines of text to a configuration file and we can learn about them by reading blogs and Google searches. We’re often more productive with a day of research than a week of coding.

There is no “right answer” here since it depends on the ecosystem. Some languages have deeper third-party libraries than others. Some shops require highly specialized needs that they must write themselves. But it really struck me just how different my world has become over the last few years when she asked that question and my first thought was how much time I spent in research vs. coding and how that affects the raw SLOC numbers.

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: Webservice Integration

Bear Giles | December 25, 2013

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer, Persistence with Spring Data and Sharding Integration Test Data.

Earlier we successfully ran integration tests for both the persistence/business layer (using an embedded H2 database) and the REST server/client layers (using a Jetty server). It’s time to knit everything together.

Fortunately we already have all of our code in place and tested. All we need to do now is create some configuration file magic.

Limitations

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

Encryption – no effort has been made to encrypt communications.

Container-Managed Datasource and JNDI

Container-managed datasources have a bad reputation for many developers and I’m not sure why. Confusion with Container-Managed Persistence (CMP), perhaps?

In any case the idea behind a container-managed datasource is simple. You don’t need to figure out how to maintain database connection parameters in a deployed system – no need to modify a deployed webapp (which is insecure) or read a file from the filesystem (which you may not be able to access), etc. You just hand the problem to the person maintaining the webserver/appserver and retrieve the value via JNDI.

Tomcat and Jetty require manual configuration in an XML file. More advanced appservers like JBoss and GlassFish allow you to configure datasources via a nice GUI. It doesn’t really matter since you only have to do it once. Instructions for Tomcat 7 and Jetty.

The key points for Tomcat are that the server library is under $CATALINA_HOME/lib and that might not be where you expect. E.g., in Ubuntu it’s /usr/share/tomcat7/lib, not /var/lib/tomcat7/lib. Second, if the META-INF/context.xml file isn’t picked up from the .war file you must place it under conf/Catalina/localhost/student-ws-webapp.xml (or whatever you have named your .war file). The latter overrides the former so it’s common to set up the .war file to run in the development environment and then override the configuration in the test and production environments.

META-INF/context.xml (Tomcat)

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <Context>
  3.  
  4.     <Resource name="jdbc/studentDS"
  5.        auth="Container"
  6.        type="javax.sql.DataSource"
  7.        driverClassName="org.postgresql.Driver"
  8.        url="jdbc:postgresql:student"
  9.        username="student"
  10.        password="student"
  11.        maxActive="20"
  12.        maxIdle="10"
  13.        maxWait="-1"
  14.        factory="org.apache.commons.dbcp.BasicDataSourceFactory" />
  15.        
  16. </Context>
<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <Resource name="jdbc/studentDS"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql:student"
        username="student"
        password="student"
        maxActive="20"
        maxIdle="10"
        maxWait="-1"
        factory="org.apache.commons.dbcp.BasicDataSourceFactory" />
        
</Context>

It’s worth noting that it’s trivial to pass an encryption key via JNDI (as a java.lang.String). This has the same benefits as discussed earlier with regards to the need to modify a deployed webapp or access the server’s filesystem.

(The implementation is a little more complex since you want your actual encryption key to require both the JNDI key AND a filesystem-based salt but this is easy to handle during initial deployment of the webapp.)

JPA Configuration

Our persistence.xml file is extremely minimal. We’ll typically want two persistence units, one for JTA transactions (production) and one for non-JTA transactions (development and testing).

META-INF/persistence.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <persistence xmlns="http://java.sun.com/xml/ns/persistence"
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.     xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
  5.   http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
  6.     version="1.0">
  7.  
  8.     <persistence-unit name="studentPU-local"
  9.         transaction-type="RESOURCE_LOCAL">
  10.         <provider>org.hibernate.ejb.HibernatePersistence</provider>
  11.         <non-jta-data-source>jdbc/studentDS</non-jta-data-source>
  12.     </persistence-unit>
  13. </persistence>
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
   http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
	version="1.0">

	<persistence-unit name="studentPU-local"
		transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<non-jta-data-source>jdbc/studentDS</non-jta-data-source>
	</persistence-unit>
</persistence>

Web Configuration

The web.xml file is nearly identical to the one used in integration testing. The key difference is that it pulls in a resource reference for the container-provided datasource.

WEB-INF/web.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
  3.    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  5.  
  6.     <display-name>Project Student Webservice</display-name>
  7.    
  8.     <context-param>
  9.         <param-name>contextClass</param-name>
  10.         <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  11.     </context-param>
  12.     <context-param>
  13.         <param-name>contextConfigLocation</param-name>
  14.         <param-value>
  15.             com.invariantproperties.sandbox.student.config.PersistenceJpaConfig
  16.             com.invariantproperties.sandbox.student.config.BusinessApplicationContext
  17.             com.invariantproperties.sandbox.student.webservice.config.RestApplicationContext
  18.         </param-value>
  19.     </context-param>
  20.  
  21.     <listener>
  22.         <listener-class>
  23.             org.springframework.web.context.ContextLoaderListener
  24.         </listener-class>
  25.     </listener>
  26.  
  27.     <servlet>
  28.         <servlet-name>REST dispatcher</servlet-name>
  29.         <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
  30.         <init-param>
  31.             <param-name>spring.profiles.active</param-name>
  32.             <param-value>test</param-value>
  33.         </init-param>
  34.     </servlet>
  35.  
  36.     <servlet-mapping>
  37.         <servlet-name>REST dispatcher</servlet-name>
  38.         <url-pattern>/rest/*</url-pattern>
  39.     </servlet-mapping>
  40.    
  41.     <resource-ref>
  42.         <description>Student Datasource</description>
  43.         <res-ref-name>jdbc/studentDS</res-ref-name>
  44.         <res-type>javax.sql.DataSource</res-type>
  45.         <res-auth>Container</res-auth>
  46.     </resource-ref>
  47. </web-app>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <display-name>Project Student Webservice</display-name>
    
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            com.invariantproperties.sandbox.student.config.PersistenceJpaConfig
            com.invariantproperties.sandbox.student.config.BusinessApplicationContext
            com.invariantproperties.sandbox.student.webservice.config.RestApplicationContext
        </param-value>
    </context-param>

    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <servlet>
        <servlet-name>REST dispatcher</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>test</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>REST dispatcher</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
    
    <resource-ref>
        <description>Student Datasource</description>
        <res-ref-name>jdbc/studentDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>

Spring Configuration

The Spring configuration for the persistence layer is dramatically different from before because of two changes. Stylistically, we can use a standard configuration file instead of a configuration class since we no longer have to deal with an embedded database.

The more important change is that we’ve moved all of the datasource configuration to the container and we can eliminate it from our spring configuration. We need to point to the right datasource and persistence unit and that’s about it!

applicationContext-dao.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3.    xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  4.    xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5.    xmlns:jee="http://www.springframework.org/schema/jee"
  6.    xsi:schemaLocation="
  7.       http://www.springframework.org/schema/beans
  8.       http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
  9.       http://www.springframework.org/schema/context
  10.       http://www.springframework.org/schema/context/spring-context-3.2.xsd
  11.       http://www.springframework.org/schema/tx
  12.       http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
  13.       http://www.springframework.org/schema/data/jpa
  14.       http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
  15.       http://www.springframework.org/schema/jee
  16.       http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">
  17.  
  18.     <context:property-placeholder location="WEB-INF/database.properties" />
  19.  
  20.     <context:annotation-config />
  21.  
  22.     <!-- we use container-based datasource -->
  23.     <jee:jndi-lookup id="dataSource" jndi-name="${persistence.unit.dataSource}"
  24.        expected-type="javax.sql.DataSource" />
  25.  
  26.     <bean name="entityManagerFactory"
  27.        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  28.         <property name="dataSource" ref="dataSource" />
  29.         <property name="persistenceUnitName" value="${persistence.unit.name}" />
  30.         <property name="packagesToScan" value="${entitymanager.packages.to.scan}" />
  31.         <property name="jpaVendorAdapter">
  32.             <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
  33.         </property>
  34.     </bean>
  35.  
  36.     <bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" />
  37.  
  38.     <bean name="exceptionTranslation"
  39.        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
  40.  
  41.     <tx:annotation-driven transaction-manager="transactionManager"
  42.        proxy-target-class="false" />
  43.  
  44. </beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.2.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
       http://www.springframework.org/schema/data/jpa
       http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
       http://www.springframework.org/schema/jee
       http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

    <context:property-placeholder location="WEB-INF/database.properties" />

    <context:annotation-config />

    <!-- we use container-based datasource -->
    <jee:jndi-lookup id="dataSource" jndi-name="${persistence.unit.dataSource}"
        expected-type="javax.sql.DataSource" />

    <bean name="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="persistenceUnitName" value="${persistence.unit.name}" />
        <property name="packagesToScan" value="${entitymanager.packages.to.scan}" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
    </bean>

    <bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" />

    <bean name="exceptionTranslation"
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <tx:annotation-driven transaction-manager="transactionManager"
        proxy-target-class="false" />

</beans>

Our properties file just contains the name of the datasource, the persistence unit name, and the list of packages to scan that contain JPA annotations.

WEB-INF/database.properties

  1. # jpa configuration
  2. entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
  3. persistence.unit.dataSource=java:comp/env/jdbc/studentDS
  4. persistence.unit.name=studentPU-local
# jpa configuration
entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
persistence.unit.dataSource=java:comp/env/jdbc/studentDS
persistence.unit.name=studentPU-local

Database Schema and Security

Finally the integration tests can use Hibernate auto-creation but we should always explicitly maintain our schemas in a development and production environment. This allows us to maintain infrastructure values in addition to avoiding potential problems from automated tools acting in an unexpected manner.

It’s important to write and test both upgrade and downgrade scripts before going into production. We always need a way to recover gracefully if there’s a problem.

More importantly our database schema should always be owned by a different user than the webapp. E.g., the webapp may use ‘student-user’ for the tables created by ‘student-owner’. This will prevent an attacker using SQL injection from deleting or modifying tables.

  1. --
  2. -- for security this must run as student-owner, not student-user!
  3. --
  4.  
  5. --
  6. -- create an idempotent stored procedure that creates the initial database schema.
  7. --
  8. create or replace function create_schema_0_0_2() returns void as $$
  9. declare
  10.     schema_version_rec record;
  11.     schema_count int;
  12. begin
  13.     create table if not exists schema_version (
  14.         schema_version varchar(20) not null
  15.     );
  16.  
  17.     select count(*) into schema_count from schema_version;
  18.  
  19.     case schema_count
  20.         when 0 then
  21.             -- we just created table
  22.             insert into schema_version(schema_version) values('0.0.2');
  23.         when 1 then
  24.             -- this is 'create' so we only need to make sure it's current version
  25.             -- normally we accept either current version or immediately prior version.
  26.             select * into strict schema_version_rec from schema_version;
  27.             if schema_version_rec.schema_version  '0.0.2' then
  28.                 raise notice 'Unwilling to run updates - check prior version';
  29.                 exit;
  30.             end if;      
  31.         else
  32.             raise notice 'Bad database - more than one schema versions defined!';
  33.             exit;
  34.     end case;
  35.  
  36.     -- create tables!
  37.  
  38.     create table if not exists test_run (
  39.         test_run_pkey int primary key,
  40.         uuid varchar(40) unique not null,
  41.         creation_date timestamp not null,
  42.         name varchar(80) not null,
  43.         test_date timestamp not null,
  44.         username varchar(40) not null
  45.     );
  46.  
  47.     create table if not exists classroom (
  48.         classroom_pkey int primary key,
  49.         uuid varchar(40) unique not null,
  50.         creation_date timestamp not null,
  51.         test_run_pkey int references test_run(test_run_pkey),
  52.         name varchar(80) not null
  53.     );
  54.  
  55.     create table if not exists course (
  56.         course_pkey int primary key,
  57.         uuid varchar(40) unique not null,
  58.         creation_date timestamp not null,
  59.         test_run_pkey int references test_run(test_run_pkey),
  60.         name varchar(80) not null
  61.     );
  62.  
  63.     create table if not exists instructor (
  64.         instructor_pkey int primary key,
  65.         uuid varchar(40) unique not null,
  66.         creation_date timestamp not null,
  67.         test_run_pkey int references test_run(test_run_pkey),
  68.         name varchar(80) not null,
  69.         email varchar(200) unique not null
  70.     );
  71.  
  72.     create table if not exists section (
  73.         section_pkey int primary key,
  74.         uuid varchar(40) unique not null,
  75.         creation_date timestamp not null,
  76.         test_run_pkey int references test_run(test_run_pkey),
  77.         name varchar(80) not null
  78.     );
  79.  
  80.     create table if not exists student (
  81.         student_pkey int primary key,
  82.         uuid varchar(40) unique not null,
  83.         creation_date timestamp not null,
  84.         test_run_pkey int references test_run(test_run_pkey),
  85.         name varchar(80) not null,
  86.         email varchar(200) unique not null
  87.     );
  88.  
  89.     create table if not exists term (
  90.         term_pkey int primary key,
  91.         uuid varchar(40) unique not null,
  92.         creation_date timestamp not null,
  93.         test_run_pkey int references test_run(test_run_pkey),
  94.         name varchar(80) not null
  95.     );
  96.  
  97.     -- correction: need to define this!
  98.     create sequence hibernate_sequence;
  99.  
  100.     -- make sure nobody can truncate our tables
  101.     revoke truncate on classroom, course, instructor, section, student, term, test_run from public;
  102.  
  103.     -- grant CRUD privileges to student-user.
  104.     grant select, insert, update, delete on classroom, course, instructor, section, student, term, test_run to student;
  105.  
  106.     grant usage on hibernate_sequence to student;
  107.  
  108.     return;
  109. end;
  110. $$ language plpgsql;
  111.  
  112. -- create database schema
  113. select create_schema_0_0_2() is null;
  114.  
  115. -- clean up
  116. drop function create_schema_0_0_2();
--
-- for security this must run as student-owner, not student-user!
--

--
-- create an idempotent stored procedure that creates the initial database schema.
--
create or replace function create_schema_0_0_2() returns void as $$
declare
    schema_version_rec record;
    schema_count int;
begin
    create table if not exists schema_version (
        schema_version varchar(20) not null
    );

    select count(*) into schema_count from schema_version;

    case schema_count
        when 0 then
            -- we just created table
            insert into schema_version(schema_version) values('0.0.2');
        when 1 then
            -- this is 'create' so we only need to make sure it's current version
            -- normally we accept either current version or immediately prior version.
            select * into strict schema_version_rec from schema_version;
            if schema_version_rec.schema_version  '0.0.2' then
                raise notice 'Unwilling to run updates - check prior version';
                exit;
            end if;      
        else
            raise notice 'Bad database - more than one schema versions defined!';
            exit;
    end case;

    -- create tables!

    create table if not exists test_run (
        test_run_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        name varchar(80) not null,
        test_date timestamp not null,
        username varchar(40) not null
    );

    create table if not exists classroom (
        classroom_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );

    create table if not exists course (
        course_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );

    create table if not exists instructor (
        instructor_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );

    create table if not exists section (
        section_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );

    create table if not exists student (
        student_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );

    create table if not exists term (
        term_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );

    -- correction: need to define this!
    create sequence hibernate_sequence;

    -- make sure nobody can truncate our tables
    revoke truncate on classroom, course, instructor, section, student, term, test_run from public;

    -- grant CRUD privileges to student-user.
    grant select, insert, update, delete on classroom, course, instructor, section, student, term, test_run to student;

    grant usage on hibernate_sequence to student;

    return;
end;
$$ language plpgsql;

-- create database schema
select create_schema_0_0_2() is null;

-- clean up
drop function create_schema_0_0_2();

Integration Testing

We can reuse the integration tests from the webservice service but I haven’t copied them to this project since 1) I hate duplicating code and it would be better to pull the integration tests into a separate project used by both server and webapp and 2) it’s easy to configure Jetty to supply JNDI values but for some reason the documented class is throwing an exception and it’s not important enough (at this time) to spend more than a few hours researching.

Source Code

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

Correction

The schema provided overlooked the ‘hibernate_sequence’ required to create new objects. It must be defined and readable by the ‘student’ user.

Comments
No Comments »
Categories
java, linux
Comments rss Comments rss
Trackback Trackback

Project Student: Sharding Integration Test Data

Bear Giles | December 23, 2013

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer and Persistence with Spring Data.

All of the integration tests until now have used an in-memory embedded database that did not retain information from run to run. This changes when we fully integrate the REST server with a “real” database server – leftover test data will pollute our development or test database. This can be a real headache once we have continuous integration that runs integration tests code is checked in.

One solution is to ‘shard’ our integration test data in a way that allows our tests to use the shared development database without polluting it or other tests. The easiest approach is to add a TestRun field to all of our objects. “Test” data will have a value that indicates the specific test run, “live” data will have a null value.

The exact timeline is

  1. create and persist a TestRun object
  2. create test objects with appropriate TestRun value
  3. perform the integration tests
  4. delete the test objects
  5. delete the TestRun object

Any entry in the TestRun table will either be 1) active integration tests or 2) failed integration tests that threw an unhandled exception (depending upon the transaction manager, of course). It’s important to note that we can also capture the database state after an unexpected exception is thrown even if the transaction manager performs a rollback – it’s a simple extension to the junit test runner.

Timestamp and user fields make it easy to delete stale test data according to its age (e.g., any test more than 7 days old) or the person who ran the test.

TestablePersistentObject abstract base class

This change starts at the persistence level so we should start there and work our way outwards.

We first extend our PersistentObject abstract base class with a test run value.

  1. @MappedSuperclass
  2. public abstract class TestablePersistentObject extends PersistentObject {
  3.     private static final long serialVersionUID = 1L;
  4.     private TestRun testRun;
  5.  
  6.     /**
  7.      * Fetch testRun object. We use lazy fetching since we rarely care about the
  8.      * contents of this object - we just want to ensure referential integrity to
  9.      * an existing testRun object when persisting a TPO.
  10.      *
  11.      * @return
  12.      */
  13.     @ManyToOne(fetch = FetchType.LAZY, optional = true)
  14.     public TestRun getTestRun() {
  15.         return testRun;
  16.     }
  17.  
  18.     public void setTestRun(TestRun testRun) {
  19.         this.testRun = testRun;
  20.     }
  21.  
  22.     @Transient
  23.     public boolean isTestData() {
  24.         return testRun != null;
  25.     }
  26. }
@MappedSuperclass
public abstract class TestablePersistentObject extends PersistentObject {
    private static final long serialVersionUID = 1L;
    private TestRun testRun;

    /**
     * Fetch testRun object. We use lazy fetching since we rarely care about the
     * contents of this object - we just want to ensure referential integrity to
     * an existing testRun object when persisting a TPO.
     * 
     * @return
     */
    @ManyToOne(fetch = FetchType.LAZY, optional = true)
    public TestRun getTestRun() {
        return testRun;
    }

    public void setTestRun(TestRun testRun) {
        this.testRun = testRun;
    }

    @Transient
    public boolean isTestData() {
        return testRun != null;
    }
}

TestRun class

The TestRun class contains identifying information about a single integration test run. It contains a name (by default the classname#methodname() of the surrounding integration test), the date and time of the test, and the name of the user running the test. It would be easy to capture additional information.

The list of test objects gives us two big wins. First, it makes it easy to capture the state of the database if needed (e.g., after an unexpected exception). Second, cascading deletions makes it easy to delete all test objects.

  1. @XmlRootElement
  2. @Entity
  3. @Table(name = "test_run")
  4. @AttributeOverride(name = "id", column = @Column(name = "test_run_pkey"))
  5. public class TestRun extends PersistentObject {
  6.     private static final long serialVersionUID = 1L;
  7.  
  8.     private String name;
  9.     private Date testDate;
  10.     private String user;
  11.     private List<TestablePersistentObject> objects = Collections.emptyList();
  12.  
  13.     @Column(length = 80, unique = false, updatable = true)
  14.     public String getName() {
  15.         return name;
  16.     }
  17.  
  18.     public void setName(String name) {
  19.         this.name = name;
  20.     }
  21.  
  22.     @Column(name = "test_date", nullable = false, updatable = false)
  23.     @Temporal(TemporalType.TIMESTAMP)
  24.     public Date getTestDate() {
  25.         return testDate;
  26.     }
  27.  
  28.     public void setTestDate(Date testDate) {
  29.         this.testDate = testDate;
  30.     }
  31.  
  32.     @Column(length = 40, unique = false, updatable = false)
  33.     public String getUser() {
  34.         return user;
  35.     }
  36.  
  37.     public void setUser(String user) {
  38.         this.user = user;
  39.     }
  40.  
  41.     @OneToMany(cascade = CascadeType.ALL)
  42.     public List<TestablePersistentObject> getObjects() {
  43.         return objects;
  44.     }
  45.  
  46.     public void setObjects(List<TestablePersistentObject> objects) {
  47.         this.objects = objects;
  48.     }
  49.  
  50.     /**
  51.      * This is similar to standard prepersist method but we also set default
  52.      * values for everything else.
  53.      */
  54.     @PrePersist
  55.     public void prepersist() {
  56.         if (getCreationDate() == null) {
  57.             setCreationDate(new Date());
  58.         }
  59.  
  60.         if (getTestDate() == null) {
  61.             setTestDate(new Date());
  62.         }
  63.  
  64.         if (getUuid() == null) {
  65.             setUuid(UUID.randomUUID().toString());
  66.         }
  67.  
  68.         if (getUser() == null) {
  69.             setUser(System.getProperty("user.name"));
  70.         }
  71.  
  72.         if (name == null) {
  73.             setName("test run " + getUuid());
  74.         }
  75.     }
  76. }
@XmlRootElement
@Entity
@Table(name = "test_run")
@AttributeOverride(name = "id", column = @Column(name = "test_run_pkey"))
public class TestRun extends PersistentObject {
    private static final long serialVersionUID = 1L;

    private String name;
    private Date testDate;
    private String user;
    private List<TestablePersistentObject> objects = Collections.emptyList();

    @Column(length = 80, unique = false, updatable = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(name = "test_date", nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    public Date getTestDate() {
        return testDate;
    }

    public void setTestDate(Date testDate) {
        this.testDate = testDate;
    }

    @Column(length = 40, unique = false, updatable = false)
    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    @OneToMany(cascade = CascadeType.ALL)
    public List<TestablePersistentObject> getObjects() {
        return objects;
    }

    public void setObjects(List<TestablePersistentObject> objects) {
        this.objects = objects;
    }

    /**
     * This is similar to standard prepersist method but we also set default
     * values for everything else.
     */
    @PrePersist
    public void prepersist() {
        if (getCreationDate() == null) {
            setCreationDate(new Date());
        }

        if (getTestDate() == null) {
            setTestDate(new Date());
        }

        if (getUuid() == null) {
            setUuid(UUID.randomUUID().toString());
        }

        if (getUser() == null) {
            setUser(System.getProperty("user.name"));
        }

        if (name == null) {
            setName("test run " + getUuid());
        }
    }
}

The TestRun class extends PersistentObject, not TestablePersistentObject, since our other integration tests will sufficiently exercise it.

Spring Data Repository

We must add one additional method to every Repository.

  1. @Repository
  2. public interface CourseRepository extends JpaRepository {
  3.     List<Course> findCoursesByTestRun(TestRun testRun);
  4.  
  5.     ....
  6. }
@Repository
public interface CourseRepository extends JpaRepository {
    List<Course> findCoursesByTestRun(TestRun testRun);

    ....
}

Service Interface

Likewise we must add two additional methods to every service.

  1. public interface CourseService {
  2.     List<Course> findAllCourses();
  3.  
  4.     Course findCourseById(Integer id);
  5.  
  6.     Course findCourseByUuid(String uuid);
  7.  
  8.     Course createCourse(String name);
  9.  
  10.     Course updateCourse(Course course, String name);
  11.  
  12.     void deleteCourse(String uuid);
  13.  
  14.     // new method for testing
  15.     Course createCourseForTesting(String name, TestRun testRun);
  16.  
  17.     // new method for testing
  18.     List<Course> findAllCoursesForTestRun(TestRun testRun);
  19. }
public interface CourseService {
    List<Course> findAllCourses();

    Course findCourseById(Integer id);

    Course findCourseByUuid(String uuid);

    Course createCourse(String name);

    Course updateCourse(Course course, String name);

    void deleteCourse(String uuid);

    // new method for testing
    Course createCourseForTesting(String name, TestRun testRun);

    // new method for testing
    List<Course> findAllCoursesForTestRun(TestRun testRun);
}

I won’t show the TestRunRepository, the TestRunService interface, or the TestRunService implementation since they’re identical to what I’ve described in the last few blog entries.

Service Implementation

We have to make one small change to an existing Service implementation, plus add two new methods.

  1. @Service
  2. public class CourseServiceImpl implements CourseService {
  3.     @Resource
  4.     private TestRunService testRunService;
  5.  
  6.     /**
  7.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  8.      *      findAllCourses()
  9.      */
  10.     @Transactional(readOnly = true)
  11.     @Override
  12.     public List<Course> findAllCourses() {
  13.         List<Course> courses = null;
  14.  
  15.         try {
  16.             courses = courseRepository.findCoursesByTestRun(null);
  17.         } catch (DataAccessException e) {
  18.             if (!(e instanceof UnitTestException)) {
  19.                 log.info("error loading list of courses: " + e.getMessage(), e);
  20.             }
  21.             throw new PersistenceException("unable to get list of courses.", e);
  22.         }
  23.  
  24.         return courses;
  25.     }
  26.  
  27.     /**
  28.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  29.      *      findAllCoursesForTestRun(com.invariantproperties.sandbox.student.common.TestRun)
  30.      */
  31.     @Transactional(readOnly = true)
  32.     @Override
  33.     public List<Course> findAllCoursesForTestRun(TestRun testRun) {
  34.         List<Course> courses = null;
  35.  
  36.         try {
  37.             courses = courseRepository.findCoursesByTestRun(testRun);
  38.         } catch (DataAccessException e) {
  39.             if (!(e instanceof UnitTestException)) {
  40.                 log.info("error loading list of courses: " + e.getMessage(), e);
  41.             }
  42.             throw new PersistenceException("unable to get list of courses.", e);
  43.         }
  44.  
  45.         return courses;
  46.     }
  47.  
  48.     /**
  49.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  50.      *      createCourseForTesting(java.lang.String,
  51.      *      com.invariantproperties.sandbox.student.common.TestRun)
  52.      */
  53.     @Transactional
  54.     @Override
  55.     public Course createCourseForTesting(String name, TestRun testRun) {
  56.         final Course course = new Course();
  57.         course.setName(name);
  58.         course.setTestUuid(testRun.getTestUuid());
  59.  
  60.         Course actual = null;
  61.         try {
  62.             actual = courseRepository.saveAndFlush(course);
  63.         } catch (DataAccessException e) {
  64.             if (!(e instanceof UnitTestException)) {
  65.                 log.info("internal error retrieving course: " + name, e);
  66.             }
  67.             throw new PersistenceException("unable to create course", e);
  68.         }
  69.  
  70.         return actual;
  71.     }
  72. }
@Service
public class CourseServiceImpl implements CourseService {
    @Resource
    private TestRunService testRunService;

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findAllCourses()
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCourses() {
        List<Course> courses = null;

        try {
            courses = courseRepository.findCoursesByTestRun(null);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }

        return courses;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findAllCoursesForTestRun(com.invariantproperties.sandbox.student.common.TestRun)
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCoursesForTestRun(TestRun testRun) {
        List<Course> courses = null;

        try {
            courses = courseRepository.findCoursesByTestRun(testRun);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }

        return courses;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      createCourseForTesting(java.lang.String,
     *      com.invariantproperties.sandbox.student.common.TestRun)
     */
    @Transactional
    @Override
    public Course createCourseForTesting(String name, TestRun testRun) {
        final Course course = new Course();
        course.setName(name);
        course.setTestUuid(testRun.getTestUuid());

        Course actual = null;
        try {
            actual = courseRepository.saveAndFlush(course);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving course: " + name, e);
            }
            throw new PersistenceException("unable to create course", e);
        }

        return actual;
    }
}

CourseServiceIntegrationTest

We make a few changes to our integration tests. We only have to change one test method since it’s the only one that actually creates a test object. The rest of the methods are queries that don’t require test data.

Note that we change the name value to ensure it’s unique. This is a way to work around uniqueness constraints, e.g., for email addresses.

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
  3.         TestPersistenceJpaConfig.class })
  4. @Transactional
  5. @TransactionConfiguration(defaultRollback = true)
  6. public class CourseServiceIntegrationTest {
  7.  
  8.     @Resource
  9.     private CourseService dao;
  10.  
  11.     @Resource
  12.     private TestRunService testService;
  13.    
  14.     @Test
  15.     public void testCourseLifecycle() throws Exception {
  16.         final TestRun testRun = testService.createTestRun();
  17.  
  18.         final String name = "Calculus 101 : " + testRun.getUuid();
  19.  
  20.         final Course expected = new Course();
  21.         expected.setName(name);
  22.  
  23.         assertNull(expected.getId());
  24.  
  25.         // create course
  26.         Course actual = dao.createCourseForTesting(name, testRun);
  27.         expected.setId(actual.getId());
  28.         expected.setUuid(actual.getUuid());
  29.         expected.setCreationDate(actual.getCreationDate());
  30.  
  31.         assertThat(expected, equalTo(actual));
  32.         assertNotNull(actual.getUuid());
  33.         assertNotNull(actual.getCreationDate());
  34.  
  35.         // get course by id
  36.         actual = dao.findCourseById(expected.getId());
  37.         assertThat(expected, equalTo(actual));
  38.  
  39.         // get course by uuid
  40.         actual = dao.findCourseByUuid(expected.getUuid());
  41.         assertThat(expected, equalTo(actual));
  42.  
  43.         // get all courses
  44.         final List<Course> courses = dao.findCoursesByTestRun(testRun);
  45.         assertTrue(courses.contains(actual));
  46.  
  47.         // update course
  48.         expected.setName("Calculus 102 : " + testRun.getUuid());
  49.         actual = dao.updateCourse(actual, expected.getName());
  50.         assertThat(expected, equalTo(actual));
  51.  
  52.         // verify testRun.getObjects
  53.         final List<TestablePersistentObject> objects = testRun.getObjects();
  54.         assertTrue(objects.contains(actual));
  55.  
  56.         // delete Course
  57.         dao.deleteCourse(expected.getUuid());
  58.         try {
  59.             dao.findCourseByUuid(expected.getUuid());
  60.             fail("exception expected");
  61.         } catch (ObjectNotFoundException e) {
  62.             // expected
  63.         }
  64.  
  65.         testService.deleteTestRun(testRun.getUuid());
  66.     }
  67.  
  68.     ....
  69. }
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
        TestPersistenceJpaConfig.class })
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class CourseServiceIntegrationTest {

    @Resource
    private CourseService dao;

    @Resource
    private TestRunService testService;
    
    @Test
    public void testCourseLifecycle() throws Exception {
        final TestRun testRun = testService.createTestRun();

        final String name = "Calculus 101 : " + testRun.getUuid();

        final Course expected = new Course();
        expected.setName(name);

        assertNull(expected.getId());

        // create course
        Course actual = dao.createCourseForTesting(name, testRun);
        expected.setId(actual.getId());
        expected.setUuid(actual.getUuid());
        expected.setCreationDate(actual.getCreationDate());

        assertThat(expected, equalTo(actual));
        assertNotNull(actual.getUuid());
        assertNotNull(actual.getCreationDate());

        // get course by id
        actual = dao.findCourseById(expected.getId());
        assertThat(expected, equalTo(actual));

        // get course by uuid
        actual = dao.findCourseByUuid(expected.getUuid());
        assertThat(expected, equalTo(actual));

        // get all courses
        final List<Course> courses = dao.findCoursesByTestRun(testRun);
        assertTrue(courses.contains(actual));

        // update course
        expected.setName("Calculus 102 : " + testRun.getUuid());
        actual = dao.updateCourse(actual, expected.getName());
        assertThat(expected, equalTo(actual));

        // verify testRun.getObjects
        final List<TestablePersistentObject> objects = testRun.getObjects();
        assertTrue(objects.contains(actual));

        // delete Course
        dao.deleteCourse(expected.getUuid());
        try {
            dao.findCourseByUuid(expected.getUuid());
            fail("exception expected");
        } catch (ObjectNotFoundException e) {
            // expected
        }

        testService.deleteTestRun(testRun.getUuid());
    }

    ....
}

We could use @Before and @After to transparently wrap all test methods but many tests don’t require test data and many tests that do require test data require unique test data, e.g., for email addresses. In the latter case we fold in the Test UUID as above.

REST Webservice Server

The REST webservice requires adding a test uuid to the request classes and adding a bit of logic to properly handle it when creating an object.

The REST webservice does not support getting a list of all test objects. The “correct” approach will be creating a TestRun service and providing associated objects in response to a /get/{id} query.

  1. @XmlRootElement
  2. public class Name {
  3.     private String name;
  4.     private String testUuid;
  5.  
  6.     public String getName() {
  7.         return name;
  8.     }
  9.  
  10.     public void setName(String name) {
  11.         this.name = name;
  12.     }
  13.  
  14.     public String getTestUuid() {
  15.         return testUuid;
  16.     }
  17.  
  18.     public void setTestUuid(String testUuid) {
  19.         this.testUuid = testUuid;
  20.     }
  21. }
@XmlRootElement
public class Name {
    private String name;
    private String testUuid;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getTestUuid() {
        return testUuid;
    }

    public void setTestUuid(String testUuid) {
        this.testUuid = testUuid;
    }
}

We can now check for the optional testUuid field and call the appropriate create method.

  1. @Service
  2. @Path("/course")
  3. public class CourseResource extends AbstractResource {
  4.     @Resource
  5.     private CourseService service;
  6.  
  7.     @Resource
  8.     private TestRunService testRunService;
  9.  
  10.     /**
  11.      * Create a Course.
  12.      *
  13.      * @param req
  14.      * @return
  15.      */
  16.     @POST
  17.     @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  18.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  19.     public Response createCourse(Name req) {
  20.         log.debug("CourseResource: createCourse()");
  21.  
  22.         final String name = req.getName();
  23.         if ((name == null) || name.isEmpty()) {
  24.             return Response.status(Status.BAD_REQUEST).entity("'name' is required'").build();
  25.         }
  26.  
  27.         Response response = null;
  28.  
  29.         try {
  30.             Course course = null;
  31.  
  32.             if (req.getTestUuid() != null) {
  33.                 TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());
  34.                 if (testRun != null) {
  35.                     course = service.createCourseForTesting(name, testRun);
  36.                 } else {
  37.                     response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();
  38.                 }
  39.             } else {
  40.                 course = service.createCourse(name);
  41.             }
  42.             if (course == null) {
  43.                 response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  44.             } else {
  45.                 response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
  46.             }
  47.         } catch (Exception e) {
  48.             if (!(e instanceof UnitTestException)) {
  49.                 log.info("unhandled exception", e);
  50.             }
  51.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  52.         }
  53.  
  54.         return response;
  55.     }
  56.  
  57.     ....
  58. }
@Service
@Path("/course")
public class CourseResource extends AbstractResource {
    @Resource
    private CourseService service;

    @Resource
    private TestRunService testRunService;

    /**
     * Create a Course.
     * 
     * @param req
     * @return
     */
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response createCourse(Name req) {
        log.debug("CourseResource: createCourse()");

        final String name = req.getName();
        if ((name == null) || name.isEmpty()) {
            return Response.status(Status.BAD_REQUEST).entity("'name' is required'").build();
        }

        Response response = null;

        try {
            Course course = null;

            if (req.getTestUuid() != null) {
                TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());
                if (testRun != null) {
                    course = service.createCourseForTesting(name, testRun);
                } else {
                    response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();
                }
            } else {
                course = service.createCourse(name);
            }
            if (course == null) {
                response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            } else {
                response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
            }
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }

    ....
}

REST Webservice Client

Finally the REST server must add one additional method. The client does not support getting a list of all test objects yet.

  1. public interface CourseRestClient {
  2.  
  3.     /**
  4.      * Create specific course for testing.
  5.      *
  6.      * @param name
  7.      * @param testRun
  8.      */
  9.     Course createCourseForTesting(String name, TestRun testRun);
  10.  
  11.     ....
  12. }
public interface CourseRestClient {

    /**
     * Create specific course for testing.
     * 
     * @param name
     * @param testRun
     */
    Course createCourseForTesting(String name, TestRun testRun);

    ....
}

and

  1. public class CourseRestClientImpl extends AbstractRestClientImpl implements CourseRestClient {
  2.  
  3.     /**
  4.      * Create JSON string.
  5.      *
  6.      * @param name
  7.      * @return
  8.      */
  9.     String createJson(final String name, final TestRun testRun) {
  10.         return String.format("{ \"name\": \"%s\", \"testUuid\": \"%s\" }", name, testRun.getTestUuid());
  11.     }
  12.  
  13.     /**
  14.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
  15.      */
  16.     @Override
  17.     public Course createCourseForTesting(final String name, final TestRun testRun) {
  18.         if (name == null || name.isEmpty()) {
  19.             throw new IllegalArgumentException("'name' is required");
  20.         }
  21.  
  22.         if (testRun == null || testRun.getTestUuid() == null || testRun.getTestUuid().isEmpty()) {
  23.             throw new IllegalArgumentException("'testRun' is required");
  24.         }
  25.  
  26.         return createObject(createJson(name, testRun));
  27.     }
  28.  
  29.     ....
  30. }
public class CourseRestClientImpl extends AbstractRestClientImpl implements CourseRestClient {

    /**
     * Create JSON string.
     * 
     * @param name
     * @return
     */
    String createJson(final String name, final TestRun testRun) {
        return String.format("{ \"name\": \"%s\", \"testUuid\": \"%s\" }", name, testRun.getTestUuid());
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
     */
    @Override
    public Course createCourseForTesting(final String name, final TestRun testRun) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }

        if (testRun == null || testRun.getTestUuid() == null || testRun.getTestUuid().isEmpty()) {
            throw new IllegalArgumentException("'testRun' is required");
        }

        return createObject(createJson(name, testRun));
    }

    ....
}

Source Code

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

Clarification

I didn’t think it was possible to have a @OneToMany to TestablePersistentObject in TestRun but the integration tests using H2 succeeded. Unfortunately it’s causing problems as I bring up the fully integrated webservice with a PostgreSQL database. I’m leaving the code in place above since it’s always possible to have a list of Classrooms, a list of Courses, etc., even if we can’t have a generic collection. However the code is being removed from the version under source control.

Correction

The interface method should be findCourseByTestRun_Uuid(), not findCourseByTestRun(). Another approach is using JPA criteria queries – see Project Student: JPA Criteria Queries.

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: Persistence With Spring Data

Bear Giles | December 19, 2013

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey and Business Layer.

The final layer of the RESTful webapp onion is the persistence layer.

There are two philosophies for persistence layers. One camp sees the database as a simple store and wants to keep this layer extremely thin. The other camp knows it’s often much faster to perform tasks in the database than to hit the database for the data, do the necessary work in java, and possibly hit the database a second time with the results. This camp often wants a fairly thick persistence layer.

The second camp also has a group of exiles that wants to use stored procedures extensively. This makes the database much more powerful but at the cost of a bit of additional complexity in the persistence layer. It has a major drawback in that stored procedures are database-specific.

A second group of exiles just uses stored procedures for security. I’ve discussed this earlier, e.g., the idea that you should call a stored procedure with username and password and get an “accept” or “deny” response instead of retrieving the hashed password and doing the comparison in the application. The first approach allows you to use the database’s GRANT and REVOKE privileges to store hashed passwords in a table that’s inaccessible even if an attacker can perform arbitrary SQL injection as the application user.

(Sidenote: you can often write your stored procedures in Java! Oracle supports it, PostgreSQL supports it (via PL/Java extension), H2 supports it (via the classpath). I don’t know if MySQL supports it. This approach has definite costs but it may be the best solution for many problems.)

Anyway this project only supports CRUD operations at the moment and they’re a classic example of using a thin persistence layer. It’s easy to add ‘thick’ methods though – simply create a CourseRepositoryImpl class with them. (This class should NOT implement the CourseRepository interface!)

Design Decisions

Spring Data – we’re using Spring Data because it autogenerates the persistence layer classes.

Limitations

Pagination – there is no attempt to support pagination. This isn’t a big issue since Spring Data already supports it – we only need to write the glue.

Configuration

The basic configuration only requires the @EnableJpaRepositories annotation.

The integration test using a pure in-memory embedded database requires a bit more work. Using the Spring embedded database directly doesn’t work even if you use the H2 url that tells it to leave the database server up. The database is properly initialized but then closed before the tests actually run. The result is failures since the database schema is missing.

It would be trivial to use an in-memory database backed by a file but that could cause problems with concurrent runs, accidently pulling in old test data, etc. The obvious solution is using a random temporary backing file but that approach introduces its own problems.

The approach below is to cache the embedded database in the configuration class and only destroy it as the app shuts down. This introduces some non-obvious behavior that forces us to explicitly create a few additional beans as well.

(IIRC if you create the embedded database in a configuration class and the transaction beans in a configuration file then spring was creating a phantom datasource in the configuration file and initialization failed. This problem went away when I started to create the transaction beans in the same place as the datasource.)

  1. @Configuration
  2. @EnableJpaRepositories(basePackages = { "com.invariantproperties.sandbox.student.repository" })
  3. @EnableTransactionManagement
  4. @PropertySource("classpath:test-application.properties")
  5. @ImportResource("classpath:applicationContext-dao.xml")
  6. public class TestPersistenceJpaConfig implements DisposableBean {
  7.     private static final Logger log = LoggerFactory.getLogger(TestPersistenceJpaConfig.class);
  8.  
  9.     private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect";
  10.     private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
  11.     private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy";
  12.     private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
  13.     private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
  14.     private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan";
  15.     // private static final String PROPERTY_NAME_PERSISTENCE_UNIT_NAME =
  16.     // "persistence.unit.name";
  17.  
  18.     @Resource
  19.     private Environment environment;
  20.  
  21.     private EmbeddedDatabase db = null;
  22.  
  23.     @Bean
  24.     public DataSource dataSource() {
  25.         final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
  26.         db = builder.setType(EmbeddedDatabaseType.H2).build(); // .script("foo.sql")
  27.         return db;
  28.     }
  29.  
  30.     @Bean
  31.     public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws ClassNotFoundException {
  32.         LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
  33.  
  34.         bean.setDataSource(dataSource());
  35.         bean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN));
  36.         bean.setPersistenceProviderClass(HibernatePersistence.class);
  37.         // bean.setPersistenceUnitName(environment
  38.         // .getRequiredProperty(PROPERTY_NAME_PERSISTENCE_UNIT_NAME));
  39.  
  40.         HibernateJpaVendorAdapter va = new HibernateJpaVendorAdapter();
  41.         bean.setJpaVendorAdapter(va);
  42.  
  43.         Properties jpaProperties = new Properties();
  44.         jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT,
  45. environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
  46.         jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL,
  47.                 environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL));
  48.         jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY,
  49.                 environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY));
  50.         jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL,
  51.                 environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
  52.         jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO,
  53.                 environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO));
  54.  
  55.         bean.setJpaProperties(jpaProperties);
  56.  
  57.         return bean;
  58.     }
  59.  
  60.     @Bean
  61.     public PlatformTransactionManager transactionManager() {
  62.         JpaTransactionManager tm = new JpaTransactionManager();
  63.  
  64.         try {
  65.             tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
  66.         } catch (ClassNotFoundException e) {
  67.             // TODO: log.
  68.         }
  69.  
  70.         return tm;
  71.     }
  72.  
  73.     @Bean
  74.     public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
  75.         return new PersistenceExceptionTranslationPostProcessor();
  76.     }
  77.  
  78.     @Override
  79.     public void destroy() {
  80.         if (db != null) {
  81.             db.shutdown();
  82.         }
  83.     }
  84. }
@Configuration
@EnableJpaRepositories(basePackages = { "com.invariantproperties.sandbox.student.repository" })
@EnableTransactionManagement
@PropertySource("classpath:test-application.properties")
@ImportResource("classpath:applicationContext-dao.xml")
public class TestPersistenceJpaConfig implements DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(TestPersistenceJpaConfig.class);

    private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect";
    private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
    private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy";
    private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
    private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
    private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan";
    // private static final String PROPERTY_NAME_PERSISTENCE_UNIT_NAME =
    // "persistence.unit.name";

    @Resource
    private Environment environment;

    private EmbeddedDatabase db = null;

    @Bean
    public DataSource dataSource() {
        final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        db = builder.setType(EmbeddedDatabaseType.H2).build(); // .script("foo.sql")
        return db;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws ClassNotFoundException {
        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();

        bean.setDataSource(dataSource());
        bean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN));
        bean.setPersistenceProviderClass(HibernatePersistence.class);
        // bean.setPersistenceUnitName(environment
        // .getRequiredProperty(PROPERTY_NAME_PERSISTENCE_UNIT_NAME));

        HibernateJpaVendorAdapter va = new HibernateJpaVendorAdapter();
        bean.setJpaVendorAdapter(va);

        Properties jpaProperties = new Properties();
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT,
environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO));

        bean.setJpaProperties(jpaProperties);

        return bean;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();

        try {
            tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
        } catch (ClassNotFoundException e) {
            // TODO: log.
        }

        return tm;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

    @Override
    public void destroy() {
        if (db != null) {
            db.shutdown();
        }
    }
}

The applicationContext.xml file is empty. The properties file is

  1. # hibernate configuration
  2. hibernate.dialect=org.hibernate.dialect.H2Dialect
  3. hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
  4. hibernate.show_sql=false
  5. hibernate.format_sql=true
  6. hibernate.hbm2ddl.auto=create
  7.  
  8. # jpa configuration
  9. entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
  10. persistence.unit.dataSource=java:comp/env/jdbc/ssDS
  11. persistence.unit.name=ssPU
# hibernate configuration
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.show_sql=false
hibernate.format_sql=true
hibernate.hbm2ddl.auto=create

# jpa configuration
entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
persistence.unit.dataSource=java:comp/env/jdbc/ssDS
persistence.unit.name=ssPU

Unit Testing

There are no unit tests since all of the code is autogenerated. This will change as custom methods are added.

Integration Testing

We can now write the integration tests for the business layer.

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
  3.         TestPersistenceJpaConfig.class })
  4. @Transactional
  5. @TransactionConfiguration(defaultRollback = true)
  6. public class CourseServiceIntegrationTest {
  7.  
  8.     @Resource
  9.     private CourseService dao;
  10.  
  11.     @Test
  12.     public void testCourseLifecycle() throws Exception {
  13.         final String name = "Calculus 101";
  14.  
  15.         final Course expected = new Course();
  16.         expected.setName(name);
  17.  
  18.         assertNull(expected.getId());
  19.  
  20.         // create course
  21.         Course actual = dao.createCourse(name);
  22.         expected.setId(actual.getId());
  23.         expected.setUuid(actual.getUuid());
  24.         expected.setCreationDate(actual.getCreationDate());
  25.  
  26.         assertThat(expected, equalTo(actual));
  27.         assertNotNull(actual.getUuid());
  28.         assertNotNull(actual.getCreationDate());
  29.  
  30.         // get course by id
  31.         actual = dao.findCourseById(expected.getId());
  32.         assertThat(expected, equalTo(actual));
  33.  
  34.         // get course by uuid
  35.         actual = dao.findCourseByUuid(expected.getUuid());
  36.         assertThat(expected, equalTo(actual));
  37.  
  38.         // update course
  39.         expected.setName("Calculus 102");
  40.         actual = dao.updateCourse(actual, expected.getName());
  41.         assertThat(expected, equalTo(actual));
  42.  
  43.         // delete Course
  44.         dao.deleteCourse(expected.getUuid());
  45.         try {
  46.             dao.findCourseByUuid(expected.getUuid());
  47.             fail("exception expected");
  48.         } catch (ObjectNotFoundException e) {
  49.             // expected
  50.         }
  51.     }
  52.  
  53.     /**
  54.      * @test findCourseById() with unknown course.
  55.      */
  56.     @Test(expected = ObjectNotFoundException.class)
  57.     public void testfindCourseByIdWhenCourseIsNotKnown() {
  58.         final Integer id = 1;
  59.         dao.findCourseById(id);
  60.     }
  61.  
  62.     /**
  63.      * @test findCourseByUuid() with unknown Course.
  64.      */
  65.     @Test(expected = ObjectNotFoundException.class)
  66.     public void testfindCourseByUuidWhenCourseIsNotKnown() {
  67.         final String uuid = "missing";
  68.         dao.findCourseByUuid(uuid);
  69.     }
  70.  
  71.     /**
  72.      * Test updateCourse() with unknown course.
  73.      *
  74.      * @throws ObjectNotFoundException
  75.      */
  76.     @Test(expected = ObjectNotFoundException.class)
  77.     public void testUpdateCourseWhenCourseIsNotFound() {
  78.         final Course course = new Course();
  79.         course.setUuid("missing");
  80.         dao.updateCourse(course, "Calculus 102");
  81.     }
  82.  
  83.     /**
  84.      * Test deleteCourse() with unknown course.
  85.      *
  86.      * @throws ObjectNotFoundException
  87.      */
  88.     @Test(expected = ObjectNotFoundException.class)
  89.     public void testDeleteCourseWhenCourseIsNotFound() {
  90.         dao.deleteCourse("missing");
  91.     }
  92. }
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
        TestPersistenceJpaConfig.class })
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class CourseServiceIntegrationTest {

    @Resource
    private CourseService dao;

    @Test
    public void testCourseLifecycle() throws Exception {
        final String name = "Calculus 101";

        final Course expected = new Course();
        expected.setName(name);

        assertNull(expected.getId());

        // create course
        Course actual = dao.createCourse(name);
        expected.setId(actual.getId());
        expected.setUuid(actual.getUuid());
        expected.setCreationDate(actual.getCreationDate());

        assertThat(expected, equalTo(actual));
        assertNotNull(actual.getUuid());
        assertNotNull(actual.getCreationDate());

        // get course by id
        actual = dao.findCourseById(expected.getId());
        assertThat(expected, equalTo(actual));

        // get course by uuid
        actual = dao.findCourseByUuid(expected.getUuid());
        assertThat(expected, equalTo(actual));

        // update course
        expected.setName("Calculus 102");
        actual = dao.updateCourse(actual, expected.getName());
        assertThat(expected, equalTo(actual));

        // delete Course
        dao.deleteCourse(expected.getUuid());
        try {
            dao.findCourseByUuid(expected.getUuid());
            fail("exception expected");
        } catch (ObjectNotFoundException e) {
            // expected
        }
    }

    /**
     * @test findCourseById() with unknown course.
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testfindCourseByIdWhenCourseIsNotKnown() {
        final Integer id = 1;
        dao.findCourseById(id);
    }

    /**
     * @test findCourseByUuid() with unknown Course.
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testfindCourseByUuidWhenCourseIsNotKnown() {
        final String uuid = "missing";
        dao.findCourseByUuid(uuid);
    }

    /**
     * Test updateCourse() with unknown course.
     * 
     * @throws ObjectNotFoundException
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testUpdateCourseWhenCourseIsNotFound() {
        final Course course = new Course();
        course.setUuid("missing");
        dao.updateCourse(course, "Calculus 102");
    }

    /**
     * Test deleteCourse() with unknown course.
     * 
     * @throws ObjectNotFoundException
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testDeleteCourseWhenCourseIsNotFound() {
        dao.deleteCourse("missing");
    }
}

Source Code

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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: Business Layer

Bear Giles | December 19, 2013

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey and Persistence with Spring Data.

The third layer of the RESTful webapp onion is the business layer. This is the guts of the application – well-written persistence and webservice layers are constrained but anything goes in the business layer.

We’re only implementing CRUD methods at this point so these methods are straightforward.

Design Decisions

Spring Data – we will be using Spring Data for the persistence layer so we will want to specify the persistence layer interface with that in mind. As we’ll see in the next blog this greatly simplifies our life.

Limitations

DataAccessException – no attempt is made to throw different exceptions according to the type of DataAccessException we receive. This will be important later – we should treat a constraint violation differently than a lost database connection.

Service Interface

First a reminder of the service interface we identified in the last post.

  1. public interface CourseService {
  2.     List<Course> findAllCourses();
  3.  
  4.     Course findCourseById(Integer id);
  5.  
  6.     Course findCourseByUuid(String uuid);
  7.  
  8.     Course createCourse(String name);
  9.  
  10.     Course updateCourse(Course course, String name);
  11.  
  12.     void deleteCourse(String uuid);
  13. }
public interface CourseService {
    List<Course> findAllCourses();

    Course findCourseById(Integer id);

    Course findCourseByUuid(String uuid);

    Course createCourse(String name);

    Course updateCourse(Course course, String name);

    void deleteCourse(String uuid);
}

Service Implementation

The service implementation goes quickly since it’s basic CRUD operations. Our only concerns are if there’s a problem with the database (DataAccessException in Spring) or if an expected value can’t be found. We need to add a new exception to our API for the former, already have an exception in the API for the latter.

  1. public class CourseServiceImpl implements CourseService {
  2.     private static final Logger log = LoggerFactory
  3.             .getLogger(CourseServiceImpl.class);
  4.  
  5.     @Resource
  6.     private CourseRepository courseRepository;
  7.  
  8.     /**
  9.      * Default constructor
  10.      */
  11.     public CourseServiceImpl() {
  12.  
  13.     }
  14.  
  15.     /**
  16.      * Constructor used in unit tests
  17.      */
  18.     CourseServiceImpl(CourseRepository courseRepository) {
  19.         this.courseRepository = courseRepository;
  20.     }
  21.  
  22.     /**
  23.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  24.      *      findAllCourses()
  25.      */
  26.     @Transactional(readOnly = true)
  27.     @Override
  28.     public List<Course> findAllCourses() {
  29.         List<Course> courses = null;
  30.  
  31.         try {
  32.             courses = courseRepository.findAll();
  33.         } catch (DataAccessException e) {
  34.             if (!(e instanceof UnitTestException)) {
  35.                 log.info("error loading list of courses: " + e.getMessage(), e);
  36.             }
  37.             throw new PersistenceException("unable to get list of courses.", e);
  38.         }
  39.  
  40.         return courses;
  41.     }
  42.  
  43.     /**
  44.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  45.      *      findCourseById(java.lang.Integer)
  46.      */
  47.     @Transactional(readOnly = true)
  48.     @Override
  49.     public Course findCourseById(Integer id) {
  50.         Course course = null;
  51.         try {
  52.             course = courseRepository.findOne(id);
  53.         } catch (DataAccessException e) {
  54.             if (!(e instanceof UnitTestException)) {
  55.                 log.info("internal error retrieving course: " + id, e);
  56.             }
  57.             throw new PersistenceException("unable to find course by id", e, id);
  58.         }
  59.  
  60.         if (course == null) {
  61.             throw new ObjectNotFoundException(id);
  62.         }
  63.  
  64.         return course;
  65.     }
  66.  
  67.     /**
  68.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  69.      *      findCourseByUuid(java.lang.String)
  70.      */
  71.     @Transactional(readOnly = true)
  72.     @Override
  73.     public Course findCourseByUuid(String uuid) {
  74.         Course course = null;
  75.         try {
  76.             course = courseRepository.findCourseByUuid(uuid);
  77.         } catch (DataAccessException e) {
  78.             if (!(e instanceof UnitTestException)) {
  79.                 log.info("internal error retrieving course: " + uuid, e);
  80.             }
  81.             throw new PersistenceException("unable to find course by uuid", e,
  82.                     uuid);
  83.         }
  84.  
  85.         if (course == null) {
  86.             throw new ObjectNotFoundException(uuid);
  87.         }
  88.  
  89.         return course;
  90.     }
  91.  
  92.     /**
  93.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  94.      *      createCourse(java.lang.String)
  95.      */
  96.     @Transactional
  97.     @Override
  98.     public Course createCourse(String name) {
  99.         final Course course = new Course();
  100.         course.setName(name);
  101.  
  102.         Course actual = null;
  103.         try {
  104.             actual = courseRepository.saveAndFlush(course);
  105.         } catch (DataAccessException e) {
  106.             if (!(e instanceof UnitTestException)) {
  107.                 log.info("internal error retrieving course: " + name, e);
  108.             }
  109.             throw new PersistenceException("unable to create course", e);
  110.         }
  111.  
  112.         return actual;
  113.     }
  114.  
  115.     /**
  116.      * @see com.invariantproperties.sandbox.course.persistence.CourseService#
  117.      *      updateCourse(com.invariantproperties.sandbox.course.domain.Course,
  118.      *      java.lang.String)
  119.      */
  120.     @Transactional
  121.     @Override
  122.     public Course updateCourse(Course course, String name) {
  123.         Course updated = null;
  124.         try {
  125.             final Course actual = courseRepository.findCourseByUuid(course
  126.                     .getUuid());
  127.  
  128.             if (actual == null) {
  129.                 log.debug("did not find course: " + course.getUuid());
  130.                 throw new ObjectNotFoundException(course.getUuid());
  131.             }
  132.  
  133.             actual.setName(name);
  134.             updated = courseRepository.saveAndFlush(actual);
  135.             course.setName(name);
  136.  
  137.         } catch (DataAccessException e) {
  138.             if (!(e instanceof UnitTestException)) {
  139.                 log.info("internal error deleting course: " + course.getUuid(),
  140.                         e);
  141.             }
  142.             throw new PersistenceException("unable to delete course", e,
  143.                     course.getUuid());
  144.         }
  145.  
  146.         return updated;
  147.     }
  148.  
  149.     /**
  150.      * @see com.invariantproperties.sandbox.student.business.CourseService#
  151.      *      deleteCourse(java.lang.String)
  152.      */
  153.     @Transactional
  154.     @Override
  155.     public void deleteCourse(String uuid) {
  156.         Course course = null;
  157.         try {
  158.             course = courseRepository.findCourseByUuid(uuid);
  159.  
  160.             if (course == null) {
  161.                 log.debug("did not find course: " + uuid);
  162.                 throw new ObjectNotFoundException(uuid);
  163.             }
  164.             courseRepository.delete(course);
  165.  
  166.         } catch (DataAccessException e) {
  167.             if (!(e instanceof UnitTestException)) {
  168.                 log.info("internal error deleting course: " + uuid, e);
  169.             }
  170.             throw new PersistenceException("unable to delete course", e, uuid);
  171.         }
  172.     }
  173. }
public class CourseServiceImpl implements CourseService {
    private static final Logger log = LoggerFactory
            .getLogger(CourseServiceImpl.class);

    @Resource
    private CourseRepository courseRepository;

    /**
     * Default constructor
     */
    public CourseServiceImpl() {

    }

    /**
     * Constructor used in unit tests
     */
    CourseServiceImpl(CourseRepository courseRepository) {
        this.courseRepository = courseRepository;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findAllCourses()
     */
    @Transactional(readOnly = true)
    @Override
    public List<Course> findAllCourses() {
        List<Course> courses = null;

        try {
            courses = courseRepository.findAll();
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("error loading list of courses: " + e.getMessage(), e);
            }
            throw new PersistenceException("unable to get list of courses.", e);
        }

        return courses;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findCourseById(java.lang.Integer)
     */
    @Transactional(readOnly = true)
    @Override
    public Course findCourseById(Integer id) {
        Course course = null;
        try {
            course = courseRepository.findOne(id);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving course: " + id, e);
            }
            throw new PersistenceException("unable to find course by id", e, id);
        }

        if (course == null) {
            throw new ObjectNotFoundException(id);
        }

        return course;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      findCourseByUuid(java.lang.String)
     */
    @Transactional(readOnly = true)
    @Override
    public Course findCourseByUuid(String uuid) {
        Course course = null;
        try {
            course = courseRepository.findCourseByUuid(uuid);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving course: " + uuid, e);
            }
            throw new PersistenceException("unable to find course by uuid", e,
                    uuid);
        }

        if (course == null) {
            throw new ObjectNotFoundException(uuid);
        }

        return course;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      createCourse(java.lang.String)
     */
    @Transactional
    @Override
    public Course createCourse(String name) {
        final Course course = new Course();
        course.setName(name);

        Course actual = null;
        try {
            actual = courseRepository.saveAndFlush(course);
        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error retrieving course: " + name, e);
            }
            throw new PersistenceException("unable to create course", e);
        }

        return actual;
    }

    /**
     * @see com.invariantproperties.sandbox.course.persistence.CourseService#
     *      updateCourse(com.invariantproperties.sandbox.course.domain.Course,
     *      java.lang.String)
     */
    @Transactional
    @Override
    public Course updateCourse(Course course, String name) {
        Course updated = null;
        try {
            final Course actual = courseRepository.findCourseByUuid(course
                    .getUuid());

            if (actual == null) {
                log.debug("did not find course: " + course.getUuid());
                throw new ObjectNotFoundException(course.getUuid());
            }

            actual.setName(name);
            updated = courseRepository.saveAndFlush(actual);
            course.setName(name);

        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error deleting course: " + course.getUuid(),
                        e);
            }
            throw new PersistenceException("unable to delete course", e,
                    course.getUuid());
        }

        return updated;
    }

    /**
     * @see com.invariantproperties.sandbox.student.business.CourseService#
     *      deleteCourse(java.lang.String)
     */
    @Transactional
    @Override
    public void deleteCourse(String uuid) {
        Course course = null;
        try {
            course = courseRepository.findCourseByUuid(uuid);

            if (course == null) {
                log.debug("did not find course: " + uuid);
                throw new ObjectNotFoundException(uuid);
            }
            courseRepository.delete(course);

        } catch (DataAccessException e) {
            if (!(e instanceof UnitTestException)) {
                log.info("internal error deleting course: " + uuid, e);
            }
            throw new PersistenceException("unable to delete course", e, uuid);
        }
    }
}

This implementation tells us the required interface for the persistence layer.

Persistence Layer Interface

We will be using Spring Data for our persistence layer and our DAO interface is the same as a Spring Data repository. We only need one nonstandard method.

  1. @Repository
  2. public interface CourseRepository extends JpaRepository<Course, Integer> {
  3.     Course findCourseByUuid(String uuid);
  4. }
@Repository
public interface CourseRepository extends JpaRepository<Course, Integer> {
    Course findCourseByUuid(String uuid);
}

Unit Testing

The unit tests also go quickly since we only need to consider three cases (at most) – the happy path, a database problem, and (sometimes) a nonexistent value.

  1. public class CourseServiceImplTest {
  2.  
  3.     @Test
  4.     public void testFindAllCourses() {
  5.         final List<Course> expected = Collections.emptyList();
  6.  
  7.         final CourseRepository repository = Mockito
  8.                 .mock(CourseRepository.class);
  9.         when(repository.findAll()).thenReturn(expected);
  10.  
  11.         final CourseService service = new CourseServiceImpl(repository);
  12.         final List<Course> actual = service.findAllCourses();
  13.  
  14.         assertEquals(expected, actual);
  15.     }
  16.  
  17.     @Test(expected = PersistenceException.class)
  18.     public void testFindAllCoursesError() {
  19.         final List<Course> expected = Collections.emptyList();
  20.  
  21.         final CourseRepository repository = Mockito
  22.                 .mock(CourseRepository.class);
  23.         when(repository.findAll()).thenThrow(new UnitTestException());
  24.  
  25.         final CourseService service = new CourseServiceImpl(repository);
  26.         final List<Course> actual = service.findAllCourses();
  27.  
  28.         assertEquals(expected, actual);
  29.     }
  30.  
  31.     @Test
  32.     public void testFindCourseById() {
  33.         final Course expected = new Course();
  34.         expected.setId(1);
  35.  
  36.         final CourseRepository repository = Mockito
  37.                 .mock(CourseRepository.class);
  38.         when(repository.findOne(any(Integer.class))).thenReturn(expected);
  39.  
  40.         final CourseService service = new CourseServiceImpl(repository);
  41.         final Course actual = service.findCourseById(expected.getId());
  42.  
  43.         assertEquals(expected, actual);
  44.     }
  45.  
  46.     @Test(expected = ObjectNotFoundException.class)
  47.     public void testFindCourseByIdMissing() {
  48.         final CourseRepository repository = Mockito
  49.                 .mock(CourseRepository.class);
  50.         when(repository.findOne(any(Integer.class))).thenReturn(null);
  51.  
  52.         final CourseService service = new CourseServiceImpl(repository);
  53.         service.findCourseById(1);
  54.     }
  55.  
  56.     @Test(expected = PersistenceException.class)
  57.     public void testFindCourseByIdError() {
  58.         final CourseRepository repository = Mockito
  59.                 .mock(CourseRepository.class);
  60.         when(repository.findOne(any(Integer.class))).thenThrow(
  61.                 new UnitTestException());
  62.  
  63.         final CourseService service = new CourseServiceImpl(repository);
  64.         service.findCourseById(1);
  65.     }
  66.  
  67.     @Test
  68.     public void testFindCourseByUuid() {
  69.         final Course expected = new Course();
  70.         expected.setUuid("[uuid]");
  71.  
  72.         final CourseRepository repository = Mockito
  73.                 .mock(CourseRepository.class);
  74.         when(repository.findCourseByUuid(any(String.class))).thenReturn(
  75.                 expected);
  76.  
  77.         final CourseService service = new CourseServiceImpl(repository);
  78.         final Course actual = service.findCourseByUuid(expected.getUuid());
  79.  
  80.         assertEquals(expected, actual);
  81.     }
  82.  
  83.     @Test(expected = ObjectNotFoundException.class)
  84.     public void testFindCourseByUuidMissing() {
  85.         final CourseRepository repository = Mockito
  86.                 .mock(CourseRepository.class);
  87.         when(repository.findCourseByUuid(any(String.class))).thenReturn(null);
  88.  
  89.         final CourseService service = new CourseServiceImpl(repository);
  90.         service.findCourseByUuid("[uuid]");
  91.     }
  92.  
  93.     @Test(expected = PersistenceException.class)
  94.     public void testFindCourseByUuidError() {
  95.         final CourseRepository repository = Mockito
  96.                 .mock(CourseRepository.class);
  97.         when(repository.findCourseByUuid(any(String.class))).thenThrow(
  98.                 new UnitTestException());
  99.  
  100.         final CourseService service = new CourseServiceImpl(repository);
  101.         service.findCourseByUuid("[uuid]");
  102.     }
  103.  
  104.     @Test
  105.     public void testCreateCourse() {
  106.         final Course expected = new Course();
  107.         expected.setName("name");
  108.         expected.setUuid("[uuid]");
  109.  
  110.         final CourseRepository repository = Mockito
  111.                 .mock(CourseRepository.class);
  112.         when(repository.saveAndFlush(any(Course.class))).thenReturn(expected);
  113.  
  114.         final CourseService service = new CourseServiceImpl(repository);
  115.         final Course actual = service.createCourse(expected.getName());
  116.  
  117.         assertEquals(expected, actual);
  118.     }
  119.  
  120.     @Test(expected = PersistenceException.class)
  121.     public void testCreateCourseError() {
  122.         final CourseRepository repository = Mockito
  123.                 .mock(CourseRepository.class);
  124.         when(repository.saveAndFlush(any(Course.class))).thenThrow(
  125.                 new UnitTestException());
  126.  
  127.         final CourseService service = new CourseServiceImpl(repository);
  128.         service.createCourse("name");
  129.     }
  130.  
  131.     @Test
  132.     public void testUpdateCourse() {
  133.         final Course expected = new Course();
  134.         expected.setName("Alice");
  135.         expected.setUuid("[uuid]");
  136.  
  137.         final CourseRepository repository = Mockito
  138.                 .mock(CourseRepository.class);
  139.         when(repository.findCourseByUuid(any(String.class))).thenReturn(
  140.                 expected);
  141.         when(repository.saveAndFlush(any(Course.class))).thenReturn(expected);
  142.  
  143.         final CourseService service = new CourseServiceImpl(repository);
  144.         final Course actual = service.updateCourse(expected, "Bob");
  145.  
  146.         assertEquals("Bob", actual.getName());
  147.     }
  148.  
  149.     @Test(expected = ObjectNotFoundException.class)
  150.     public void testUpdateCourseMissing() {
  151.         final Course expected = new Course();
  152.         final CourseRepository repository = Mockito
  153.                 .mock(CourseRepository.class);
  154.         when(repository.findCourseByUuid(any(String.class))).thenReturn(null);
  155.  
  156.         final CourseService service = new CourseServiceImpl(repository);
  157.         service.updateCourse(expected, "Bob");
  158.     }
  159.  
  160.     @Test(expected = PersistenceException.class)
  161.     public void testUpdateCourseError() {
  162.         final Course expected = new Course();
  163.         expected.setUuid("[uuid]");
  164.  
  165.         final CourseRepository repository = Mockito
  166.                 .mock(CourseRepository.class);
  167.         when(repository.findCourseByUuid(any(String.class))).thenReturn(
  168.                 expected);
  169.         doThrow(new UnitTestException()).when(repository).saveAndFlush(
  170.                 any(Course.class));
  171.  
  172.         final CourseService service = new CourseServiceImpl(repository);
  173.         service.updateCourse(expected, "Bob");
  174.     }
  175.  
  176.     @Test
  177.     public void testDeleteCourse() {
  178.         final Course expected = new Course();
  179.         expected.setUuid("[uuid]");
  180.  
  181.         final CourseRepository repository = Mockito
  182.                 .mock(CourseRepository.class);
  183.         when(repository.findCourseByUuid(any(String.class))).thenReturn(
  184.                 expected);
  185.         doNothing().when(repository).delete(any(Course.class));
  186.  
  187.         final CourseService service = new CourseServiceImpl(repository);
  188.         service.deleteCourse(expected.getUuid());
  189.     }
  190.  
  191.     @Test(expected = ObjectNotFoundException.class)
  192.     public void testDeleteCourseMissing() {
  193.         final CourseRepository repository = Mockito
  194.                 .mock(CourseRepository.class);
  195.         when(repository.findCourseByUuid(any(String.class))).thenReturn(null);
  196.  
  197.         final CourseService service = new CourseServiceImpl(repository);
  198.         service.deleteCourse("[uuid]");
  199.     }
  200.  
  201.     @Test(expected = PersistenceException.class)
  202.     public void testDeleteCourseError() {
  203.         final Course expected = new Course();
  204.         expected.setUuid("[uuid]");
  205.  
  206.         final CourseRepository repository = Mockito
  207.                 .mock(CourseRepository.class);
  208.         when(repository.findCourseByUuid(any(String.class))).thenReturn(
  209.                 expected);
  210.         doThrow(new UnitTestException()).when(repository).delete(
  211.                 any(Course.class));
  212.  
  213.         final CourseService service = new CourseServiceImpl(repository);
  214.         service.deleteCourse(expected.getUuid());
  215.     }
  216. }
public class CourseServiceImplTest {

    @Test
    public void testFindAllCourses() {
        final List<Course> expected = Collections.emptyList();

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findAll()).thenReturn(expected);

        final CourseService service = new CourseServiceImpl(repository);
        final List<Course> actual = service.findAllCourses();

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testFindAllCoursesError() {
        final List<Course> expected = Collections.emptyList();

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findAll()).thenThrow(new UnitTestException());

        final CourseService service = new CourseServiceImpl(repository);
        final List<Course> actual = service.findAllCourses();

        assertEquals(expected, actual);
    }

    @Test
    public void testFindCourseById() {
        final Course expected = new Course();
        expected.setId(1);

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findOne(any(Integer.class))).thenReturn(expected);

        final CourseService service = new CourseServiceImpl(repository);
        final Course actual = service.findCourseById(expected.getId());

        assertEquals(expected, actual);
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testFindCourseByIdMissing() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findOne(any(Integer.class))).thenReturn(null);

        final CourseService service = new CourseServiceImpl(repository);
        service.findCourseById(1);
    }

    @Test(expected = PersistenceException.class)
    public void testFindCourseByIdError() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findOne(any(Integer.class))).thenThrow(
                new UnitTestException());

        final CourseService service = new CourseServiceImpl(repository);
        service.findCourseById(1);
    }

    @Test
    public void testFindCourseByUuid() {
        final Course expected = new Course();
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(
                expected);

        final CourseService service = new CourseServiceImpl(repository);
        final Course actual = service.findCourseByUuid(expected.getUuid());

        assertEquals(expected, actual);
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testFindCourseByUuidMissing() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(null);

        final CourseService service = new CourseServiceImpl(repository);
        service.findCourseByUuid("[uuid]");
    }

    @Test(expected = PersistenceException.class)
    public void testFindCourseByUuidError() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenThrow(
                new UnitTestException());

        final CourseService service = new CourseServiceImpl(repository);
        service.findCourseByUuid("[uuid]");
    }

    @Test
    public void testCreateCourse() {
        final Course expected = new Course();
        expected.setName("name");
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.saveAndFlush(any(Course.class))).thenReturn(expected);

        final CourseService service = new CourseServiceImpl(repository);
        final Course actual = service.createCourse(expected.getName());

        assertEquals(expected, actual);
    }

    @Test(expected = PersistenceException.class)
    public void testCreateCourseError() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.saveAndFlush(any(Course.class))).thenThrow(
                new UnitTestException());

        final CourseService service = new CourseServiceImpl(repository);
        service.createCourse("name");
    }

    @Test
    public void testUpdateCourse() {
        final Course expected = new Course();
        expected.setName("Alice");
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(
                expected);
        when(repository.saveAndFlush(any(Course.class))).thenReturn(expected);

        final CourseService service = new CourseServiceImpl(repository);
        final Course actual = service.updateCourse(expected, "Bob");

        assertEquals("Bob", actual.getName());
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testUpdateCourseMissing() {
        final Course expected = new Course();
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(null);

        final CourseService service = new CourseServiceImpl(repository);
        service.updateCourse(expected, "Bob");
    }

    @Test(expected = PersistenceException.class)
    public void testUpdateCourseError() {
        final Course expected = new Course();
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(
                expected);
        doThrow(new UnitTestException()).when(repository).saveAndFlush(
                any(Course.class));

        final CourseService service = new CourseServiceImpl(repository);
        service.updateCourse(expected, "Bob");
    }

    @Test
    public void testDeleteCourse() {
        final Course expected = new Course();
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(
                expected);
        doNothing().when(repository).delete(any(Course.class));

        final CourseService service = new CourseServiceImpl(repository);
        service.deleteCourse(expected.getUuid());
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testDeleteCourseMissing() {
        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(null);

        final CourseService service = new CourseServiceImpl(repository);
        service.deleteCourse("[uuid]");
    }

    @Test(expected = PersistenceException.class)
    public void testDeleteCourseError() {
        final Course expected = new Course();
        expected.setUuid("[uuid]");

        final CourseRepository repository = Mockito
                .mock(CourseRepository.class);
        when(repository.findCourseByUuid(any(String.class))).thenReturn(
                expected);
        doThrow(new UnitTestException()).when(repository).delete(
                any(Course.class));

        final CourseService service = new CourseServiceImpl(repository);
        service.deleteCourse(expected.getUuid());
    }
}

Integration Testing

Integration testing has been deferred until the persistence layer is implemented.

Source Code

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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Broken Eclipse Menubar in Ubuntu 13.10

Bear Giles | December 18, 2013

The eclipse menubar in Ubuntu 13.10 is broken. It appears but it’s inert – you can’t pull down anything.

Fortunately there’s a simple fix. We only need to create one file and then load the app via Dash. The configuration below is for Spring Tool Suite and easily modified for other versions of eclipse.

/usr/share/applications/eclipse.desktop

  1. [Desktop Entry]
  2. Version=3.4.0
  3. Name=Eclipse
  4. Comment=IDE for all seasons
  5. Exec=env UBUNTU_MENUPROXY=0 /opt/springsource/sts-3.4.0.RELEASE/STS
  6. Icon=/opt/springsource/sts-3.4.0.RELEASE/icon.xpm
  7. Terminal=false
  8. Type=Application
  9. Categories=Utility;Application
[Desktop Entry]
Version=3.4.0
Name=Eclipse
Comment=IDE for all seasons
Exec=env UBUNTU_MENUPROXY=0 /opt/springsource/sts-3.4.0.RELEASE/STS
Icon=/opt/springsource/sts-3.4.0.RELEASE/icon.xpm
Terminal=false
Type=Application
Categories=Utility;Application
Comments
No Comments »
Categories
java, linux
Comments rss Comments rss
Trackback Trackback

Project Student: Webservice Server with Jersey

Bear Giles | December 18, 2013

This is part of Project Student. Other posts are Webservice Client With Jersey, Business Layer and Persistence with Spring Data.

The second layer of the RESTful webapp onion is the webservice server. It should be a thin layer that wraps calls to the business layer but does not do significant processing of its own. This post has a lot of code but it’s mostly test classes.

Design Decisions

Jersey – I use Jersey for the REST server. I considered alternatives – Spring MVC, Netty, etc., but decided to go with Jersey for the same reason as the client. It’s lightweight and doesn’t constrain the developer.

Dependency Injection – I need dependency injection and that means I need to decide on a framework: Spring, EJB3, Guice, etc. I already know that I’ll be using Spring Data for the persistence layer so it’s a no-brainer to use the spring framework. I will still be careful to minimize any dependencies (ha!) on that framework for maximum flexibility.

Limitations

Jersey – I don’t know how well Jersey will handle high loads. This is a key reason why the REST server must be a thin wrapper around the business layer – it will be relatively painless to change libraries if it becomes necessary.

User Permissions – there is no attempt to restrict access to certain methods to specific users or hosts. This should be handled by the business layer with security exceptions translated to FORBIDDEN status codes by the REST server.

Jersey REST Server

One of our early design documents is the REST API. For the server this means that we implement the layer from the REST server down instead of from the business layer API up. In fact the REST server defines the necessary methods in the business layer API.

There is one small deviation from the standard REST CRUD API: objects are created with a POST instead of a PUT since the semantics of the latter is that the object is created exactly as provided. We can’t do that – for security reasons we never expose our internal ID and must never accept a user-defined UUID. That means we’ll violate the REST API contract so we use a POST instead.

There is also one small cheat: the CRUD contract only requires the ability to create or update objects. This means that we can figure out the required action given just the path – we don’t need to add a specific ‘action’ field. This may change as we extend the implementation to include more than just CRUD actions.

Onward to the code…

  1. @Service
  2. @Path("/course")
  3. public class CourseResource extends AbstractResource {
  4.     private static final Logger log = Logger.getLogger(CourseResource.class);
  5.     private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];
  6.  
  7.     @Context
  8.     UriInfo uriInfo;
  9.  
  10.     @Context
  11.     Request request;
  12.  
  13.     @Resource
  14.     private CourseService service;
  15.  
  16.     /**
  17.      * Default constructor.
  18.      */
  19.     public CourseResource() {
  20.  
  21.     }
  22.  
  23.     /**
  24.      * Unit test constructor.
  25.      *
  26.      * @param service
  27.      */
  28.     CourseResource(CourseService service) {
  29.         this.service = service;
  30.     }
  31.  
  32.     /**
  33.      * Get all Courses.
  34.      *
  35.      * @return
  36.      */
  37.     @GET
  38.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  39.     public Response findAllCourses() {
  40.         log.debug("CourseResource: findAllCourses()");
  41.  
  42.         Response response = null;
  43.         try {
  44.             List<Course> courses = service.findAllCourses();
  45.  
  46.             List<Course> results = new ArrayList<Course>(courses.size());
  47.             for (Course course : courses) {
  48.                 results.add(scrubCourse(course));
  49.             }
  50.  
  51.             response = Response.ok(results.toArray(EMPTY_COURSE_ARRAY)).build();
  52.         } catch (Exception e) {
  53.             if (!(e instanceof UnitTestException)) {
  54.                 log.info("unhandled exception", e);
  55.             }
  56.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  57.         }
  58.  
  59.         return response;
  60.     }
  61.  
  62.     /**
  63.      * Create a Course.
  64.      *
  65.      * @param req
  66.      * @return
  67.      */
  68.     @POST
  69.     @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  70.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  71.     public Response createCourse(Name req) {
  72.         log.debug("CourseResource: createCourse()");
  73.  
  74.         final String name = req.getName();
  75.         if ((name == null) || name.isEmpty()) {
  76.             return Response.status(Status.BAD_REQUEST).entity("'name' is required").build();
  77.         }
  78.  
  79.         Response response = null;
  80.  
  81.         try {
  82.             Course course = service.createCourse(name);
  83.             if (course == null) {
  84.                 response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  85.             } else {
  86.                 response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
  87.             }
  88.         } catch (Exception e) {
  89.             if (!(e instanceof UnitTestException)) {
  90.                 log.info("unhandled exception", e);
  91.             }
  92.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  93.         }
  94.  
  95.         return response;
  96.     }
  97.  
  98.     /**
  99.      * Get a specific Course.
  100.      *
  101.      * @param uuid
  102.      * @return
  103.      */
  104.     @Path("/{courseId}")
  105.     @GET
  106.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  107.     public Response getCourse(@PathParam("courseId") String id) {
  108.         log.debug("CourseResource: getCourse()");
  109.  
  110.         Response response = null;
  111.         try {
  112.             Course course = service.findCourseByUuid(id);
  113.             response = Response.ok(scrubCourse(course)).build();
  114.         } catch (ObjectNotFoundException e) {
  115.             response = Response.status(Status.NOT_FOUND).build();
  116.         } catch (Exception e) {
  117.             if (!e instanceof UnitTestException)) {
  118.                 log.info("unhandled exception", e);
  119.             }
  120.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  121.         }
  122.  
  123.         return response;
  124.     }
  125.  
  126.     /**
  127.      * Update a Course.
  128.      *
  129.      * FIXME: what about uniqueness violations?
  130.      *
  131.      * @param id
  132.      * @param req
  133.      * @return
  134.      */
  135.     @Path("/{courseId}")
  136.     @POST
  137.     @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  138.     @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
  139.     public Response updateCourse(@PathParam("courseId") String id, Name req) {
  140.         log.debug("CourseResource: updateCourse()");
  141.  
  142.         final String name = req.getName();
  143.         if ((name == null) || name.isEmpty()) {
  144.             return Response.status(Status.BAD_REQUEST).entity("'name' is required").build();
  145.         }
  146.  
  147.         Response response = null;
  148.         try {
  149.             final Course course = service.findCourseByUuid(id);
  150.             final Course updatedCourse = service.updateCourse(course, name);
  151.             response = Response.ok(scrubCourse(updatedCourse)).build();
  152.         } catch (ObjectNotFoundException exception) {
  153.             response = Response.status(Status.NOT_FOUND).build();
  154.         } catch (Exception e) {
  155.             if (!(e instanceof UnitTestException)) {
  156.                 log.info("unhandled exception", e);
  157.             }
  158.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  159.         }
  160.  
  161.         return response;
  162.     }
  163.  
  164.     /**
  165.      * Delete a Course.
  166.      *
  167.      * @param id
  168.      * @return
  169.      */
  170.     @Path("/{courseId}")
  171.     @DELETE
  172.     public Response deleteCourse(@PathParam("courseId") String id) {
  173.         log.debug("CourseResource: deleteCourse()");
  174.  
  175.         Response response = null;
  176.         try {
  177.             service.deleteCourse(id);
  178.             response = Response.noContent().build();
  179.         } catch (ObjectNotFoundException exception) {
  180.             response = Response.noContent().build();
  181.         } catch (Exception e) {
  182.             if (!(e instanceof UnitTestException)) {
  183.                 log.info("unhandled exception", e);
  184.             }
  185.             response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
  186.         }
  187.  
  188.         return response;
  189.     }
  190. }
@Service
@Path("/course")
public class CourseResource extends AbstractResource {
    private static final Logger log = Logger.getLogger(CourseResource.class);
    private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];

    @Context
    UriInfo uriInfo;

    @Context
    Request request;

    @Resource
    private CourseService service;

    /**
     * Default constructor.
     */
    public CourseResource() {

    }

    /**
     * Unit test constructor.
     * 
     * @param service
     */
    CourseResource(CourseService service) {
        this.service = service;
    }

    /**
     * Get all Courses.
     * 
     * @return
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response findAllCourses() {
        log.debug("CourseResource: findAllCourses()");

        Response response = null;
        try {
            List<Course> courses = service.findAllCourses();

            List<Course> results = new ArrayList<Course>(courses.size());
            for (Course course : courses) {
                results.add(scrubCourse(course));
            }

            response = Response.ok(results.toArray(EMPTY_COURSE_ARRAY)).build();
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }

    /**
     * Create a Course.
     * 
     * @param req
     * @return
     */
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response createCourse(Name req) {
        log.debug("CourseResource: createCourse()");

        final String name = req.getName();
        if ((name == null) || name.isEmpty()) {
            return Response.status(Status.BAD_REQUEST).entity("'name' is required").build();
        }

        Response response = null;

        try {
            Course course = service.createCourse(name);
            if (course == null) {
                response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            } else {
                response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();
            }
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }

    /**
     * Get a specific Course.
     * 
     * @param uuid
     * @return
     */
    @Path("/{courseId}")
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response getCourse(@PathParam("courseId") String id) {
        log.debug("CourseResource: getCourse()");

        Response response = null;
        try {
            Course course = service.findCourseByUuid(id);
            response = Response.ok(scrubCourse(course)).build();
        } catch (ObjectNotFoundException e) {
            response = Response.status(Status.NOT_FOUND).build();
        } catch (Exception e) {
            if (!e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }

    /**
     * Update a Course.
     * 
     * FIXME: what about uniqueness violations?
     * 
     * @param id
     * @param req
     * @return
     */
    @Path("/{courseId}")
    @POST
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response updateCourse(@PathParam("courseId") String id, Name req) {
        log.debug("CourseResource: updateCourse()");

        final String name = req.getName();
        if ((name == null) || name.isEmpty()) {
            return Response.status(Status.BAD_REQUEST).entity("'name' is required").build();
        }

        Response response = null;
        try {
            final Course course = service.findCourseByUuid(id);
            final Course updatedCourse = service.updateCourse(course, name);
            response = Response.ok(scrubCourse(updatedCourse)).build();
        } catch (ObjectNotFoundException exception) {
            response = Response.status(Status.NOT_FOUND).build();
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }

    /**
     * Delete a Course.
     * 
     * @param id
     * @return
     */
    @Path("/{courseId}")
    @DELETE
    public Response deleteCourse(@PathParam("courseId") String id) {
        log.debug("CourseResource: deleteCourse()");

        Response response = null;
        try {
            service.deleteCourse(id);
            response = Response.noContent().build();
        } catch (ObjectNotFoundException exception) {
            response = Response.noContent().build();
        } catch (Exception e) {
            if (!(e instanceof UnitTestException)) {
                log.info("unhandled exception", e);
            }
            response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        return response;
    }
}

The implementation tells us that we need three things:

  • a service API (CourseService)
  • request parameter classes (Name)
  • a scrubber (scrubCourse)

I did not show full logging. Request parameters must be scrubbed to avoid log contamination.. As a simple example consider using a logger that writes to a SQL database for the ease of analysis. A naive implementation of this logger – one that does not use positional parameters – would permit SQL injection via carefully crafted request parameters!

OWASP ESAPI contains methods that can be used for log scrubbing. I haven’t included here since it’s a bit of a pain to set up. (It should be in the checked-in code soon.)

Why would you log to a database? One good practice is to log all unhandled exceptions that reach the server layer – you never want to rely on the user to report problems and errors written to the log files are easily overlooked. In contrast reports written to a database are easy to check with simple tools.

Advanced developers can even create new bug reports when unhandled exceptions occur. In this case it’s critical to maintain a separate exception database to avoid submitting duplicate entries and overwhelming developers. (The database can contain details for each exception but the bug reporting system should only have one bug report per exception class + stack trace.)

Service API

The service API for CRUD operations is straightforward.

  1. public interface CourseService {
  2.     List<Course> findAllCourses();
  3.  
  4.     Course findCourseById(Integer id);
  5.  
  6.     Course findCourseByUuid(String uuid);
  7.  
  8.     Course createCourse(String name);
  9.  
  10.     Course updateCourse(Course course, String name);
  11.  
  12.     void deleteCourse(String uuid);
  13. }
public interface CourseService {
    List<Course> findAllCourses();

    Course findCourseById(Integer id);

    Course findCourseByUuid(String uuid);

    Course createCourse(String name);

    Course updateCourse(Course course, String name);

    void deleteCourse(String uuid);
}

The API also includes an ObjectNotFoundException. (This should be expanded to include the type of the object that could not be found.)

  1. public class ObjectNotFoundException extends RuntimeException {
  2.     private static final long serialVersionUID = 1L;
  3.    
  4.     private final String uuid;
  5.    
  6.     public ObjectNotFoundException(String uuid) {
  7.         super("object not found: [" + uuid + "]");
  8.         this.uuid = uuid;
  9.     }
  10.    
  11.     public String getUuid() {
  12.         return uuid;
  13.     }
  14. }
public class ObjectNotFoundException extends RuntimeException {
	private static final long serialVersionUID = 1L;
	
	private final String uuid;
	
	public ObjectNotFoundException(String uuid) {
		super("object not found: [" + uuid + "]");
		this.uuid = uuid;
	}
	
	public String getUuid() {
		return uuid;
	}
}

As mentioned above we’ll also want an UnauthorizedOperationException eventually.

Request Parameters

The request parameters are simple POJOs that encapsulate POST payloads.

  1. @XmlRootElement
  2. public class Name {
  3.     private String name;
  4.  
  5.     public String getName() {
  6.         return name;
  7.     }
  8.  
  9.     public void setName(String name) {
  10.         this.name = name;
  11.     }
  12. }
@XmlRootElement
public class Name {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Students and instructors also require email addresses.

  1. @XmlRootElement
  2. public class NameAndEmailAddress {
  3.     private String name;
  4.     private String emailAddress;
  5.  
  6.     public String getName() {
  7.         return name;
  8.     }
  9.  
  10.     public void setName(String name) {
  11.         this.name = name;
  12.     }
  13.  
  14.     public String getEmailAddress() {
  15.         return emailAddress;
  16.     }
  17.  
  18.     public void setEmailAddress(String emailAddress) {
  19.         this.emailAddress = emailAddress;
  20.     }
  21. }
@XmlRootElement
public class NameAndEmailAddress {
    private String name;
    private String emailAddress;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }
}

The final application will have a large number of request parameter classes.

Scrubber

A scrubber serves three purposes. First, it removes sensitive information that should not be provided to the client, e.g., internal database identifiers.

Second, it prevents an massive database dump due to pulling in collections. For instance a student should include a list of current sections, but each section has a list of enrolled students and instructors. Each of those students and instructors has his own list of current sections. Lather, rinse, repeat, and you end up dumping the entire database in response to a single query.

The solution is to only include shallow information about every object that can be independently queried. For instance a student will have a list of current sections but those sections will only contain UUID and name. A very good rule of thumb is that scrubbed collections should contain exactly the information that will be used in pulldown lists and presentation tables, nothing more. Presentation lists can include links (or AJAX actions) to pull in additional information as required.

Finally, it’s a good place to perform HTML encoding and scrubbing. The returned values should be scrubbed to prevent cross-site scripting (CSS) attacks.

  1. public abstract class AbstractResource {
  2.  
  3.     /**
  4.      * Scrub 'course' object.
  5.      *
  6.      * FIXME add HTML scrubbing and encoding for string values!
  7.      */
  8.     public Course scrubCourse(final Course dirty) {
  9.         final Course clean = new Course();
  10.         clean.setUuid(dirty.getUuid());
  11.         clean.setName(dirty.getName());
  12.         // clean.setSelf("resource/" + dirty.getUuid());
  13.         return clean;
  14.     }
  15. }
public abstract class AbstractResource {

    /**
     * Scrub 'course' object.
     *
     * FIXME add HTML scrubbing and encoding for string values!
     */	
    public Course scrubCourse(final Course dirty) {
        final Course clean = new Course();
        clean.setUuid(dirty.getUuid());
        clean.setName(dirty.getName());
        // clean.setSelf("resource/" + dirty.getUuid());
        return clean;
    }
}

Configuration Classes

We have two configuration classes. The first is always used by the server, the second is only used by the server during integration testing. The latter configuration (and referenced classes) are located in the integration-test source tree.

I prefer to use configuration classes (introduced in Spring 3.0) since they provide the most flexibility – e.g., I could conditionally define beans according to the user running the application or environmental variables – and allow me to still include standard configuration files.

  1. @Configuration
  2. @ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" })
  3. @ImportResource({ "classpath:applicationContext-rest.xml" })
  4. // @PropertySource("classpath:application.properties")
  5. public class RestApplicationContext {
  6.  
  7.     @Resource
  8.     private Environment environment;
  9. }
@Configuration
@ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" })
@ImportResource({ "classpath:applicationContext-rest.xml" })
// @PropertySource("classpath:application.properties")
public class RestApplicationContext {

    @Resource
    private Environment environment;
}

Spring 3.1 introduced configuration profiles. They work – but the spring-aware jersey servlet I am using appears to be unable to properly set the active profiles.

  1. @Configuration
  2. //@Profile("test")
  3. public class RestApplicationContextTest {
  4.    
  5.     @Bean
  6.     StudentService studentService() {
  7.         return new DummyStudentService();
  8.     }
  9. }
@Configuration
//@Profile("test")
public class RestApplicationContextTest {
	
    @Bean
    StudentService studentService() {
        return new DummyStudentService();
    }
}

web.xml

We now have enough to implement our web server. The servlet used is a spring-enabled
Jersey servlet that uses the configuration classes given in the contextClass parameter. (It is also possible to use configuration files, but not a combination of configuration classes and files.)

The servlet also contains a definition of spring.profiles.active. The intent is to conditionally include the definitions inside of RestApplicationContextTest via the spring 3.1 @Profile annotation but I haven’t been able to get it to work. I’ve left it in for future reference.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
  3.    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  5.  
  6.     <display-name>Project Student Webservice</display-name>
  7.    
  8.     <context-param>
  9.         <param-name>contextClass</param-name>
  10.         <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  11.     </context-param>
  12.     <context-param>
  13.         <param-name>contextConfigLocation</param-name>
  14.         <param-value>
  15.             com.invariantproperties.sandbox.student.webservice.server.config.RestApplicationContext
  16.             com.invariantproperties.sandbox.student.webservice.server.config.RestApplicationContextTest
  17.         </param-value>
  18.     </context-param>
  19.  
  20.     <listener>
  21.         <listener-class>
  22.             org.springframework.web.context.ContextLoaderListener
  23.         </listener-class>
  24.     </listener>
  25.  
  26.     <servlet>
  27.         <servlet-name>REST dispatcher</servlet-name>
  28.         <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
  29.         <init-param>
  30.             <param-name>spring.profiles.active</param-name>
  31.             <param-value>test</param-value>
  32.         </init-param>
  33.     </servlet>
  34.  
  35.     <servlet-mapping>
  36.         <servlet-name>REST dispatcher</servlet-name>
  37.         <url-pattern>/rest/*</url-pattern>
  38.     </servlet-mapping>
  39. </web-app>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <display-name>Project Student Webservice</display-name>
    
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            com.invariantproperties.sandbox.student.webservice.server.config.RestApplicationContext
            com.invariantproperties.sandbox.student.webservice.server.config.RestApplicationContextTest
        </param-value>
    </context-param>

    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <servlet>
        <servlet-name>REST dispatcher</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>test</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>REST dispatcher</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

Unit Testing

The unit tests are straightforward.

  1. public class CourseResourceTest {
  2.     private Course physics = new Course();
  3.     private Course mechanics = new Course();
  4.  
  5.     @Before
  6.     public void init() {
  7.         physics.setId(1);
  8.         physics.setName("physics");
  9.         physics.setUuid(UUID.randomUUID().toString());
  10.  
  11.         mechanics.setId(1);
  12.         mechanics.setName("mechanics");
  13.         mechanics.setUuid(UUID.randomUUID().toString());
  14.     }
  15.  
  16.     @Test
  17.     public void testFindAllCourses() {
  18.         final List<Course> expected = Arrays.asList(physics);
  19.  
  20.         final CourseService service = Mockito.mock(CourseService.class);
  21.         when(service.findAllCourses()).thenReturn(expected);
  22.  
  23.         final CourseResource resource = new CourseResource(service);
  24.         final Response response = resource.findAllCourses();
  25.  
  26.         assertEquals(200, response.getStatus());
  27.         final Course[] actual = (Course[]) response.getEntity();
  28.         assertEquals(expected.size(), actual.length);
  29.         assertNull(actual[0].getId());
  30.         assertEquals(expected.get(0).getName(), actual[0].getName());
  31.         assertEquals(expected.get(0).getUuid(), actual[0].getUuid());
  32.     }
  33.  
  34.     @Test
  35.     public void testFindAllCoursesEmpty() {
  36.         final List<Course> expected = new ArrayList<>();
  37.  
  38.         final CourseService service = Mockito.mock(CourseService.class);
  39.         when(service.findAllCourses()).thenReturn(expected);
  40.  
  41.         final CourseResource resource = new CourseResource(service);
  42.         final Response response = resource.findAllCourses();
  43.  
  44.         assertEquals(200, response.getStatus());
  45.         final Course[] actual = (Course[]) response.getEntity();
  46.         assertEquals(0, actual.length);
  47.     }
  48.  
  49.     @Test
  50.     public void testFindAllCoursesFailure() {
  51.         final CourseService service = Mockito.mock(CourseService.class);
  52.         when(service.findAllCourses()).thenThrow(
  53.                 new UnitTestException();
  54.  
  55.         final CourseResource resource = new CourseResource(service);
  56.         final Response response = resource.findAllCourses();
  57.  
  58.         assertEquals(500, response.getStatus());
  59.     }
  60.  
  61.     @Test
  62.     public void testGetCourse() {
  63.         final Course expected = physics;
  64.  
  65.         final CourseService service = Mockito.mock(CourseService.class);
  66.         when(service.findCourseByUuid(expected.getUuid())).thenReturn(expected);
  67.  
  68.         final CourseResource resource = new CourseResource(service);
  69.         final Response response = resource.getCourse(expected.getUuid());
  70.  
  71.         assertEquals(200, response.getStatus());
  72.         final Course actual = (Course) response.getEntity();
  73.         assertNull(actual.getId());
  74.         assertEquals(expected.getName(), actual.getName());
  75.         assertEquals(expected.getUuid(), actual.getUuid());
  76.     }
  77.  
  78.     @Test
  79.     public void testGetCourseMissing() {
  80.         final CourseService service = Mockito.mock(CourseService.class);
  81.         when(service.findCourseByUuid(physics.getUuid())).thenThrow(
  82.                 new ObjectNotFoundException(physics.getUuid()));
  83.  
  84.         final CourseResource resource = new CourseResource(service);
  85.         final Response response = resource.getCourse(physics.getUuid());
  86.  
  87.         assertEquals(404, response.getStatus());
  88.     }
  89.  
  90.     @Test
  91.     public void testGetCourseFailure() {
  92.         final CourseService service = Mockito.mock(CourseService.class);
  93.         when(service.findCourseByUuid(physics.getUuid())).thenThrow(
  94.                 new UnitTestException();
  95.  
  96.         final CourseResource resource = new CourseResource(service);
  97.         final Response response = resource.getCourse(physics.getUuid());
  98.  
  99.         assertEquals(500, response.getStatus());
  100.     }
  101.  
  102.     @Test
  103.     public void testCreateCourse() {
  104.         final Course expected = physics;
  105.         final Name name = new Name();
  106.         name.setName(expected.getName());
  107.  
  108.         final CourseService service = Mockito.mock(CourseService.class);
  109.         when(service.createCourse(name.getName())).thenReturn(expected);
  110.  
  111.         final CourseResource resource = new CourseResource(service);
  112.         final Response response = resource.createCourse(name);
  113.  
  114.         assertEquals(201, response.getStatus());
  115.         final Course actual = (Course) response.getEntity();
  116.         assertNull(actual.getId());
  117.         assertEquals(expected.getName(), actual.getName());
  118.     }
  119.  
  120.     @Test
  121.     public void testCreateCourseBlankName() {
  122.         final Course expected = physics;
  123.         final Name name = new Name();
  124.  
  125.         final CourseService service = Mockito.mock(CourseService.class);
  126.  
  127.         final CourseResource resource = new CourseResource(service);
  128.         final Response response = resource.createCourse(name);
  129.  
  130.         assertEquals(400, response.getStatus());
  131.     }
  132.  
  133.     /**
  134.      * Test handling when the course can't be created for some reason. For now
  135.      * the service layer just returns a null value - it should throw an
  136.      * appropriate exception.
  137.      */
  138.     @Test
  139.     public void testCreateCourseProblem() {
  140.         final Course expected = physics;
  141.         final Name name = new Name();
  142.         name.setName(expected.getName());
  143.  
  144.         final CourseService service = Mockito.mock(CourseService.class);
  145.         when(service.createCourse(name.getName())).thenReturn(null);
  146.  
  147.         final CourseResource resource = new CourseResource(service);
  148.         final Response response = resource.createCourse(name);
  149.  
  150.         assertEquals(500, response.getStatus());
  151.     }
  152.  
  153.     @Test
  154.     public void testCreateCourseFailure() {
  155.         final Course expected = physics;
  156.         final Name name = new Name();
  157.         name.setName(expected.getName());
  158.  
  159.         final CourseService service = Mockito.mock(CourseService.class);
  160.         when(service.createCourse(name.getName())).thenThrow(
  161.                 new UnitTestException();
  162.  
  163.         final CourseResource resource = new CourseResource(service);
  164.         final Response response = resource.createCourse(name);
  165.  
  166.         assertEquals(500, response.getStatus());
  167.     }
  168.  
  169.     @Test
  170.     public void testUpdateCourse() {
  171.         final Course expected = physics;
  172.         final Name name = new Name();
  173.         name.setName(mechanics.getName());
  174.         final Course updated = new Course();
  175.         updated.setId(expected.getId());
  176.         updated.setName(mechanics.getName());
  177.         updated.setUuid(expected.getUuid());
  178.  
  179.         final CourseService service = Mockito.mock(CourseService.class);
  180.         when(service.findCourseByUuid(expected.getUuid())).thenReturn(expected);
  181.         when(service.updateCourse(expected, name.getName()))
  182.                 .thenReturn(updated);
  183.  
  184.         final CourseResource resource = new CourseResource(service);
  185.         final Response response = resource.updateCourse(expected.getUuid(),
  186.                 name);
  187.  
  188.         assertEquals(200, response.getStatus());
  189.         final Course actual = (Course) response.getEntity();
  190.         assertNull(actual.getId());
  191.         assertEquals(mechanics.getName(), actual.getName());
  192.         assertEquals(expected.getUuid(), actual.getUuid());
  193.     }
  194.  
  195.     /**
  196.      * Test handling when the course can't be updated for some reason. For now
  197.      * the service layer just returns a null value - it should throw an
  198.      * appropriate exception.
  199.      */
  200.     @Test
  201.     public void testUpdateCourseProblem() {
  202.         final Course expected = physics;
  203.         final Name name = new Name();
  204.         name.setName(expected.getName());
  205.  
  206.         final CourseService service = Mockito.mock(CourseService.class);
  207.         when(service.updateCourse(expected, name.getName())).thenReturn(null);
  208.  
  209.         final CourseResource resource = new CourseResource(service);
  210.         final Response response = resource.createCourse(name);
  211.  
  212.         assertEquals(500, response.getStatus());
  213.     }
  214.  
  215.     @Test
  216.     public void testUpdateCourseFailure() {
  217.         final Course expected = physics;
  218.         final Name name = new Name();
  219.         name.setName(expected.getName());
  220.  
  221.         final CourseService service = Mockito.mock(CourseService.class);
  222.         when(service.updateCourse(expected, name.getName())).thenThrow(
  223.                 new UnitTestException();
  224.  
  225.         final CourseResource resource = new CourseResource(service);
  226.         final Response response = resource.createCourse(name);
  227.  
  228.         assertEquals(500, response.getStatus());
  229.     }
  230.  
  231.     @Test
  232.     public void testDeleteCourse() {
  233.         final Course expected = physics;
  234.  
  235.         final CourseService service = Mockito.mock(CourseService.class);
  236.         doNothing().when(service).deleteCourse(expected.getUuid());
  237.  
  238.         final CourseResource resource = new CourseResource(service);
  239.         final Response response = resource.deleteCourse(expected.getUuid());
  240.  
  241.         assertEquals(204, response.getStatus());
  242.     }
  243.  
  244.     @Test
  245.     public void testDeleteCourseMissing() {
  246.         final Course expected = physics;
  247.         final Name name = new Name();
  248.         name.setName(expected.getName());
  249.  
  250.         final CourseService service = Mockito.mock(CourseService.class);
  251.         doThrow(new ObjectNotFoundException(expected.getUuid())).when(service)
  252.                 .deleteCourse(expected.getUuid());
  253.  
  254.         final CourseResource resource = new CourseResource(service);
  255.         final Response response = resource.deleteCourse(expected.getUuid());
  256.  
  257.         assertEquals(204, response.getStatus());
  258.     }
  259.  
  260.     @Test
  261.     public void testDeleteCourseFailure() {
  262.         final Course expected = physics;
  263.  
  264.         final CourseService service = Mockito.mock(CourseService.class);
  265.         doThrow(new UnitTestException()).when(service)
  266.                 .deleteCourse(expected.getUuid());
  267.  
  268.         final CourseResource resource = new CourseResource(service);
  269.         final Response response = resource.deleteCourse(expected.getUuid());
  270.  
  271.         assertEquals(500, response.getStatus());
  272.     }
  273. }
public class CourseResourceTest {
    private Course physics = new Course();
    private Course mechanics = new Course();

    @Before
    public void init() {
        physics.setId(1);
        physics.setName("physics");
        physics.setUuid(UUID.randomUUID().toString());

        mechanics.setId(1);
        mechanics.setName("mechanics");
        mechanics.setUuid(UUID.randomUUID().toString());
    }

    @Test
    public void testFindAllCourses() {
        final List<Course> expected = Arrays.asList(physics);

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findAllCourses()).thenReturn(expected);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.findAllCourses();

        assertEquals(200, response.getStatus());
        final Course[] actual = (Course[]) response.getEntity();
        assertEquals(expected.size(), actual.length);
        assertNull(actual[0].getId());
        assertEquals(expected.get(0).getName(), actual[0].getName());
        assertEquals(expected.get(0).getUuid(), actual[0].getUuid());
    }

    @Test
    public void testFindAllCoursesEmpty() {
        final List<Course> expected = new ArrayList<>();

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findAllCourses()).thenReturn(expected);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.findAllCourses();

        assertEquals(200, response.getStatus());
        final Course[] actual = (Course[]) response.getEntity();
        assertEquals(0, actual.length);
    }

    @Test
    public void testFindAllCoursesFailure() {
        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findAllCourses()).thenThrow(
                new UnitTestException();

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.findAllCourses();

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testGetCourse() {
        final Course expected = physics;

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findCourseByUuid(expected.getUuid())).thenReturn(expected);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.getCourse(expected.getUuid());

        assertEquals(200, response.getStatus());
        final Course actual = (Course) response.getEntity();
        assertNull(actual.getId());
        assertEquals(expected.getName(), actual.getName());
        assertEquals(expected.getUuid(), actual.getUuid());
    }

    @Test
    public void testGetCourseMissing() {
        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findCourseByUuid(physics.getUuid())).thenThrow(
                new ObjectNotFoundException(physics.getUuid()));

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.getCourse(physics.getUuid());

        assertEquals(404, response.getStatus());
    }

    @Test
    public void testGetCourseFailure() {
        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findCourseByUuid(physics.getUuid())).thenThrow(
                new UnitTestException();

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.getCourse(physics.getUuid());

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testCreateCourse() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.createCourse(name.getName())).thenReturn(expected);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(201, response.getStatus());
        final Course actual = (Course) response.getEntity();
        assertNull(actual.getId());
        assertEquals(expected.getName(), actual.getName());
    }

    @Test
    public void testCreateCourseBlankName() {
        final Course expected = physics;
        final Name name = new Name();

        final CourseService service = Mockito.mock(CourseService.class);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(400, response.getStatus());
    }

    /**
     * Test handling when the course can't be created for some reason. For now
     * the service layer just returns a null value - it should throw an
     * appropriate exception.
     */
    @Test
    public void testCreateCourseProblem() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.createCourse(name.getName())).thenReturn(null);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testCreateCourseFailure() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.createCourse(name.getName())).thenThrow(
                new UnitTestException();

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testUpdateCourse() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(mechanics.getName());
        final Course updated = new Course();
        updated.setId(expected.getId());
        updated.setName(mechanics.getName());
        updated.setUuid(expected.getUuid());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.findCourseByUuid(expected.getUuid())).thenReturn(expected);
        when(service.updateCourse(expected, name.getName()))
                .thenReturn(updated);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.updateCourse(expected.getUuid(),
                name);

        assertEquals(200, response.getStatus());
        final Course actual = (Course) response.getEntity();
        assertNull(actual.getId());
        assertEquals(mechanics.getName(), actual.getName());
        assertEquals(expected.getUuid(), actual.getUuid());
    }

    /**
     * Test handling when the course can't be updated for some reason. For now
     * the service layer just returns a null value - it should throw an
     * appropriate exception.
     */
    @Test
    public void testUpdateCourseProblem() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.updateCourse(expected, name.getName())).thenReturn(null);

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testUpdateCourseFailure() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        when(service.updateCourse(expected, name.getName())).thenThrow(
                new UnitTestException();

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.createCourse(name);

        assertEquals(500, response.getStatus());
    }

    @Test
    public void testDeleteCourse() {
        final Course expected = physics;

        final CourseService service = Mockito.mock(CourseService.class);
        doNothing().when(service).deleteCourse(expected.getUuid());

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.deleteCourse(expected.getUuid());

        assertEquals(204, response.getStatus());
    }

    @Test
    public void testDeleteCourseMissing() {
        final Course expected = physics;
        final Name name = new Name();
        name.setName(expected.getName());

        final CourseService service = Mockito.mock(CourseService.class);
        doThrow(new ObjectNotFoundException(expected.getUuid())).when(service)
                .deleteCourse(expected.getUuid());

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.deleteCourse(expected.getUuid());

        assertEquals(204, response.getStatus());
    }

    @Test
    public void testDeleteCourseFailure() {
        final Course expected = physics;

        final CourseService service = Mockito.mock(CourseService.class);
        doThrow(new UnitTestException()).when(service)
                .deleteCourse(expected.getUuid());

        final CourseResource resource = new CourseResource(service);
        final Response response = resource.deleteCourse(expected.getUuid());

        assertEquals(500, response.getStatus());
    }
}

Integration Testing

Question: should REST server integration tests use a live database?

Answer: it’s a trick question. We need both.

The overall architecture has three maven modules. We covered student-ws-client earlier and we’re covering student-ws-server today. Each creates a .jar file. There’s a third module – student-ws-webapp – that creates the actual .war file. The integration tests for the student-ws-server module should use a dummied service layer while the integration tests for the student-ws-webapp module uses the full stack.

We start with the integration tests that mirror the unit tests in the client module.

  1. public class CourseRestServerIntegrationTest {
  2.  
  3.     CourseRestClient client = new CourseRestClientImpl(
  4.             "http://localhost:8080/rest/course/");
  5.  
  6.     @Test
  7.     public void testGetAll() throws IOException {
  8.         Course[] courses = client.getAllCourses();
  9.         assertNotNull(courses);
  10.     }
  11.  
  12.     @Test(expected = ObjectNotFoundException.class)
  13.     public void testUnknownCourse() throws IOException {
  14.         client.getCourse("missing");
  15.     }
  16.  
  17.     @Test
  18.     public void testLifecycle() throws IOException {
  19.         final String physicsName = "Physics 201";
  20.         final Course expected = client.createCourse(physicsName);
  21.         assertEquals(physicsName, expected.getName());
  22.  
  23.         final Course actual1 = client.getCourse(expected.getUuid());
  24.         assertEquals(physicsName, actual1.getName());
  25.  
  26.         final Course[] courses = client.getAllCourses();
  27.         assertTrue(courses.length > 0);
  28.  
  29.         final String mechanicsName = "Newtonian Mechanics 201";
  30.         final Course actual2 = client.updateCourse(actual1.getUuid(),
  31.                 mechanicsName);
  32.         assertEquals(mechanicsName, actual2.getName());
  33.  
  34.         client.deleteCourse(actual1.getUuid());
  35.         try {
  36.             client.getCourse(expected.getUuid());
  37.             fail("should have thrown exception");
  38.         } catch (ObjectNotFoundException e) {
  39.             // do nothing
  40.         }
  41.     }
  42. }
public class CourseRestServerIntegrationTest {

    CourseRestClient client = new CourseRestClientImpl(
            "http://localhost:8080/rest/course/");

    @Test
    public void testGetAll() throws IOException {
        Course[] courses = client.getAllCourses();
        assertNotNull(courses);
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testUnknownCourse() throws IOException {
        client.getCourse("missing");
    }

    @Test
    public void testLifecycle() throws IOException {
        final String physicsName = "Physics 201";
        final Course expected = client.createCourse(physicsName);
        assertEquals(physicsName, expected.getName());

        final Course actual1 = client.getCourse(expected.getUuid());
        assertEquals(physicsName, actual1.getName());

        final Course[] courses = client.getAllCourses();
        assertTrue(courses.length > 0);

        final String mechanicsName = "Newtonian Mechanics 201";
        final Course actual2 = client.updateCourse(actual1.getUuid(),
                mechanicsName);
        assertEquals(mechanicsName, actual2.getName());

        client.deleteCourse(actual1.getUuid());
        try {
            client.getCourse(expected.getUuid());
            fail("should have thrown exception");
        } catch (ObjectNotFoundException e) {
            // do nothing
        }
    }
}

We also need a dummy service class that implements just enough functionality to support our integration tests.

  1. public class DummyCourseService implements CourseService {
  2.     private Map cache = Collections.synchronizedMap(new HashMap<String, Course>());
  3.  
  4.     public List<Course> findAllCourses() {
  5.         return new ArrayList(cache.values());
  6.     }
  7.  
  8.     public Course findCourseById(Integer id) {
  9.         throw new ObjectNotFoundException(null);       
  10.     }
  11.  
  12.     public Course findCourseByUuid(String uuid) {
  13.         if (!cache.containsKey(uuid)) {
  14.             throw new ObjectNotFoundException(uuid);       
  15.         }
  16.         return cache.get(uuid);
  17.     }
  18.  
  19.     public Course createCourse(String name) {
  20.         Course course = new Course();
  21.         course.setUuid(UUID.randomUUID().toString());
  22.         course.setName(name);
  23.         cache.put(course.getUuid(), course);
  24.         return course;
  25.     }
  26.  
  27.     public Course updateCourse(Course oldCourse, String name) {
  28.         if (!cache.containsKey(oldCourse.getUuid())) {
  29.             throw new ObjectNotFoundException(oldCourse.getUuid());                
  30.         }
  31.        
  32.         Course course = cache.get(oldCourse.getUuid());
  33.         course.setUuid(UUID.randomUUID().toString());
  34.         course.setName(name);
  35.         return course;     
  36.     }
  37.  
  38.     public void deleteCourse(String uuid) {
  39.         if (cache.containsKey(uuid)) {
  40.             cache.remove(uuid);
  41.         }
  42.     }
  43. }
public class DummyCourseService implements CourseService {
    private Map cache = Collections.synchronizedMap(new HashMap<String, Course>());

    public List<Course> findAllCourses() {
        return new ArrayList(cache.values());
    }

    public Course findCourseById(Integer id) {
        throw new ObjectNotFoundException(null);    	
    }

    public Course findCourseByUuid(String uuid) {
        if (!cache.containsKey(uuid)) {
            throw new ObjectNotFoundException(uuid);    	
        }
        return cache.get(uuid);
    }

    public Course createCourse(String name) {
        Course course = new Course();
        course.setUuid(UUID.randomUUID().toString());
        course.setName(name);
        cache.put(course.getUuid(), course);
        return course;
    }

    public Course updateCourse(Course oldCourse, String name) {
        if (!cache.containsKey(oldCourse.getUuid())) {
            throw new ObjectNotFoundException(oldCourse.getUuid());    	    		
        }
    	
        Course course = cache.get(oldCourse.getUuid());
        course.setUuid(UUID.randomUUID().toString());
        course.setName(name);
        return course;    	
    }

    public void deleteCourse(String uuid) {
        if (cache.containsKey(uuid)) {
            cache.remove(uuid);
        }
    }
}

pom.xml

The pom.xml file should include a plugin to run an embedded jetty or tomcat server. Advanced users can spin up and tear down the embedded server as part of the integration test – see the update.

  1. <build>
  2.     <plugins>
  3.  
  4.         <!-- Run the application using "mvn jetty:run" -->
  5.         <plugin>
  6.             <groupId>org.mortbay.jetty</groupId>
  7.             <artifactId>maven-jetty-plugin</artifactId>
  8.             <version>6.1.16</version> <!-- ancient! -->
  9.             <configuration>
  10.                 <!-- Log to the console. -->
  11.                 <requestLog implementation="org.mortbay.jetty.NCSARequestLog">
  12.                     <!-- This doesn't do anything for Jetty, but is a workaround for a
  13.                        Maven bug that prevents the requestLog from being set. -->
  14.                     <append>true</append>
  15.                 </requestLog>
  16.                 <webAppConfig>
  17.                     <contextPath>/</contextPath>
  18.                     <extraClasspath>${basedir}/target/test-classes/</extraClasspath>
  19.                 </webAppConfig>
  20.             </configuration>
  21.         </plugin>
  22.     </plugins>
  23. </build>
<build>
    <plugins>

        <!-- Run the application using "mvn jetty:run" -->
        <plugin>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>maven-jetty-plugin</artifactId>
            <version>6.1.16</version> <!-- ancient! -->
            <configuration>
                <!-- Log to the console. -->
                <requestLog implementation="org.mortbay.jetty.NCSARequestLog">
                    <!-- This doesn't do anything for Jetty, but is a workaround for a 
                        Maven bug that prevents the requestLog from being set. -->
                    <append>true</append>
                </requestLog>
                <webAppConfig>
                    <contextPath>/</contextPath>
                    <extraClasspath>${basedir}/target/test-classes/</extraClasspath>
                </webAppConfig>
            </configuration>
        </plugin>
    </plugins>
</build>

Update

After a bit more research I have the configuration to setup and teardown a jetty server during integration tests. This configuration uses non-standard ports so we can run it without having to shut down another jetty or tomcat instance running at the same time.

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <build>
  4.         <plugins>
  5.  
  6.             <!-- Run the application using "mvn jetty:run" -->
  7.             <plugin>
  8.                 <groupId>org.eclipse.jetty</groupId>
  9.                 <artifactId>jetty-maven-plugin</artifactId>
  10.                 <version>9.1.0.v20131115</version>
  11.                 <configuration>
  12.                     <webApp>
  13.                         <extraClasspath>${basedir}/target/test-classes/</extraClasspath>
  14.                     </webApp>
  15.                     <scanIntervalSeconds>10</scanIntervalSeconds>
  16.                     <stopPort>18005</stopPort>
  17.                     <stopKey>STOP</stopKey>
  18.                     <systemProperties>
  19.                        <systemProperty>
  20.                            <name>jetty.port</name>
  21.                            <value>18080</value>
  22.                        </systemProperty>
  23.                     </systemProperties>
  24.                 </configuration>
  25.                 <executions>
  26.                     <execution>
  27.                         <id>start-jetty</id>
  28.                         <phase>pre-integration-test</phase>
  29.                         <goals>
  30.                             <goal>run</goal>
  31.                         </goals>
  32.                         <configuration>
  33.                             <scanIntervalSeconds>0</scanIntervalSeconds>
  34.                             <daemon>true</daemon>
  35.                         </configuration>
  36.                     </execution>
  37.                     <execution>
  38.                         <id>stop-jetty</id>
  39.                         <phase>post-integration-test</phase>
  40.                         <goals>
  41.                             <goal>stop</goal>
  42.                         </goals>
  43.                     </execution>
  44.                 </executions>
  45.             </plugin>
  46.         </plugins>
  47.     </build>
  48. </project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <build>
        <plugins>

            <!-- Run the application using "mvn jetty:run" -->
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.1.0.v20131115</version>
                <configuration>
                    <webApp>
                        <extraClasspath>${basedir}/target/test-classes/</extraClasspath>
                    </webApp>
                    <scanIntervalSeconds>10</scanIntervalSeconds>
                    <stopPort>18005</stopPort>
                    <stopKey>STOP</stopKey>
                    <systemProperties>
                       <systemProperty>
                           <name>jetty.port</name>
                           <value>18080</value>
                       </systemProperty>
                    </systemProperties>
                </configuration>
                <executions>
                    <execution>
                        <id>start-jetty</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <scanIntervalSeconds>0</scanIntervalSeconds>
                            <daemon>true</daemon>
                        </configuration>
                    </execution>
                    <execution>
                        <id>stop-jetty</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Source Code

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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

Project Student: Webservice Client with Jersey

Bear Giles | December 16, 2013

This is part of Project Student. Other posts are Webservice Client with Jersey, Business Layer and Persistence with Spring Data.

The first layer of the RESTful webapp onion is the webservice client. It can be used to mimic web pages containing AJAX content or by programmatic users of the webapp. N.B., the latter might include other webapps, e.g., if you have an internal RESTful server surrounded by a number of presentation servers that create conventional web pages.

Design Decisions

Jersey – I use the Jersey library for the REST calls. I considered several choices but decided to go with Jersey since it’s lightweight and won’t impose many restrictions on the developer. In contrast using a Spring library, for instance, could cause problems in an EJB3 environment in addition to pulling in additional libraries.

UUIDs – the database will use integer primary keys but the web service will use UUIDs to identify values. This is for security – if an attacker knows that account ID 1008 exists then it’s a fairly safe bet that account ID 1007 exists. More importantly user ID 0 probably exists and has additional privileges than the typical user.

This isn’t true with UUIDs – for the most part knowing one UUID gives no insight into other UUIDs. That’s not 100% accurate – some UUIDs are composed with IP addresses or timestamps so the universe of possible values can be dramatically reduced by a knowledgeable attacker – but a random UUID is “good enough” for now.

Limitations

I am taking a “implement the least functionality required” approach so the initial implementation has a number of limitations.

Authentication – there is no attempt made to provide authentication information.

Encryption – there is no attempt made to encrypt the webservice call.

Only CRUD methods – only basic CRUD methods are supported.

Remember – limitations are fine but they must be clearly documented. In the best of worlds they’ll be added to an agile backlog.

Client API

The client API is basic CRUD. We can add functionality later.

  1. public interface CourseRestClient {
  2.  
  3.     /**
  4.      * Get list of all courses.
  5.      */
  6.     Course[] getAllCourses();
  7.  
  8.     /**
  9.      * Get details for specific course.
  10.      * @param uuid
  11.      */
  12.     Course getCourse(String uuid);
  13.  
  14.     /**
  15.      * Create specific course.
  16.      * @param name
  17.      */
  18.     Course createCourse(String name);
  19.  
  20.     /**
  21.      * Update specific course.
  22.      * @param uuid
  23.      * @param name
  24.      */
  25.     Course updateCourse(String uuid, String name);
  26.  
  27.     /**
  28.      * Delete course.
  29.      * @param uuid
  30.      */
  31.     void deleteCourse(String uuid);
  32. }
public interface CourseRestClient {

    /**
     * Get list of all courses.
     */
    Course[] getAllCourses();

    /**
     * Get details for specific course.
     * @param uuid
     */
    Course getCourse(String uuid);

    /**
     * Create specific course.
     * @param name
     */
    Course createCourse(String name);

    /**
     * Update specific course.
     * @param uuid
     * @param name
     */
    Course updateCourse(String uuid, String name);

    /**
     * Delete course.
     * @param uuid
     */
    void deleteCourse(String uuid);
}

Exceptions

The API includes three runtime exceptions. The first, RestClientException, is an abstract runtime exception that is the base class for all other exceptions.

An ObjectNotFoundException is thrown when an expected value is missing. (Implementation note: this is triggered by a 404 status code.) This exception contains enough information to uniquely identify the expected object.

  1. public class ObjectNotFoundException extends RestClientException {
  2.     private static final long serialVersionUID = 1L;
  3.  
  4.     private final String resource;
  5.     private final Class<? extends PersistentObject> objectClass;
  6.     private final String uuid;
  7.  
  8.     public ObjectNotFoundException(final String resource,
  9.             final Class<? extends PersistentObject> objectClass,
  10.             final String uuid) {
  11.         super("object not found: " + resource + "[" + uuid + "]");
  12.         this.resource = resource;
  13.         this.objectClass = objectClass;
  14.         this.uuid = uuid;
  15.     }
  16.  
  17.     public String getResource() {
  18.         return resource;
  19.     }
  20.  
  21.     public Class<? extends PersistentObject> getObjectClass() {
  22.         return objectClass;
  23.     }
  24.  
  25.     public String getUuid() {
  26.         return uuid;
  27.     }
  28. }
public class ObjectNotFoundException extends RestClientException {
    private static final long serialVersionUID = 1L;

    private final String resource;
    private final Class<? extends PersistentObject> objectClass;
    private final String uuid;

    public ObjectNotFoundException(final String resource,
            final Class<? extends PersistentObject> objectClass,
            final String uuid) {
        super("object not found: " + resource + "[" + uuid + "]");
        this.resource = resource;
        this.objectClass = objectClass;
        this.uuid = uuid;
    }

    public String getResource() {
        return resource;
    }

    public Class<? extends PersistentObject> getObjectClass() {
        return objectClass;
    }

    public String getUuid() {
        return uuid;
    }
}

A RestClientFailureException is a generic handler for unexpected or unhandled status codes.

  1. public class RestClientFailureException extends RestClientException {
  2.     private static final long serialVersionUID = 1L;
  3.  
  4.     private final String resource;
  5.     private final Class<? extends PersistentObject> objectClass;
  6.     private final String uuid;
  7.     private final int statusCode;
  8.  
  9.     /**
  10.      * Constructor
  11.      *
  12.      * @param resource
  13.      * @param objectClass
  14.      * @param uuid
  15.      * @param response
  16.      */
  17.     public RestClientFailureException(final String resource,
  18.             final Class<? extends PersistentObject> objectClass,
  19.             final String uuid, final ClientResponse response) {
  20.         super("rest client received error: " + resource + "[" + uuid + "]");
  21.         this.resource = resource;
  22.         this.objectClass = objectClass;
  23.         this.uuid = uuid;
  24.         this.statusCode = response.getStatus();
  25.     }
  26.  
  27.     public String getResource() {
  28.         return resource;
  29.     }
  30.  
  31.     public Class<? extends PersistentObject> getObjectClass() {
  32.         return objectClass;
  33.     }
  34.  
  35.     /**
  36.      * Get UUID, "<none>" (during listAllX()) or "(name)" (during createX())
  37.      *
  38.      * @return
  39.      */
  40.     public String getUuid() {
  41.         return uuid;
  42.     }
  43.  
  44.     /**
  45.      * Get standard HTTP status code.
  46.      *
  47.      * @return
  48.      */
  49.     public int getStatusCode() {
  50.         return statusCode;
  51.     }
  52. }
public class RestClientFailureException extends RestClientException {
    private static final long serialVersionUID = 1L;

    private final String resource;
    private final Class<? extends PersistentObject> objectClass;
    private final String uuid;
    private final int statusCode;

    /**
     * Constructor
     * 
     * @param resource
     * @param objectClass
     * @param uuid
     * @param response
     */
    public RestClientFailureException(final String resource,
            final Class<? extends PersistentObject> objectClass,
            final String uuid, final ClientResponse response) {
        super("rest client received error: " + resource + "[" + uuid + "]");
        this.resource = resource;
        this.objectClass = objectClass;
        this.uuid = uuid;
        this.statusCode = response.getStatus();
    }

    public String getResource() {
        return resource;
    }

    public Class<? extends PersistentObject> getObjectClass() {
        return objectClass;
    }

    /**
     * Get UUID, "<none>" (during listAllX()) or "(name)" (during createX())
     * 
     * @return
     */
    public String getUuid() {
        return uuid;
    }

    /**
     * Get standard HTTP status code.
     * 
     * @return
     */
    public int getStatusCode() {
        return statusCode;
    }
}

We’ll want to add an UnauthorizedOperationException after we add client authentication.

Client Implementation

Basic CRUD implementations are typically boilerplate so we can use an abstract class to do most of the heavy lifting. More advanced functionality will probably require this class to make Jersey calls directly.

  1. /**
  2.  * This is the Course-specific implementation.
  3.  */
  4. public class CourseRestClientImpl extends AbstractRestClientImpl<Course>
  5.         implements CourseRestClient {
  6.     private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];
  7.  
  8.     /**
  9.      * Constructor.
  10.      *
  11.      * @param courseResource
  12.      */
  13.     public CourseRestClientImpl(final String resource) {
  14.         super(resource, Course.class, Course[].class);
  15.     }
  16.  
  17.     /**
  18.      * Create JSON string.
  19.      *
  20.      * @param name
  21.      * @return
  22.      */
  23.     String createJson(final String name) {
  24.         return String.format("{ \"name\": \"%s\" }", name);
  25.     }
  26.  
  27.     /**
  28.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getAllCourses()
  29.      */
  30.     public Course[] getAllCourses() {
  31.         return super.getAllObjects(EMPTY_COURSE_ARRAY);
  32.     }
  33.  
  34.     /**
  35.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getCourse(java.lang.String)
  36.      */
  37.     public Course getCourse(final String uuid) {
  38.         return super.getObject(uuid);
  39.     }
  40.  
  41.     /**
  42.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
  43.      */
  44.     public Course createCourse(final String name) {
  45.         if (name == null || name.isEmpty()) {
  46.             throw new IllegalArgumentException("'name' is required");
  47.         }
  48.  
  49.         return createObject(createJson(name));
  50.     }
  51.  
  52.     /**
  53.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#updateCourse(java.lang.String,
  54.      *      java.lang.String)
  55.      */
  56.     public Course updateCourse(final String uuid, final String name) {
  57.         if (name == null || name.isEmpty()) {
  58.             throw new IllegalArgumentException("'name' is required");
  59.         }
  60.  
  61.         return super.updateObject(createJson(name), uuid);
  62.     }
  63.  
  64.     /**
  65.      * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#deleteCourse(java.lang.String)
  66.      */
  67.     public void deleteCourse(final String uuid) {
  68.         super.deleteObject(uuid);
  69.     }
  70. }
/**
 * This is the Course-specific implementation.
 */
public class CourseRestClientImpl extends AbstractRestClientImpl<Course>
        implements CourseRestClient {
    private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];

    /**
     * Constructor.
     * 
     * @param courseResource
     */
    public CourseRestClientImpl(final String resource) {
        super(resource, Course.class, Course[].class);
    }

    /**
     * Create JSON string.
     * 
     * @param name
     * @return
     */
    String createJson(final String name) {
        return String.format("{ \"name\": \"%s\" }", name);
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getAllCourses()
     */
    public Course[] getAllCourses() {
        return super.getAllObjects(EMPTY_COURSE_ARRAY);
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getCourse(java.lang.String)
     */
    public Course getCourse(final String uuid) {
        return super.getObject(uuid);
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
     */
    public Course createCourse(final String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }

        return createObject(createJson(name));
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#updateCourse(java.lang.String,
     *      java.lang.String)
     */
    public Course updateCourse(final String uuid, final String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }

        return super.updateObject(createJson(name), uuid);
    }

    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#deleteCourse(java.lang.String)
     */
    public void deleteCourse(final String uuid) {
        super.deleteObject(uuid);
    }
}

The abstract base class does the heavy lifting.

  1. public class AbstractRestClientImpl<T extends PersistentObject> {
  2.     private final String resource;
  3.     private final Class<T> objectClass;
  4.     private final Class<T[]> objectArrayClass;
  5.  
  6.     /**
  7.      * Constructor.
  8.      *
  9.      * @param resource
  10.      */
  11.     public AbstractRestClientImpl(final String resource,
  12.             final Class<T> objectClass, final Class<T[]> objectArrayClass) {
  13.         this.resource = resource;
  14.         this.objectClass = objectClass;
  15.         this.objectArrayClass = objectArrayClass;
  16.     }
  17.  
  18.     /**
  19.      * Helper method for testing.
  20.      *
  21.      * @return
  22.      */
  23.     Client createClient() {
  24.         return Client.create();
  25.     }
  26.  
  27.     /**
  28.      * List all objects. This is a risky method since there's no attempt at
  29.      * pagination.
  30.      */
  31.     public T[] getAllObjects(final T[] emptyListClass) {
  32.         final Client client = createClient();
  33.  
  34.         try {
  35.             final WebResource webResource = client.resource(resource);
  36.             final ClientResponse response = webResource.accept(
  37.                     MediaType.APPLICATION_JSON).get(ClientResponse.class);
  38.  
  39.             if (response.getStatus() == Response.Status.OK.getStatusCode()) {
  40.                 T[] entities = response.getEntity(objectArrayClass);
  41.                 return entities;
  42.             } else {
  43.                 throw new RestClientFailureException(resource, objectClass,
  44.                         "<none>", response);
  45.             }
  46.         } finally {
  47.             client.destroy();
  48.         }
  49.     }
  50.  
  51.     /**
  52.      * Get a specific object.
  53.      */
  54.     public T getObject(String uuid) {
  55.         final Client client = createClient();
  56.  
  57.         try {
  58.             final WebResource webResource = client.resource(resource + uuid);
  59.             final ClientResponse response = webResource.accept(
  60.                     MediaType.APPLICATION_JSON).get(ClientResponse.class);
  61.  
  62.             if (response.getStatus() == Response.Status.OK.getStatusCode()) {
  63.                 final T entity = response.getEntity(objectClass);
  64.                 return entity;
  65.             } else if (response.getStatus() == Response.Status.NOT_FOUND
  66.                     .getStatusCode()) {
  67.                 throw new ObjectNotFoundException(resource, objectClass, uuid);
  68.             } else {
  69.                 throw new RestClientFailureException(resource, objectClass,
  70.                         uuid, response);
  71.             }
  72.         } finally {
  73.             client.destroy();
  74.         }
  75.     }
  76.  
  77.     /**
  78.      * Create an object with the specified values.
  79.      */
  80.     public T createObject(final String json) {
  81.         final Client client = createClient();
  82.  
  83.         try {
  84.             final WebResource webResource = client.resource(resource);
  85.             final ClientResponse response = webResource
  86.                     .type(MediaType.APPLICATION_JSON)
  87.                     .accept(MediaType.APPLICATION_JSON)
  88.                     .post(ClientResponse.class, json);
  89.  
  90.             if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
  91.                 final T entity = response.getEntity(objectClass);
  92.                 return entity;
  93.             } else {
  94.                 throw new RestClientFailureException(resource, objectClass, "("
  95.                         + json + ")", response);
  96.             }
  97.         } finally {
  98.             client.destroy();
  99.         }
  100.     }
  101.  
  102.     /**
  103.      * Update an object with the specified json.
  104.      */
  105.     public T updateObject(final String json, final String uuid) {
  106.         final Client client = createClient();
  107.  
  108.         try {
  109.             final WebResource webResource = client.resource(resource + uuid);
  110.             final ClientResponse response = webResource
  111.                     .type(MediaType.APPLICATION_JSON)
  112.                     .accept(MediaType.APPLICATION_JSON)
  113.                     .post(ClientResponse.class, json);
  114.  
  115.             if (response.getStatus() == Response.Status.OK.getStatusCode()) {
  116.                 final T entity = response.getEntity(objectClass);
  117.                 return entity;
  118.             } else if (response.getStatus() == Response.Status.NOT_FOUND
  119.                     .getStatusCode()) {
  120.                 throw new ObjectNotFoundException(resource, objectClass, uuid);
  121.             } else {
  122.                 throw new RestClientFailureException(resource, objectClass,
  123.                         uuid, response);
  124.             }
  125.         } finally {
  126.             client.destroy();
  127.         }
  128.     }
  129.  
  130.     /**
  131.      * Delete specified object.
  132.      */
  133.     public void deleteObject(String uuid) {
  134.         final Client client = createClient();
  135.  
  136.         try {
  137.             final WebResource webResource = client.resource(resource + uuid);
  138.             final ClientResponse response = webResource.accept(
  139.                     MediaType.APPLICATION_JSON).delete(ClientResponse.class);
  140.  
  141.             if (response.getStatus() == Response.Status.GONE.getStatusCode()) {
  142.                 // do nothing
  143.             } else if (response.getStatus() == Response.Status.NOT_FOUND
  144.                     .getStatusCode()) {
  145.                 // do nothing - delete is idempotent
  146.             } else {
  147.                 throw new RestClientFailureException(resource, objectClass,
  148.                         uuid, response);
  149.             }
  150.         } finally {
  151.             client.destroy();
  152.         }
  153.     }
  154. }
public class AbstractRestClientImpl<T extends PersistentObject> {
    private final String resource;
    private final Class<T> objectClass;
    private final Class<T[]> objectArrayClass;

    /**
     * Constructor.
     * 
     * @param resource
     */
    public AbstractRestClientImpl(final String resource,
            final Class<T> objectClass, final Class<T[]> objectArrayClass) {
        this.resource = resource;
        this.objectClass = objectClass;
        this.objectArrayClass = objectArrayClass;
    }

    /**
     * Helper method for testing.
     * 
     * @return
     */
    Client createClient() {
        return Client.create();
    }

    /**
     * List all objects. This is a risky method since there's no attempt at
     * pagination.
     */
    public T[] getAllObjects(final T[] emptyListClass) {
        final Client client = createClient();

        try {
            final WebResource webResource = client.resource(resource);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).get(ClientResponse.class);

            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                T[] entities = response.getEntity(objectArrayClass);
                return entities;
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        "<none>", response);
            }
        } finally {
            client.destroy();
        }
    }

    /**
     * Get a specific object.
     */
    public T getObject(String uuid) {
        final Client client = createClient();

        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).get(ClientResponse.class);

            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                throw new ObjectNotFoundException(resource, objectClass, uuid);
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }

    /**
     * Create an object with the specified values.
     */
    public T createObject(final String json) {
        final Client client = createClient();

        try {
            final WebResource webResource = client.resource(resource);
            final ClientResponse response = webResource
                    .type(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, json);

            if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else {
                throw new RestClientFailureException(resource, objectClass, "("
                        + json + ")", response);
            }
        } finally {
            client.destroy();
        }
    }

    /**
     * Update an object with the specified json.
     */
    public T updateObject(final String json, final String uuid) {
        final Client client = createClient();

        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource
                    .type(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, json);

            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                throw new ObjectNotFoundException(resource, objectClass, uuid);
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }

    /**
     * Delete specified object.
     */
    public void deleteObject(String uuid) {
        final Client client = createClient();

        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).delete(ClientResponse.class);

            if (response.getStatus() == Response.Status.GONE.getStatusCode()) {
                // do nothing
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                // do nothing - delete is idempotent
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }
}

Unit Testing

We now come to our test code. Important: we want to test our code’s behavior and not its implementation.

  1. public class CourseRestClientImplTest {
  2.     private static final String UUID = "uuid";
  3.     private static final String NAME = "name";
  4.  
  5.     @Test
  6.     public void testGetAllCoursesEmpty() {
  7.         CourseRestClient client = new CourseRestClientMock(200, new Course[0]);
  8.         Course[] results = client.getAllCourses();
  9.         assertEquals(0, results.length);
  10.     }
  11.  
  12.     @Test
  13.     public void testGetAllCoursesNonEmpty() {
  14.         Course course = new Course();
  15.         course.setUuid(UUID);
  16.         CourseRestClient client = new CourseRestClientMock(200,
  17.                 new Course[] { course });
  18.         Course[] results = client.getAllCourses();
  19.         assertEquals(1, results.length);
  20.     }
  21.  
  22.     @Test(expected = RestClientFailureException.class)
  23.     public void testGetAllCoursesError() {
  24.         CourseRestClient client = new CourseRestClientMock(500, null);
  25.         client.getAllCourses();
  26.     }
  27.  
  28.     @Test
  29.     public void testGetCourse() {
  30.         Course course = new Course();
  31.         course.setUuid(UUID);
  32.         CourseRestClient client = new CourseRestClientMock(200, course);
  33.         Course results = client.getCourse(course.getUuid());
  34.         assertEquals(course.getUuid(), results.getUuid());
  35.     }
  36.  
  37.     @Test(expected = ObjectNotFoundException.class)
  38.     public void testGetCourseMissing() {
  39.         CourseRestClient client = new CourseRestClientMock(404, null);
  40.         client.getCourse(UUID);
  41.     }
  42.  
  43.     @Test(expected = RestClientFailureException.class)
  44.     public void testGetCourseError() {
  45.         CourseRestClient client = new CourseRestClientMock(500, null);
  46.         client.getCourse(UUID);
  47.     }
  48.  
  49.     @Test
  50.     public void testCreateCourse() {
  51.         Course course = new Course();
  52.         course.setName(NAME);
  53.         CourseRestClient client = new CourseRestClientMock(
  54.                 Response.Status.CREATED.getStatusCode(), course);
  55.         Course results = client.createCourse(course.getName());
  56.         assertEquals(course.getName(), results.getName());
  57.     }
  58.  
  59.     @Test(expected = RestClientFailureException.class)
  60.     public void testCreateCourseError() {
  61.         CourseRestClient client = new CourseRestClientMock(500, null);
  62.         client.createCourse(UUID);
  63.     }
  64.  
  65.     @Test
  66.     public void testUpdateCourse() {
  67.         Course course = new Course();
  68.         course.setUuid(UUID);
  69.         course.setName(NAME);
  70.         CourseRestClient client = new CourseRestClientMock(200, course);
  71.         Course results = client
  72.                 .updateCourse(course.getUuid(), course.getName());
  73.         assertEquals(course.getUuid(), results.getUuid());
  74.         assertEquals(course.getName(), results.getName());
  75.     }
  76.  
  77.     @Test(expected = ObjectNotFoundException.class)
  78.     public void testUpdateCourseMissing() {
  79.         CourseRestClient client = new CourseRestClientMock(404, null);
  80.         client.updateCourse(UUID, NAME);
  81.     }
  82.  
  83.     @Test(expected = RestClientFailureException.class)
  84.     public void testUpdateCourseError() {
  85.         CourseRestClient client = new CourseRestClientMock(500, null);
  86.         client.updateCourse(UUID, NAME);
  87.     }
  88.  
  89.     @Test
  90.     public void testDeleteCourse() {
  91.         Course course = new Course();
  92.         course.setUuid(UUID);
  93.         CourseRestClient client = new CourseRestClientMock(
  94.                 Response.Status.GONE.getStatusCode(), null);
  95.         client.deleteCourse(course.getUuid());
  96.     }
  97.  
  98.     @Test
  99.     public void testDeleteCourseMissing() {
  100.         CourseRestClient client = new CourseRestClientMock(404, null);
  101.         client.deleteCourse(UUID);
  102.     }
  103.  
  104.     @Test(expected = RestClientFailureException.class)
  105.     public void testDeleteCourseError() {
  106.         CourseRestClient client = new CourseRestClientMock(500, null);
  107.         client.deleteCourse(UUID);
  108.     }
  109. }
public class CourseRestClientImplTest {
    private static final String UUID = "uuid";
    private static final String NAME = "name";

    @Test
    public void testGetAllCoursesEmpty() {
        CourseRestClient client = new CourseRestClientMock(200, new Course[0]);
        Course[] results = client.getAllCourses();
        assertEquals(0, results.length);
    }

    @Test
    public void testGetAllCoursesNonEmpty() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(200,
                new Course[] { course });
        Course[] results = client.getAllCourses();
        assertEquals(1, results.length);
    }

    @Test(expected = RestClientFailureException.class)
    public void testGetAllCoursesError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.getAllCourses();
    }

    @Test
    public void testGetCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(200, course);
        Course results = client.getCourse(course.getUuid());
        assertEquals(course.getUuid(), results.getUuid());
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testGetCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.getCourse(UUID);
    }

    @Test(expected = RestClientFailureException.class)
    public void testGetCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.getCourse(UUID);
    }

    @Test
    public void testCreateCourse() {
        Course course = new Course();
        course.setName(NAME);
        CourseRestClient client = new CourseRestClientMock(
                Response.Status.CREATED.getStatusCode(), course);
        Course results = client.createCourse(course.getName());
        assertEquals(course.getName(), results.getName());
    }

    @Test(expected = RestClientFailureException.class)
    public void testCreateCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.createCourse(UUID);
    }

    @Test
    public void testUpdateCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        course.setName(NAME);
        CourseRestClient client = new CourseRestClientMock(200, course);
        Course results = client
                .updateCourse(course.getUuid(), course.getName());
        assertEquals(course.getUuid(), results.getUuid());
        assertEquals(course.getName(), results.getName());
    }

    @Test(expected = ObjectNotFoundException.class)
    public void testUpdateCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.updateCourse(UUID, NAME);
    }

    @Test(expected = RestClientFailureException.class)
    public void testUpdateCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.updateCourse(UUID, NAME);
    }

    @Test
    public void testDeleteCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(
                Response.Status.GONE.getStatusCode(), null);
        client.deleteCourse(course.getUuid());
    }

    @Test
    public void testDeleteCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.deleteCourse(UUID);
    }

    @Test(expected = RestClientFailureException.class)
    public void testDeleteCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.deleteCourse(UUID);
    }
}

Finally we need to create a to-be-tested object with a mocked REST client. We can’t use dependency injection since Client.createClient is a static method but we’ve wrapped that call in a package-private method that we can override. That method creates a mocked Client that provides the rest of the values required from Jersey library.

  1. class CourseRestClientMock extends CourseRestClientImpl {
  2.     static final String RESOURCE = "test://rest/course/";
  3.     private Client client;
  4.     private WebResource webResource;
  5.     private WebResource.Builder webResourceBuilder;
  6.     private ClientResponse response;
  7.     private final int status;
  8.     private final Object results;
  9.  
  10.     CourseRestClientMock(int status, Object results) {
  11.         super(RESOURCE);
  12.         this.status = status;
  13.         this.results = results;
  14.     }
  15.  
  16.     /**
  17.      * Override createClient() so it returns mocked object. These expectations
  18.      * will handle basic CRUD operations, more advanced functionality will
  19.      * require inspecting JSON payload of POST call.
  20.      */
  21.     Client createClient() {
  22.         client = Mockito.mock(Client.class);
  23.         webResource = Mockito.mock(WebResource.class);
  24.         webResourceBuilder = Mockito.mock(WebResource.Builder.class);
  25.         response = Mockito.mock(ClientResponse.class);
  26.         when(client.resource(any(String.class))).thenReturn(webResource);
  27.         when(webResource.accept(any(String.class))).thenReturn(
  28.                 webResourceBuilder);
  29.         when(webResource.type(any(String.class)))
  30.                 .thenReturn(webResourceBuilder);
  31.         when(webResourceBuilder.accept(any(String.class))).thenReturn(
  32.                 webResourceBuilder);
  33.         when(webResourceBuilder.type(any(String.class))).thenReturn(
  34.                 webResourceBuilder);
  35.         when(webResourceBuilder.get(eq(ClientResponse.class))).thenReturn(
  36.                 response);
  37.         when(
  38.                 webResourceBuilder.post(eq(ClientResponse.class),
  39.                         any(String.class))).thenReturn(response);
  40.         when(
  41.                 webResourceBuilder.put(eq(ClientResponse.class),
  42.                         any(String.class))).thenReturn(response);
  43.         when(webResourceBuilder.delete(eq(ClientResponse.class))).thenReturn(
  44.                 response);
  45.         when(response.getStatus()).thenReturn(status);
  46.         when(response.getEntity(any(Class.class))).thenReturn(results);
  47.         return client;
  48.     }
  49. }
class CourseRestClientMock extends CourseRestClientImpl {
    static final String RESOURCE = "test://rest/course/";
    private Client client;
    private WebResource webResource;
    private WebResource.Builder webResourceBuilder;
    private ClientResponse response;
    private final int status;
    private final Object results;

    CourseRestClientMock(int status, Object results) {
        super(RESOURCE);
        this.status = status;
        this.results = results;
    }

    /**
     * Override createClient() so it returns mocked object. These expectations
     * will handle basic CRUD operations, more advanced functionality will
     * require inspecting JSON payload of POST call.
     */
    Client createClient() {
        client = Mockito.mock(Client.class);
        webResource = Mockito.mock(WebResource.class);
        webResourceBuilder = Mockito.mock(WebResource.Builder.class);
        response = Mockito.mock(ClientResponse.class);
        when(client.resource(any(String.class))).thenReturn(webResource);
        when(webResource.accept(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResource.type(any(String.class)))
                .thenReturn(webResourceBuilder);
        when(webResourceBuilder.accept(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResourceBuilder.type(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResourceBuilder.get(eq(ClientResponse.class))).thenReturn(
                response);
        when(
                webResourceBuilder.post(eq(ClientResponse.class),
                        any(String.class))).thenReturn(response);
        when(
                webResourceBuilder.put(eq(ClientResponse.class),
                        any(String.class))).thenReturn(response);
        when(webResourceBuilder.delete(eq(ClientResponse.class))).thenReturn(
                response);
        when(response.getStatus()).thenReturn(status);
        when(response.getEntity(any(Class.class))).thenReturn(results);
        return client;
    }
}

Integration Testing

This is the outermost layer of the onion so there’s no meaningful integration tests.

Source Code

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

Comments
No Comments »
Categories
java
Comments rss Comments rss
Trackback Trackback

« Previous Entries

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,314 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox