Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
EclipseLink/DesignDocs/326663
Contents
- 1 Design Specification: JPA RESTful Service
- 1.1 Document History
- 1.2 Project overview
- 1.3 Concepts
- 1.4 Requirements
- 1.5 Design Constraints
- 1.6 Design / Functionality
- 1.7 Testing
- 1.8 Repository
- 1.9 Build
- 1.10 API
- 1.11 GUI
- 1.12 Config files
- 1.13 Documentation
- 1.14 Open Issues
- 1.15 Decisions
- 1.16 Future Considerations
Design Specification: JPA RESTful Service
Document History
Date | Author | Version Description & Notes |
---|---|---|
2010/09/30 | Blaise Doughan | Initial Version |
Project overview
Provide an easy means for users to expose their JPA entities through a RESTful service.
Goals:
- Provide a means for users to implement a standards (JAX-RS/JAXB/JPA) based RESTful service with minimal code.
Proposal:
- The user would implement their JPA based JAX-RS service as follows. They would extend JPASingleKeyResource or JPACompositeKeyResource depending upon their key type.
import javax.ejb.LocalBean; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContextType; import javax.ws.rs.Path; import org.eclipse.persistence.jpa.rest.JPASingleKeyResource; @Stateless @LocalBean @Path("/customers") public class CustomerService extends JPASingleKeyResource<Customer, Long> { @PersistenceContext(unitName="CustomerService", type=PersistenceContextType.TRANSACTION) EntityManager entityManager; public CustomerService() { super(Customer.class); } @Override protected EntityManager entityManager() { return entityManager; } }
Concepts
The following example demonstrates how JAXB and JPA can be used to implement a RESTful (JAX-RS) web service:
Requirements
- Expose JPA EntityManager as RESTful (JAX-RS) service
- Execute CRUD operations
- Execute named queries:
- read one
- read many
- update
- delete
- Accept/Return both XML and JSON mime types
- Convert JPA exceptions into appropriate HTTP response codes
Design Constraints
- JAX-RS operates on the HTTP protocol, we will be limited by the constraints of this protocol:
- URI parameters are limited to simple types
- Error conditions are limited to response codes
- Operations either produce data or consume data, not both
- JAX-RS uses JAXB to produce JSON messages (atleast Jersey does). We are limited to the types of JSON messages that can be produced/consumed by JAXB.
- Collection Property & Type Erasure. Some JAX-RS implementations will not allow use to provide an abstraction for operations that return collections.
Design / Functionality
URI Representation
Data in a RESTful service is referenced through URIs. The common parts of the URI for this example are:
- http://www.example.com/customer-app/rest - the first part of the URI is based on how the application is deployed.
- customers - this part of the path corresponds to the JAX-RS @Path annotation on the RESTful service.
URI for JPA Entities with Unary Key
If the JPA entity has a single part primary key then in the corresponding URI the primary key will be represented as a path parameter. This is a common RESTful operation.
@Entity public class Customer implements Serializable { @Id private long id; }
URI corresponding to Customer entity with id == 1:
URI for JPA Entities with Composite Keys
A different mechanism needs be be employed when locating a resource with composite keys. The URI will leverage the property names from the JPA key class as matrix parameters. The advantage of using matrix paramters is that they may be cached. The same representation is also used if composite keys is represented using an embedded key class.
@Entity @IdClass(CustomerID.clsas) public class Customer implements Serializable { @Id private long id; @Id private String country; }
public class CustomerID { private String country; private long id; public CustomerID() { super(); } public CustomerID(String country, long id) { this.country = country; this.id = id; } public String getCountry() { return country; } public long getId() { return id; } }
URI corresponding to the instance of Customer with id == 1 and country == CA:
Named Queries: Read
A named read query call needs to be mapped to a URI. Below is an example named read query:
@NamedQuery(name = "findCustomerByName", query = "SELECT c " + "FROM Customer c " + "WHERE c.firstName = :firstName AND " + " c.lastName = :lastName")
Get Single Result:
Get Result List:
URI components:
- findCustomersByName - this corresponds to the name of the named query
- singleResult or resultList - this portion indicates whether one or many results are returned
- ;firstName=Jane;lastName=Doe - these are matrix parameters, the name of the parameter must match exactly the parameter name in the named query.
- ?firstResult=1&maxResults=10 - optional query parameters to specify firstResult and maxResults
The parameters will be used to build the equivalent of the following:
Query query = entityManager.createNamedQuery("findCustomersByCity"); query.setParameter("firstName", "Jane"); query.setParameter("lastName", "Doe"); query.setFirstResult(1); query.setMaxResults(10); return query.getResultList();
Named Queries: Update & Delete
A named update and delete query calls needs to be mapped to URI. Below is an example named update query:
@NamedQuery(name = "updateCustomersByCity", query = "UPDATE Customer c " + "SET c.address.city = :newCity " + "WHERE c.address.city = :oldCity")
Execute the Query:
URI components:
- updateCustomersByCity - this corresponds to the name of the named query
- execute - this portion indicates the query will be executed
- ;oldCity=Nepean;newCity=Ottawa- these are matrix parameters, the name of the parameter must match exactly the parameter name in the named query.
The parameters will be used to build the equivalent of the following:
Query query = entityManager.createNamedQuery("updateCustomersByCity"); query.setParameter("oldCity", "Nepean"); query.setParameter("newCity", "Ottawa"); query.executeUpdate();
REST (CRUD) Operations
POST - Create Operation
Using the Jersery client APIs the following is how a post operation is called on our service. The XML message will converted to the appropriate object type using JAXB.
Client c = Client.create(); WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers"); ClientResponse response = resource.type("application/xml").post(ClientResponse.class, "<customer>...</customer>"); System.out.println(response);
This call will be received by
@POST @Consumes({"application/xml", "application/json"}) public Response create(@Context UriInfo uriInfo, EntityType entity) { entityManager().persist(entity); UriBuilder uriBuilder = pkUriBuilder(uriInfo.getAbsolutePathBuilder(), entity); return Response.created(uriBuilder.build()).build(); }
Successful Responses
- 200 OK
- Return the URI (the representation discussed earlier) for the created entity.
Error Responses:
- ?
GET - Read Operation
Get is a read-only operation. It is used to query resources. The following is an example of how to invoke a GET call using the Jersey client APIs:
WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers;id=1;country=CA"); ClientResponse response = resource.accept(mimeType).get(ClientResponse.class);
We will need to differentiate between the single key case that uses path parameters and the composite key case that uses matrix parameters:
Single Key - Path Parameter
The unary key parameter will be passed directly to us. Note the String to KeyType conversion will be done using JAXB conversion rules and not JPA conversion rules. There should be no differences for common key types such as Strings and numeric types.
@GET @Path("{id}") @Produces({"application/xml", "application/json"}) public EntityType read(@PathParam("id") KeyType id) { return entityManager().find(entityClass, id); }
Composite Key - Matrix Parameters
An instance of the primary key class will need to be derived from the matrix parameters. A utility will need to be provided for this.
@GET @Produces({"application/xml", "application/json"}) public EntityType read(@Context UriInfo info) { return entityManager().find(entityClass, getPrimaryKey(info)); }
Successful Responses
- 200 OK - If a result is returned
- 204 No Content - If no results are returned
Error Responses:
- ?
PUT - Update Operation
The put operation updates the underlying resource. When using put the client knows the identity of the resource being updated. The following is an example of how to invoke a PUT call using the Jersey client APIs:
Client c = Client.create(); WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers/1"); ClientResponse response = resource.type("application/xml").put(ClientResponse.class, "<customer>...</customer>"); System.out.println(response);
This call will be received by
@PUT @Consumes({"application/xml", "application/json"}) public void update(EntityType entity) { entityManager().merge(entity); }
Successful Responses
- 200 OK
Error Responses:
- 409 Conflict - Locking related exception
DELETE - Delete Operation
The delete operation is used to remove resources. It is not an error to remove a non-existent resource. Below is an example using the Jersey client APIs:
WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers/1"); ClientResponse response = resource.delete(ClientResponse.class);
We will need to differentiate between the single key case that uses path parameters and the composite key case that uses matrix parameters:
Single Key - Path Parameter
@DELETE @Path("{id}") public void delete(@PathParam("id") KeyType id) { super.delete(id); }
Composite Key - Matrix Parameters
An instance of the primary key class will need to be derived from the matrix parameters. A utility will need to be provided for this.
@DELETE public void delete(@Context UriInfo info) { super.delete(getPrimaryKey(info)); }
Successful Responses
- 200 OK
Error Responses:
Matrix Parameters to Instance of ID Class
The matrix parameters could be converted to an ID class in the following manner (Note the code below is currently using query paramters and needs to be updated):
private KeyType getPrimaryKey(UriInfo info) { try { KeyType pk = (KeyType) PrivilegedAccessHelper.newInstanceFromClass(keyClass); for(Entry<String, List<String>> entry : info.getQueryParameters().entrySet()) { Field pkField = PrivilegedAccessHelper.getField(keyClass, entry.getKey(), true); Object pkValue = ConversionManager.getDefaultManager().convertObject(entry.getValue().get(0), pkField.getType()); PrivilegedAccessHelper.setValueInField(pkField, pk, pkValue); } return pk; } catch(Exception e) { throw new RuntimeException(e); } }
The key class can be obtained using the the JPA metamodel facility:
keyClass = (Class<KeyType>) entityManager().getMetamodel().entity(entityClass).getIdType().getJavaType();
Testing
- Standalone Testing - In JPA or seperate test framework?
- POST (Create)
- Test that create is performed
- Test that URI returned in response
- Test proper error code is returned when an attempt is made to create an existing object.
- Test proper error code is returned when an attempt is made to create an entity without pk information.
- Test proper error code is returned when an attempt is made to create a null entity
- GET (Read)
- Test that correct entity is returned
- Test that the proper success code is returned when an attempt is made to read an existing entity
- Test that the proper success code is returned when an attempt is made to read a non-existant entity with correct pk info
- Test that the proper error code is returned when an attempt is made to read an entity with incorrect pk info
- To few parameters
- To many parameters
- Correct number of parameters but value is impropery String format for non-String key parameter
- PUT (Update)
- Test that update is performed
- Test that proper success code is returned when an attempt is made to update an entity
- Test that the same update can be made multiple times
- Test that proper success code is returned when an attempt is made to update a null entity
- Test that proper error code is returned if the update fails
- Test that an create is performed if a attempt is made to update a non-existant entity
- Test that proper error code is returned if an attempt is made to update an entity with incorrect pk information.
- DELETE (Delete)
- Test that delete is performed
- Test that proper success code is returned when an attempt is made to delete an entity
- Test that proper success code is returned when an attempt is made to delete an non-existant entity
- Test that the same delete operation can be called multiple times
- Test that proper error code is returned if an attempt is made to delete an entity with incorrect pk info (parameter is of wrong type)
- Named Queries
- Test that query is performed
- Read One
- Read Many
- Update
- Delete
- Test that proper response code is returned when a successful query is performed
- Test that proper error code is returned when an unsuccessful query is performed
- Negative Test Cases
- When too few parameters are supplied
- When too many parameters are supplied
- When parameter cannot be converted to appropiate type
- When a list of values for a parameter are passed in
- Test that query is performed
- POST (Create)
- May want to test with the following JAX-RS Implementations:
- Jersey (Reference Implementation)
- Apache Wink
- JBoss RestEasy
Repository
I propose that this new code live in its own bundle alongside the other JPA bundles:
- trunk
- jpa
- org.eclipse.persistence.jpa.rest
- jpa
Build
- This code requires the JAX-RS public API in order to compile. The necessary CQ has already been filed.
- The build should build this feature into its own bundle.
API
As much shared behaviour as possible with be available in the super class:
package org.eclipse.persistence.jpa.rest; import java.util.List; import java.util.Map.Entry; import javax.persistence.*; import javax.ws.rs.*; import javax.ws.rs.core.*; public abstract class JPAResource<EntityType, KeyType> { protected Class<EntityType> entityClass; public JPAResource(Class<EntityType> entityClass) { this.entityClass = entityClass; } protected abstract EntityManager entityManager(); @POST @Consumes({"application/xml", "application/json"}) public Response create(@Context UriInfo uriInfo, EntityType entity) { ... } @GET @Path("singleResult/{namedQuery}") @Produces({"application/xml", "application/json"}) public EntityType namedQuerySingleResult(@Context UriInfo info) { ... } protected List<EntityType> namedQueryResultList(@Context UriInfo info) { ... } @PUT @Consumes({"application/xml", "application/json"}) public void update(EntityType entity) { ... } public void delete(KeyType id) { } protected abstract UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity); }
A specialized class will be available for a service based on a single key entity:
package org.eclipse.persistence.jpa.rest; import java.lang.reflect.Field; import javax.persistence.metamodel.SingularAttribute; import javax.ws.rs.*; import javax.ws.rs.core.UriBuilder; import org.eclipse.persistence.internal.security.PrivilegedAccessHelper; public abstract class JPASingleKeyResource<EntityType, KeyType> extends JPAResource<EntityType, KeyType> { public JPASingleKeyResource(Class<EntityType> entityClass) { super(entityClass); } @GET @Path("{id}") @Produces({"application/xml", "application/json"}) public EntityType read(@PathParam("id") KeyType id) { ... } @DELETE @Path("{id}") public void delete(@PathParam("id") KeyType id) { super.delete(id); } @Override protected UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity) { ... } }
Another specialized class will be available for a service based on a composite key entity:
package org.eclipse.persistence.jpa.rest; import java.lang.reflect.Field; import java.util.List; import java.util.Map.Entry; import javax.ws.rs.*; import javax.ws.rs.core.*; import org.eclipse.persistence.internal.helper.ConversionManager; import org.eclipse.persistence.internal.security.PrivilegedAccessHelper; public abstract class JPACompositeKeyResource<EntityType, KeyType> extends JPAResource<EntityType, KeyType> { public JPACompositeKeyResource(Class<EntityType> entityClass) { ... } @GET @Produces({"application/xml", "application/json"}) public EntityType read(@Context UriInfo info) { ... } @DELETE public void delete(@Context UriInfo info) { ... } private KeyType getPrimaryKey(UriInfo info) { ... } @Override protected UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity) { ... } }
GUI
TBD - Tooling could be provided outside of EclipseLink to support this feature.
Config files
This feature does not require the creation of any new configuration files. However the user will be responsible for providing the required JAX-RS, JPA, and JAXB config files.
JAX-RS
- The user will need to create the JAX-RS deployment artifacts appropriate to their deployment platform.
JPA
- The user will need to create the necessary artifacts for JPA
JAXB
- The user will need to create the necessary artifacts for JAXB
- jaxb.properties file to specify JAXB implementation
- eclipselink-oxm.xml as an alternate metadata representation
Documentation
Open Issues
This section lists the open issues that are still pending that must be decided prior to fully implementing this project's requirements.
Issue # | Owner | Description / Notes |
---|---|---|
3 | Blaise Doughan | Where will this live in the repository? Possible owner components: JPA, DBWS, new REST component. |
Decisions
This section lists decisions made. These are intended to document the resolution of open issues or constraints added to the project that are important.
Issue # | Description / Notes | Decision |
---|---|---|
1 | Parameter Type | After discussions with Paul Sandoz (JAX-RS lead). The decision was to use matrix parameters. |
2 | Should HTTP caching be used? | After some investigation, I have decided that no HTTP caching should be used with the initial release of this feature. |
Future Considerations
During the research for this project the following items were identified as out of scope but are captured here as potential future enhancements. If agreed upon during the review process these should be logged in the bug system.
- Supporting Atom Links to trim the tree
- Leveraging HTTP caching