Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
lpradel
Explorer
This article is the third in a series of three tutorials that cover the following topics:

  1. JPA2 in SAP NetWeaver

  2. Building server-side database-backed OData services in SAP NetWeaver with JPA2

  3. Building server-side database-backed OData services in SAP NetWeaver with JPA1


Introduction


In the previous tutorials we have demonstrated how to use JPA 2 in SAP NetWeaver and build database-backed OData services with it. The main takeaway of this last tutorial is how to do the same with plain JPA 1. Therefore this article is directed at developers who are stuck with JPA 1 on SAP NetWeaver AS Java (e.g. because they are bound to a specific version of SAP NetWeaver).

The main drawback of using JPA 1 to load your OData entities from your database is that you have to manually map JPA entities to OData entities as the JPA processor of Olingo requires the JPA 2 metamodel. We will demonstrate a generic approach to solving the mapping in the course of this article.

Prerequisites


We will implement the OData services with the Apache Olingo framework, specifically version 2.0.8 of Apache Olingo. Note that SAP NetWeaver 7.5 is a JEE5 application server, which means that it does not provide a JAX-RS implementation. Because of this, we must provide our own JAX-RS implementation. We decided to use Apache CXF, but you can use different implementations such as RESTEasy if you wish. In our scenario, the following artifacts are required:

  • commons-codec-1.6.jar

  • cxf-api-2.7.6.jar

  • cxf-rt-bindings-xml-2.7.6.jar

  • cxf-rt-core-2.7.6.jar

  • cxf-rt-frontend-jaxrs-2.7.6.jar

  • cxf-rt-transports-http-2.7.6.jar

  • geronimo-javamail_1.4_spec-1.7.1.jar

  • gson-2.4.jar

  • javax.ws.rs-api-2.0-m10.jar

  • jaxb-impl-2.1.13.jar

  • olingo-odata2-annotation-processor-api-2.0.8.jar

  • olingo-odata2-annotation-processor-core-2.0.8.jar

  • olingo-odata2-api-2.0.8.jar

  • olingo-odata2-api-annotation-2.0.8.jar

  • olingo-odata2-core-2.0.8.jar

  • olingo-odata2-jpa-processor-api-2.0.8.jar

  • olingo-odata2-jpa-processor-core-2.0.8.jar

  • stax2-api-3.1.1.jar

  • woodstox-core-asl-4.2.0.jar

  • wsdl4j-1.6.3.jar

  • xmlschema-core-2.0.3.jar


Pitfall:

We strongly advise not to use OData v4 in conjunction with SAPUI5, as the SAPUI5 support for OData v4 is lackluster and leads to a number of problems in practice. OData v2 however is fully supported by SAPUI5.

 

Pitfall:

Furthermore, we suggest using Hibernate rather than the default SAP JPA provider as your JPA implementation.

Bundle all artifacts in an external library "olingo/lib" DC and expose both api and archives as public parts of the DC.

Setting up Apache Olingo


Create a JPA2 JEE application with an EAR, EJB and WEB DC and make the EJB and WEB DC depend on the "olingo/lib" DC.

In your WEB-DC, edit the web.xml file to include the following:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>odata</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<filter>
<filter-name>OlingoClassloaderFilter</filter-name>
<filter-class>com.yourpackage.web.OlingoFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OlingoClassloaderFilter</filter-name>
<url-pattern>/*</url-pattern>
<servlet-name>ODataServlet</servlet-name>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet>
<servlet-name>ODataServlet</servlet-name>
<servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>org.apache.olingo.odata2.core.rest.app.ODataApplication</param-value>
</init-param>
<init-param>
<param-name>org.apache.olingo.odata2.service.factory</param-name>
<param-value>com.yourpackage.processor.AnnotationServiceFactory</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ODataServlet</servlet-name>
<url-pattern>/Data.svc/*</url-pattern>
</servlet-mapping>
</web-app>

The important bits are

  • the OlingoClassloaderFilter (which should be familiar from the previous tutorial)

  • the ODataServlet (it's a generic servlet given by the Olingo library) and finally

  • the AnnotationServiceFactory which is where the magic happens in the case of JPA 1.


Let's take a look at the AnnotationServiceFactory :
public class AnnotationServiceFactory extends ODataServiceFactory {

private static final Location LOG = Location.getLocation(AnnotationServiceFactory.class);

/**
* Instance holder for all annotation relevant instances which should be
* used as singleton instances within the ODataApplication (ODataService)
*/
private static class AnnotationInstances {
final static String MODEL_PACKAGE = "com.yourpackage.model";
final static ODataService ANNOTATION_ODATA_SERVICE;

static {
try {
ANNOTATION_ODATA_SERVICE = createAnnotationService(MODEL_PACKAGE);
} catch (ODataApplicationException ex) {
throw new RuntimeException("Exception during sample data generation.", ex);
} catch (ODataException ex) {
throw new RuntimeException("Exception during data source initialization generation.", ex);
}
}
}

private static ODataService createAnnotationService(final String modelPackage) throws ODataException {

AnnotationEdmProvider edmProvider = new AnnotationEdmProvider(modelPackage);
PersistentDataSource dataSource = new PersistentDataSource(modelPackage);
AnnotationValueAccess valueAccess = new AnnotationValueAccess();

// Edm via Annotations and ListProcessor via AnnotationDS with
// AnnotationsValueAccess
return RuntimeDelegate.createODataSingleProcessorService(edmProvider,
new ListsProcessor(dataSource, valueAccess));
}

@Override
public ODataService createService(final ODataContext context) throws ODataException {
// Edm via Annotations and ListProcessor via AnnotationDS with
// AnnotationsValueAccess
return AnnotationInstances.ANNOTATION_ODATA_SERVICE;
}

@SuppressWarnings("unchecked")
@Override
public <T extends ODataCallback> T getCallback(final Class<T> callbackInterface) {
return (T) (callbackInterface.isAssignableFrom(ScenarioErrorCallback.class) ? new ScenarioErrorCallback()
: callbackInterface.isAssignableFrom(ODataDebugCallback.class) ? new ScenarioDebugCallback()
: super.getCallback(callbackInterface));
}

/*
* Helper classes and methods
*/
private final class ScenarioDebugCallback implements ODataDebugCallback {

@Override
public boolean isDebugEnabled() {
return true;
}
}

private class ScenarioErrorCallback implements ODataErrorCallback {

@Override
public ODataResponse handleError(final ODataErrorContext context) throws ODataApplicationException {
if (context.getHttpStatus() == HttpStatusCodes.INTERNAL_SERVER_ERROR) {
LOG.errorT("Internal Server Error: " + context.getException().toString());
}

return EntityProvider.writeErrorDocument(context);
}
}
}

Essentially, we supply the package where our OData entities lie to our ODataService and create the objects required by the Olingo framework. Note that we have a different package where our JPA entities lie.

OData entities vs JPA entities


To give you a better picture of the two different entities here is an example of an OData entity:
@EdmEntityType(namespace = "com.yourpackage.model")
@EdmEntitySet(name = "Processes")
public class Process implements ProcessStepParent {

public static final String PROCESS_KEY = "Key";

@EdmKey
@EdmProperty(name = PROCESS_KEY)
private String key;

@EdmProperty
private String name;

@EdmProperty
private String helpText;

@EdmProperty
private String group;

@EdmNavigationProperty(toMultiplicity = Multiplicity.MANY, toType = ProcessStep.class)
private List<ProcessStep> steps;

@Override
public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getName() {
return name;
}

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

public String getHelpText() {
return helpText;
}

public void setHelpText(String helpText) {
this.helpText = helpText;
}

public String getGroup() {
return group;
}

public void setGroup(String group) {
this.group = group;
}

public List<ProcessStep> getSteps() {
return steps;
}

public void setSteps(List<ProcessStep> steps) {
this.steps = steps;
}
}

This entity will later be discoverable at /Data.svc/Processes (remind yourself of our web.xml file). Now, for the process entity we have a separate JPA entity class in the package com.yourpackage.domain.entity :
@Entity
@Table(name = "PROCESS")
public class ProcessEntity {

@Column(name = "Process_PK")
@Id
private String id;

@Column(name = "Process_Name")
private String name;

@Column(name = "Process_Help_Text")
private String helpText;

@Column(name = "Process_Group")
private String group;

@OneToMany(mappedBy = "process", cascade = { CascadeType.ALL })
@Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
private List<ProcessStepEntity> steps;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

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

public String getHelpText() {
return helpText;
}

public void setHelpText(String helpText) {
this.helpText = helpText;
}

public String getGroup() {
return group;
}

public void setGroup(String group) {
this.group = group;
}

public List<ProcessStepEntity> getSteps() {
return steps;
}

public void setSteps(List<ProcessStepEntity> steps) {
this.steps = steps;
}
}

Mapping between entities


Naturally, the question arises how we can inform the Olingo framework about the existence of the second entity and more importantly, how do we make Olingo understand that it needs to load the corresponding JPA entity from our database?

This is exactly where the PersistentDataSource comes into play that we use in our AnnotationServiceFactory. It is also the exact point where we "drill open" the Olingo framework and plug in our custom entity loading mechanism. Olingo provides an interface DataSource that holds all methods that any OData entity datasource needs to provide to the framework. Olingo itself provides an AnnotationInMemoryDs that is essentially an in-memory HashMap of all entites that exist in your application. Our PersistentDataSource however will bridge the gap between our database, the associated JPA entities and the OData entities:
public class PersistentDataSource implements DataSource {

private static final Location LOG = Location.getLocation(PersistentDataSource.class);

/**
* Maps the name of an EDM annotated POJO (as String) to the class of the
* corresponding data store EJB interface
*/
private static final Map<String, Class<? extends AbstractDataStore<?, ?>>> edmEntityDataStoreInterfaceLookup;

static {
edmEntityDataStoreInterfaceLookup = new HashMap<String, Class<? extends AbstractDataStore<?, ?>>>();

edmEntityDataStoreInterfaceLookup.put(Process.class.getName(), ProcessDataStore.class);
edmEntityDataStoreInterfaceLookup.put(ProcessStep.class.getName(), ProcessStepDataStore.class);
}

public PersistentDataSource(final String packageToScan) throws ODataException {

ClassHelper.loadClasses(packageToScan, new ClassHelper.ClassValidator() {
@Override
public boolean isClassValid(final Class<?> c) {
return null != c.getAnnotation(org.apache.olingo.odata2.api.annotation.edm.EdmEntitySet.class);
}
});
}

/**
* Loads the associated DataStore EJB for the EDM POJO represented by the
* EdmEntitySet.
*
* @param entitySet
* EdmEntitySet with underlying EDM POJO.
* @return DataStore EJB for underlying EDM POJO.
* @throws EdmException
*/
private static AbstractDataStore<?, ?> getDataStoreForEdmEntitySet(EdmEntitySet entitySet) throws EdmException {

String edmEntityName = mapEdmEntitySetToClassName(entitySet);
AbstractDataStore<?, ?> ds = getDataStoreForEdmEntityClass(edmEntityName);
return ds;
}

/**
* Converts EdmEntitySet to string of the fully qualified class name of the
* underlying EDM POJO. Example: EdmEntitySet for underlying class
* com.yourpackage.model.Process will be converted to
* "com.yourpackage.model.Process".
*
* @param entitySet
* EdmEntitySet with underlying EDM POJO.
* @return Fully qualified classname of kind "com.package.Pojo" of the
* underlying EDM POJO.
* @throws EdmException
*/
private static String mapEdmEntitySetToClassName(EdmEntitySet entitySet) throws EdmException {

StringBuilder sb = new StringBuilder();
sb.append(entitySet.getEntityType().getNamespace());
sb.append(".");
sb.append(entitySet.getEntityType().getName());
return sb.toString();
}

private static AbstractDataStore<?, ?> getDataStoreForEdmEntityClass(String edmEntityName) {

Class<? extends AbstractDataStore<?, ?>> dataStoreInterface = edmEntityDataStoreInterfaceLookup
.get(edmEntityName);
return getDataStoreBean(dataStoreInterface);
}

private static AbstractDataStore<?, ?> getDataStoreBean(
Class<? extends AbstractDataStore<?, ?>> dataStoreInterface) {

return ServiceLocator.getServiceInstance(dataStoreInterface, EarDc.ODATA_EAR, EjbDc.ODATA_EJB);
}

@Override
public List<?> readData(EdmEntitySet entitySet)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

AbstractDataStore<?, ?> ds = getDataStoreForEdmEntitySet(entitySet);
return ds.read();
}

@Override
public Object readData(EdmEntitySet entitySet, Map<String, Object> keys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

AbstractDataStore<?, ?> ds = getDataStoreForEdmEntitySet(entitySet);
Object result = ds.read(keys);
return result;
}

@Override
public Object readData(EdmFunctionImport function, Map<String, Object> parameters, Map<String, Object> keys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

@Override
public Object readRelatedData(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
Map<String, Object> targetKeys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

AbstractDataStore<?, ?> sourceDs = getDataStoreForEdmEntitySet(sourceEntitySet);
AbstractDataStore<?, ?> targetDs = getDataStoreForEdmEntitySet(targetEntitySet);
Object result = targetDs.readRelatedData(sourceDs.getDataTypeClass(), sourceData, targetDs.getDataTypeClass(),
targetKeys);

return result;
}

@Override
public BinaryData readBinaryData(EdmEntitySet entitySet, Object mediaLinkEntryData)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

@Override
public Object newDataObject(EdmEntitySet entitySet)
throws ODataNotImplementedException, EdmException, ODataApplicationException {

AbstractDataStore<?, ?> ds = getDataStoreForEdmEntitySet(entitySet);
return ds.createInstance();
}

@Override
public void writeBinaryData(EdmEntitySet entitySet, Object mediaLinkEntryData, BinaryData binaryData)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

@Override
public void deleteData(EdmEntitySet entitySet, Map<String, Object> keys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

AbstractDataStore<?, ?> ds = getDataStoreForEdmEntitySet(entitySet);
ds.delete(keys);
}

@Override
public void createData(EdmEntitySet entitySet, Object data)
throws ODataNotImplementedException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

@Override
public void deleteRelation(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
Map<String, Object> targetKeys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

@Override
public void writeRelation(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
Map<String, Object> targetKeys)
throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
}

}

The basic idea is to hold a map of all OData entities that exist in our application in the static initialization code of the class (in our case just two entities Process and ProcessStep) that maps the OData entities to DataStores which are in turn EJBs that use DAOs to load the associated JPA entities from the database and then perform the mapping between the two entity classes. Using this approach, we can ensure type safety for a good portion of the DataStore interface (the readData methods and deleteData). Unfortunately certain methods of the interface are typed with Object so that we have no other options but to use object as well.

Now, the AbstractDataStore interface looks like this:
public interface AbstractDataStore<T, K> {

public Class<?> getDataTypeClass();

public String getEntityTypeName();

public T createInstance();

public K mapEdmObjectToJpaEntity(T object);

public T mapJpaEntityToEdmObject(K entity);

public List<K> mapEdmObjectsToJpaEntities(Collection<T> objects);

public List<T> mapJpaEntitiesToEdmObjects(Collection<K> entities);

public T read(final Map<String, Object> keys);

public List<T> read();

public T create(final T obj);

public T update(final T obj);

public void delete(final Map<String, Object> keys);

public void delete(final T obj);

/**
* Returns the field of type <code>targetTypeClass</code> that
* <code>relatedObject</code> of type <code>relatedTypeClass</code> has. The
* contract for reading relations with this method is as follows: If for
* example the relatedObject is of type <code>A</code>, that has a
* one-to-many relationship to <code>B</code> through a List of the target
* type <code>B</code> and <code>B</code> is mapped bi-directional with a
* single object field of type <code>A</code>, then to get
*
* (1) the related List of <code>B</code>, you call this method on the
* DataStore for <code>B</code> and pass <code>A</code> as related class
*
* (2) the related parent object for <code>A</code>, you call this method on
* the DataStore for <code>A</code> and pass <code>B</code> as related
* class.
*
* That is to say, that you always <b>call this method on the DataStore for
* the target class</b>, passing the "parent" object as the
* <code>relatedObject</code>.
*
* @param relatedTypeClass
* @param relatedObject
* @param targetTypeClass
* @param targetKeys
* @return
*/
public Object readRelatedData(Class<?> relatedTypeClass, Object relatedObject, Class<?> targetTypeClass,
Map<String, Object> targetKeys);
}

The (abstract) base implementation of a DataStore (that is common to all entities) is as follows:
/**
*
* @param <EdmType>
* The type of the associated EDM object (i.e. a class annotated with
* {@link org.apache.olingo.odata2.api.annotation.edm.EdmEntityType}}
* @param <EntityType>
* The type of the corresponding JPA entity (i.e. a class annotated
* with {@link javax.persistence.Entity}
* @param <EntityKeyType>
* The type of the primary key of the JPA entity
*/
public abstract class AbstractDataStoreBean<EdmType, EntityType, EntityKeyType> {

protected static final AnnotationHelper ANNOTATION_HELPER = new AnnotationHelper();

public abstract Class<?> getDataTypeClass();

public abstract Class<?> getEntityClass();

public String getEntityTypeName() {
return ANNOTATION_HELPER.extractEntityTypeName(getDataTypeClass());
}

@SuppressWarnings("unchecked")
public EdmType createInstance() {
try {
return (EdmType) getDataTypeClass().newInstance();
} catch (InstantiationException e) {
throw new AnnotationRuntimeException("Unable to create instance of class '" + getDataTypeClass() + "'.", e);
} catch (IllegalAccessException e) {
throw new AnnotationRuntimeException("Unable to create instance of class '" + getDataTypeClass() + "'.", e);
}
}

protected abstract GenericDao<EntityType, EntityKeyType> getDao();

public abstract List<Class<?>> getRelatedEdmEntityTypeClasses();

protected abstract EntityKeyType mapKeyMapToJpaEntityKey(Map<String, Object> keyMap);

/**
* Maps an EDM object to its corresponding JPA entity.
*
* @param The
* EDM object
* @return The corresponding JPA entity object
*/
public abstract EntityType mapEdmObjectToJpaEntity(EdmType object);

public List<EntityType> mapEdmObjectsToJpaEntities(Collection<EdmType> objects) {

if (objects == null || objects.isEmpty()) {
return Collections.<EntityType>emptyList();
}

List<EntityType> result = objects.stream().map(object -> mapEdmObjectToJpaEntity(object))
.collect(Collectors.toList());

return result;
}

public List<EdmType> mapJpaEntitiesToEdmObjects(Collection<EntityType> entities) {

if (entities == null || entities.isEmpty()) {
return Collections.<EdmType>emptyList();
}

List<EdmType> result = entities.stream().map(entity -> mapJpaEntityToEdmObject(entity))
.collect(Collectors.toList());

return result;
}

/**
* Maps a JPA entity to its corresponding EDM object.
*
* @param entity
* The JPA entity object
* @return The corresponding EDM object.
*/
public abstract EdmType mapJpaEntityToEdmObject(EntityType entity);

private void assertKeyMapContainsKey(Map<String, Object> keyMap, String key) {

if (!keyMap.containsKey(key)) {
throw new IllegalArgumentException("Key map for EDM object must contain key named \"" + key + "\"");
}
}

/**
* Ensures that <code>keyMap</code> contains entries for all keys in
* <code>keys</code>. If a key in <code>keys</code> does not exist in the
* map, an {@link IllegalArgumentException} is raised.
*
* @param keyMap
* The map to check for keys
* @param keys
* The list of keys that must be in <code>keyMap</code>
*/
protected void assertKeyMapContainsKeys(Map<String, Object> keyMap, List<String> keys) {

keys.forEach((key) -> assertKeyMapContainsKey(keyMap, key));
}

protected void validateRelatedTypeClasses(Class<?> relatedTypeClass, Class<?> targetTypeClass) {

validateTargetTypeClass(targetTypeClass);
validateRelatedTypeClass(relatedTypeClass);
}

protected void validateTargetTypeClass(Class<?> targetTypeClass) {

if (targetTypeClass != getDataTypeClass()) {
throw new IllegalArgumentException("This DataStore can only return EDM entities of type "
+ getDataTypeClass().getName()
+ "! Please set the targetTypeClass parameter accordingly, or call the readRelatedData method on the correct DataStore.");
}
}

protected void validateRelatedTypeClass(Class<?> relatedTypeClass) {

if (!(getRelatedEdmEntityTypeClasses().contains(relatedTypeClass))) {
throw new IllegalArgumentException("The EDM entity " + getDataTypeClass().getName()
+ " of this DataStore is not related to the EDM entity " + relatedTypeClass.getName() + "!");
}
}

public abstract Object readRelatedData(Class<?> relatedTypeClass, Object relatedObject);

public Object readRelatedData(Class<?> relatedTypeClass, Object relatedObject, Class<?> targetTypeClass,
Map<String, Object> targetKeys) {

validateRelatedTypeClasses(relatedTypeClass, targetTypeClass);

// If the target object is determined by a (composite) key, we simply
// load it by this primary key
if (!targetKeys.isEmpty()) {
return read(targetKeys);
}

// Otherwise we need to load it by its association to the relatedObject
Object result = readRelatedData(relatedTypeClass, relatedObject);

if (result == null) {
throw new IllegalStateException(
"Ensure that the method getRelatedDataTypeClasses of this DataStore is correct!");
}
return result;
}

public List<EdmType> read() {

List<EntityType> jpaEntities = getDao().findAll();
List<EdmType> mappedEdmObjects = mapJpaEntitiesToEdmObjects(jpaEntities);
return mappedEdmObjects;
}

public EdmType read(EntityKeyType primaryKey) {

Optional<EntityType> optionalEntity = Optional.ofNullable(getDao().find(primaryKey));

return (optionalEntity.isPresent() ? mapJpaEntityToEdmObject(optionalEntity.get()) : null);
}

/**
* Load an Olingo EDM object by a primary key that consists of all fields
* annotated with
* {@link org.apache.olingo.odata2.api.annotation.edm.EdmKey}.
*
* Example: Suppose an EDM object has two fields <code>key1</code> and
* <code>key2</code> annotated with
* {@link org.apache.olingo.odata2.api.annotation.edm.EdmKey}. Then, the
* <code>keys</code> map should contain two keys "Key1" and "Key2" (note the
* capitalization). This method converts the map of keys to a primary key of
* the corresponding JPA entity and loads the entity with this primary key
* from the database.
*
* @param keys
* Map with keyvalues for each field annotated with
* {@link org.apache.olingo.odata2.api.annotation.edm.EdmKey}.
* @return The EDM object with the (composite) key represented by the
* <code>keys</code> map, iff such an entity with this key exists in
* the database.
*/
public EdmType read(Map<String, Object> keys) {

EntityKeyType key = mapKeyMapToJpaEntityKey(keys);
return read(key);
}

public EdmType create(EdmType obj) {

EntityType mappedEntity = mapEdmObjectToJpaEntity(obj);
EntityType persistentEntity = getDao().save(mappedEntity);
EdmType mappedEdmObject = mapJpaEntityToEdmObject(persistentEntity);
return mappedEdmObject;
}

public EdmType update(EdmType obj) {

EntityType mappedEntity = mapEdmObjectToJpaEntity(obj);
EntityType persistentEntity = getDao().update(mappedEntity);
EdmType mappedEdmObject = mapJpaEntityToEdmObject(persistentEntity);
return mappedEdmObject;
}

public void delete(Map<String, Object> keys) {

EntityKeyType key = mapKeyMapToJpaEntityKey(keys);
Optional<EntityType> optionalEntity = Optional.ofNullable(getDao().find(key));
optionalEntity.ifPresent(entity -> getDao().remove(entity));
}

public void delete(EdmType obj) {

EntityType mappedEntity = mapEdmObjectToJpaEntity(obj);
getDao().remove(mappedEntity);
}
}

Essentially, all DataStores rely on two mechanisms:

  • a (generic) JPA DAO that loads the JPA entities associated with our OData entity from the database and performs CRUD operations on it

  • abstract mapping methods that convert one entity into the other (plus abstract methods that provide the class of each entity).


A concrete DataStore


To give you a clearer picture, we will provide a specific sample DataStore for the Process entity. The code itself is rather straight-forward from this point and should be self-explanatory.
@Local
public interface ProcessDataStore extends AbstractDataStore<Process, ProcessEntity> {

}

@Stateless
public class ProcessDataStoreBean extends AbstractDataStoreBean<Process, ProcessEntity, String>
implements ProcessDataStore {

@EJB
ProcessEntityDao processDao;

@EJB
ProcessStepDataStore processStepDS;

@Override
public Class<?> getDataTypeClass() {
return Process.class;
}

@Override
public Class<?> getEntityClass() {
return ProcessEntity.class;
}

@Override
public Process createInstance() {
return (Process) super.createInstance();
}

@Override
protected GenericDao<ProcessEntity, String> getDao() {
return processDao;
}

@Override
public List<Class<?>> getRelatedEdmEntityTypeClasses() {

return Arrays.asList(ProcessStep.class);
}

@Override
protected String mapKeyMapToJpaEntityKey(Map<String, Object> keyMap) {

assertKeyMapContainsKeys(keyMap, Arrays.asList(Process.PROCESS_KEY));
String result = (String) keyMap.get(Process.PROCESS_KEY);
return result;
}

@Override
public ProcessEntity mapEdmObjectToJpaEntity(Process object) {

ProcessEntity result = new ProcessEntity();

result.setId(object.getKey());
result.setName(object.getName());
result.setGroup(object.getGroup());
result.setHelpText(object.getHelpText());
result.setSteps(processStepDS.mapEdmObjectsToJpaEntities(object.getSteps()));

return result;
}

@Override
public Process mapJpaEntityToEdmObject(ProcessEntity entity) {

Process result = new Process();

result.setKey(entity.getId());
result.setName(entity.getName());
result.setGroup(entity.getGroup());
result.setHelpText(entity.getHelpText());
result.setSteps(processStepDS.mapJpaEntitiesToEdmObjects(entity.getSteps()));

return result;
}

@Override
public Object readRelatedData(Class<?> relatedTypeClass, Object relatedObject) {

if (relatedTypeClass == ProcessStep.class) {

ProcessStep relatedProcessStep = (ProcessStep) relatedObject;
String processKey = relatedProcessStep.getProcessKey();

Process targetObject = read(processKey);
return targetObject;
}

return null;
}
}

Note that the only "ugly" part of this code is that we have to hard-code the information which related entities this specific entitiy has in the getRelatedEdmEntityTypeClasses and readRelatedData methods.

For the sake of completeness we will include an example of a generic DAO as well (this is not really related to the Olingo portion of this tutorial):
public interface GenericDao<E, K> {

public E find(K primaryKey);

public List<E> findAll();

public E save(E entity);

public void removeAll();

public void remove(E entity);

public E update(E entity);
}

public abstract class GenericDaoBean<E, K> implements GenericDao<E, K> {

private Class<E> entityClass;

public GenericDaoBean(Class<E> entityClass) {
this.entityClass = entityClass;
}

@Override
public E find(K primaryKey) {

E entity = getEntityManager().find(entityClass, primaryKey);
return entity;
}

@Override
@SuppressWarnings("unchecked")
public List<E> findAll() {

Query q = getEntityManager().createQuery("SELECT e FROM " + entityClass.getName() + " e");
List<E> list = (List<E>) q.getResultList();
return list;
}

@Override
public E save(E entity) {

getEntityManager().persist(entity);
return entity;
}

@Override
public void removeAll() {

Query q = getEntityManager().createQuery("DELETE FROM " + entityClass.getName());
q.executeUpdate();
}

@Override
public void remove(E entity) {

getEntityManager().remove(entity);
}

@Override
public E update(E entity) {

E mergedEntity = getEntityManager().merge(entity);
return mergedEntity;
}

protected abstract EntityManager getEntityManager();
}

Conclusion


In this tutorial, we have demonstrated how to build a generic mapping mechanism to convert between OData entities and JPA entities in a (mostly) type-safe way (as we pointed out, this is due to interface limitations of the Olingo library itself). The necessity to perform is born out of the lack of native JPA 1 support in Olingo.

Note that we have only implemented the methods of the DataSource interface that we needed for our purposes. However, it should be straight-forward to extend our framework to also implement additional methods of the interface as needed in your application setting.

Finally, as always: if you are experiencing problems, need assistance, or would like to see a minimal sample project, do not hesitate to contact us via SAP Community or by commenting on this article.

This concludes our series of tutorials on building database-backed OData services with SAP NetWeaver AS Java. We are looking forward to improved support of OData v4 in future versions of SAPUI 5 and working with newer versions of Apache Olingo for building OData v4 services.
5 Comments
Labels in this area