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: 
Former Member

As part of my work in bouvet, I've started experimenting with the new SAP Netweaver Cloud (aka. Neo and JPaaS) and boy is there much exciting stuff to share.

SAP NetWeaver Cloud has a free developer license, that is currently limited to a 90 days trial. Get access by following the instructions here. Removing the 90 day trial limitation is definitively something SAP wants, and it is being worked on (ref https://twitter.com/#!/schmerdy/status/202783043075846145). The @SAPMentors will do our utmost to make sure SAP delivers on this promise.

Update 25.05: Added information on how to support cross-origin resource sharing .

Update 22.10: Project is now shared on Github https://github.com/elsewhat/nwcloud-rest_example

Overview

The goal of this blog is to show how you can expose data through a REST API from an application hosted on the SAP NetWeaver Cloud.

Going into this experiment, I had the following goals:

  1. Data must be persisted in SAP NetWeaver Cloud
  2. REST API must support the operations: read all objects, read single object, create single object and update single object
  3. No coding for xml and json marshalling and unmarshalling
  4. Extensible to more complex objects
  5. To be consumed by a sapui5 client (next blog?)
    (the sapui5 client can subsequently be consumed in SAP NetWeaver Cloud Portal! ref @ohad_yassin)

One of the main benefits of  basing the SAP NetWeaver Cloud on the JVM and Java, is that there is a huge selection of mature and production ready libraries and frameworks. You are truly standing on the shoulders of giants that have small java-coding hands.

I had already decided to use JPA as the persistency layer, and SAP has a great tutorial in place on how to use it with SAP NW Cloud.

For the REST framework, I quickly found out that the Java Community Process had produced a standard extension to java named JAX-RS: Java API for RESTful Web Services. The reference implementation of this, called Jersey, is well suited to be include in a SAP NetWeaver Cloud application.

For the data behind the REST service, I choose a simple FeedEntry object that represents an update in a social feed.

It basically consists of a sender and a text. Simple, yet useful in several contexts.

Demo

The REST API is publicly available on #SAPNWCloud , so you can test it right now!

For testing REST APIs it's common to use the command line tool curl. It is part of most linux and unix distributions and If you are on windows or amiga you can download it from here .

Here are a few examples you can try out yourself.

Read all objects call:

curl -k -i -X GET -H 'Content-Type: application/json' https://feedstream1p013234trial.netweaver.ondemand.com/feed_stream/api/feed/
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 25 May 2012 08:01:42 GMT
Server: SAP
Set-Cookie: cookie_f=370167306.26911.0000; path=/
{"feedEntry":[{"feedText":"First post with the REST API","id":"1","isComment":"f
alse","senderEmail":"dagfinn.parnas@bouvet.no","senderName":"Dagfinn Parnas"},{"
feedText":"UPDATE:Bouvet experiment with #sapnwcloud seems to work well","id":"2
","isComment":"false","senderEmail":"dagfinn.parnas@gmail.com","senderName":"Dag
finn Parnas"}]}

Create new feed:

curl -k -i -X POST -H 'Content-Type: application/json' -d '{"senderName":"Dagfinn Parnas",
"feedText":"Bouvet experiment with #sapnwcloud seems to work well","isComment":false,"senderEmail":"dagfinn.parnas@gmail.com"}'
https://feedstream1p013234trial.netweaver.ondemand.com/feed_stream/api/feed

Update existing feed:

curl -k -i -X POST -H 'Content-Type: application/json' -d '{"feedText":"UPDATE:Bouvet experiment with #sapnwcloud seems to work well"}'
https://feedstream1p013234trial.netweaver.ondemand.com/feed_stream/api/feed/2

How to

Show us the code!

Step 1

Complete the tutorial "Adding Persistence to a Neo Application Using JPA"

After doing this, you should in eclipse have two projects:

  1. A project representing your JPA data model
    (named JPAModel)
  2. A web project which demonstrates reading and updating the data persisted in your JAP data model
    (name Hello World)

Both of these eclipse projects will be used. The JPA project will represent our data model FeedEntry and the web project will become our REST API

Step 2

Download the java library files from Jersey project.

  • Go to http://jersey.java.net
  • Select download
  • Download from the link that says: "A zip of Jersey containing the Jersey jars, core dependencies (it does not provide dependencies for third party jars beyond those for JSON support) "
  • Unzip it to a local temp folder

(it would be much better to set up this dependency to SAP NW Cloud through Maven. I'll leave that as an exercise and a potential future blog)

Step 3

Next step is to copy the jersey libraries to the web project.

Copy all jersey library files (.jars) from the lib subfolder in step 2 to your web project under the path WebContent/WEB-INF/lib

Here is my web project structure after the copy operation.

(please note that in the screenshot I have a few extra .jar files. This doesn't matter to this blog and is because I started out with the sapui5 getting-started web project)

Step 4

Next step is to setup jersey in the web project. Our goal is that all urls matching http://<server>/<web_app_context_root>/api/* are processed using jersey.

We do this setup in the Deplyoment descriptor of the web project. You can either access it from the Deployment Descriptor object in the project structure or directly to the web.xml file it represents. The web.xml file is located under WebContent/WEB-INF/web.xml.

This is the contents you need to have

<?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" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <display-name>feed_stream</display-name>
  <servlet>
    <servlet-name>Jersey REST Service</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>no.bouvet.sap.neo.rest</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey REST Service</servlet-name>
    <url-pattern>/api/*</url-pattern>
  </servlet-mapping>
</web-app>

Please note that you may have multiple other lines in the web.xml file, for example for a HelloWorldServlet. You do not need to delete them.

How does this work? Let me explain the three simple steps:

  1. We define a servlet which is instansiated through the class com.sun.jersey.spi.container.servlet.ServletContainer.
    This class is included in the java libraries we copied in the previous step
  2. This servlet has a parameter which is jersey specific. This parameter must have the value of the java package name where jersey should look for REST based services. It must match the package name we use in the FeedResource class later
  3. We map the Jersey servlet to all incoming urls with pattern /api/*

Step 5

We now have configured jersey for our web application, and are ready to implement our REST-based service.

Before we look at the actual service implementation, we'll extend our data model to the fields we need.

This step is done on the JPAModel project (and not the web project).

Right-click JPA Content and select Open Diagram

In the diagram, add new fields for the attributes you'd like.

I've renamed both the JPAModel project to FeedModel and the entity to FeedEntry. Note that you need to refer to the project name (or more correctly, the persistency model name) in the REST service class later.

Here are is my model at the end.

My JPA model has the same query defined  as the JPA tutorial (name AllFeedEntries)

Step 6

The JPA Model we saw in previous step is represented in a Java class.

We must update this class, so that Jersey understands that this is a REST data object which should be marshalled and unmarshalled based on requests.

In order to do this, we need only to add the notation @XmlRootElement to the before the java class declaration.

In the JPA Model project, open the java class under src\org.persistence\FeedEntry.java

(you name may be slightly different).

For completeness sake, I've included the whole FeedEntry.java class here. Note that I've only added the @XmlRootElement here, the rest of the class is generated based on the JPA modelling you did in step 5.

package org.persistence;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Date;
@XmlRootElement
@Entity
@Table(name = "T_FEEDENTRY")
@NamedQuery(name = "AllFeedEntries", query = "select f from FeedEntry f")
public class FeedEntry {
          @Id
          @GeneratedValue
          private long id;
          @Basic
          private String senderName;
          @Basic
          private String feedText;
          @Basic
          private String senderEmail;
          @Basic
          private boolean isComment;
          @Basic
          private String parent;
          @Temporal(TemporalType.TIMESTAMP)
          @Basic
          private Date timeCreated;
          public long getId() {
                    return id;
          }
          public void setId(long id) {
                    this.id = id;
          }
          public void setSenderName(String param) {
                    this.senderName = param;
          }
          public String getSenderName() {
                    return senderName;
          }
          public void setFeedText(String param) {
                    this.feedText = param;
          }
          public String getFeedText() {
                    return feedText;
          }
          public void setSenderEmail(String param) {
                    this.senderEmail = param;
          }
          public String getSenderEmail() {
                    return senderEmail;
          }
          public void setIsComment(boolean param) {
                    this.isComment = param;
          }
          public boolean isIsComment() {
                    return isComment;
          }
          public void setParent(String param) {
                    this.parent = param;
          }
          public String getParent() {
                    return parent;
          }
          public void setTimeCreated(Date param) {
                    this.timeCreated = param;
          }
          public Date getTimeCreated() {
                    return timeCreated;
          }
}

Step 7

All we are left with now is to implement the actual REST service class. As mentioned in the introduction, we want it to support the operations: read all objects, read single object, create single object and update single object. The operations should support both JSON and XML as input and output based on the preferences of the client. The object we are building the REST service on top of is the FeedEntry from step 6.

The code is very simple due to jersey's clever usage of annotations. This means that we can have the service as plain old java object (aka POJO) and therefore do not have to think about the traditional complexities of Java EE.

package no.bouvet.sap.neo.rest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.persistence.FeedEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bouvet Experiment: REST API for the FeedEntry class
*
* Uses annotations from JAX-RS and JAXB
* Is called from the Jersey servlet com.sun.jersey.spi.container.servlet.ServletContainer
*
* This class is processed if url is http(s)://<server>/<appl_context_root>/<jersey_root>/feed/*
*
* @author dagfinn.parnas@bouvet.no
*/
@Path("/feed")
public class FeedResource {
          final Logger logger = LoggerFactory.getLogger(FeedResource.class);
          //Ask jersey to populate this parameter for one of the REST methods
          @Context
          UriInfo uriInfo;
          //attributes used for reading/writing to JPA persistence
          private static DataSource ds;
          private static EntityManagerFactory emf;
          /**
           * Constructor needs to have no parameters.
           * It will initialize the datasource we are using (JPA)
           *
           */
          public FeedResource(){
                    try {
                              initPersistencyLayer();
                    }catch (Exception e) {
                              //TODO: Handle better
                              logger.error("Failed to initialize persistency layer", e);
                    }
          }
          /**
           * Main method that returns all feeds in the persistency layer.
           * It can produce the content in either JSON or XML (based on client preferences).
           * Jersey handles the marshalling automatically.
           *
           * Curl example (return all feeds in json format):
           * $ curl  -i -H "Accept: application/json"
           * http://localhost:8080/feed_stream/api/feed/
           */
          @GET
          @Produces( { MediaType.APPLICATION_JSON ,  MediaType.APPLICATION_XML})
          public List<FeedEntry> getAllFeedEntries() {
                    //Get all feed entries from persistency layer
                    EntityManager em = emf.createEntityManager();
                    List<FeedEntry> resultList = em.createNamedQuery("AllFeedEntries",
                                        FeedEntry.class).getResultList();
                    //Logging
                    String message = (resultList==null)? "getAllFeedEntries returning null": "getAllFeedEntries returning " + resultList.size() + " entries";
                    logger.info(message);
                    return resultList;
          }
          /**
           * Method that returns all feeds in the persistency layer.
           * Can be used for testing in the browser, as it request
           * the media type we expose in this method
           */
          @GET
          @Produces( { MediaType.TEXT_XML })
          public List<FeedEntry> getAllFeedEntriesForHTML() {
                    EntityManager em = emf.createEntityManager();
                    List<FeedEntry> resultList = em.createNamedQuery("AllFeedEntries",
                                        FeedEntry.class).getResultList();
                    //Logging
                    String message = (resultList==null)? "getAllFeedEntries returning null": "getAllFeedEntries returning " + resultList.size() + " entries";
                    logger.info(message);
                    return resultList;
          }
          /**
           * Return a single feed entry based on ID
           * Will be called if request has syntax /feed/<feed id>
           * It can produce the content in either JSON or XML (based on client preferences)
           *
           * Curl example (return feed with id 2)
           * $ curl  -i -H "Accept: application/json"
           * http://localhost:8080/feed_stream/api/feed/2
           */
          @GET
          @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
          @Path("{feedid}/")
    public FeedEntry getSingleFeed(@PathParam("feedid") String strFeedId) {
                    EntityManager em = emf.createEntityManager();
                    //Logging
                    logger.error("getSingleFeed with id:"+ strFeedId + " called");
                    try {
                              long feedId = Long.parseLong(strFeedId);
                              FeedEntry feedEntry = em.find(FeedEntry.class, feedId);
                              return feedEntry;
                    }catch (NumberFormatException e1) {
                              // TODO: Input parameter is not a long and therefore not a valid primary key
                              logger.warn("getSingleFeed for " + strFeedId + " is not a valid key", e1);
                    }catch (IllegalArgumentException e2){
                              //Invalid type of parameter . Should not happen normally
                              logger.warn("getSingleFeed for " + strFeedId + " gave exception", e2);
                    }
                    return null;
    }
          /**
           * POST a new object and store it in the persistency layer
           * Must be called with the HTTP POST method
           * and accepts input in both JSON and XML format.
           *
           * Curl example (creates new feed):
           * $ curl -i -X POST -H 'Content-Type: application/json'
           * -d '{"senderName":"Jane Doe","feedText":"test","isComment":false,"senderEmail":"dagfinn.parnas@bouvet.no"}'
           * http://localhost:8080/feed_stream/api/feed/
           *
           * @param feedEntry
           * @return
           */
          @POST
          @Consumes( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
          public Response createSingleFeed(FeedEntry feedEntry) {
                    //The feedEntry is automatically populated based on the input. Yeah!
                    logger.info("Creating new feed ");
                    //persist the entry
                    EntityManager em = emf.createEntityManager();
                    em.getTransaction().begin();
                    em.persist(feedEntry);
                    em.getTransaction().commit();
                    //The HTTP response should include the URL to the newly generated new entry.
                    //Probably exist a better way of doing this, but it works
                    try {
                              URI createdURI = new URI(uriInfo.getAbsolutePath()+""+ feedEntry.getId());
                              return Response.created(createdURI).build();
                    } catch (URISyntaxException e) {
                              logger.warn("Unable to create correct URI for newly created feed " + feedEntry, e);
                              //fallback is to include the input path (which will be lacking the id of the new object)
                              return Response.created(uriInfo.getAbsolutePath()).build();
                    }
          }
          /**
           * Update one or more fields of a single feed entry
           *
           * Curl example (updates senderEmail for feed with id 2) :
           * $ curl -i -X POST -H 'Content-Type: application/json'
           * -d '{"senderEmail":"dagfinn.parnas@gmail.com"}' http://localhost:8080/feed_stream/api/feed/2
           */
          @POST
          @Consumes( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
          @Path("{feedid}/")
    public Response updateSingleFeed(@PathParam("feedid") String strFeedId, FeedEntry modifiedFeedEntry) {
                    logger.info("updateSingleFeed with id:"+ strFeedId);
                    try {
                              long feedId = Long.parseLong(strFeedId);
                              EntityManager em = emf.createEntityManager();
                              FeedEntry currentFeedEntry = em.find(FeedEntry.class, feedId);
                              if(currentFeedEntry==null){
                                        logger.warn("updateSingleFeed failed as " + strFeedId + " does not exist");
                                        return Response.notModified(strFeedId + " does not exist").build();
                              }
                              //allow the post to only have one or more fields updated
                              if(modifiedFeedEntry.getParent()!=null){
                                        currentFeedEntry.setParent(modifiedFeedEntry.getParent());
                              }
                              if(modifiedFeedEntry.getSenderEmail()!=null){
                                        currentFeedEntry.setSenderEmail(modifiedFeedEntry.getSenderEmail());
                              }
                              if(modifiedFeedEntry.getSenderName()!=null){
                                        currentFeedEntry.setSenderName(modifiedFeedEntry.getSenderName());
                              }
                              if(modifiedFeedEntry.getFeedText()!=null){
                                        currentFeedEntry.setFeedText(modifiedFeedEntry.getFeedText());
                              }
                              if(modifiedFeedEntry.getTimeCreated()!=null){
                                        currentFeedEntry.setTimeCreated(modifiedFeedEntry.getTimeCreated());
                              }
                              //store in persistency store
                              em.getTransaction().begin();
                              em.persist(currentFeedEntry);
                              em.getTransaction().commit();
                              //return an ok response
                              return Response.ok().build();
                    }catch (NumberFormatException e1) {
                              // TODO: Input parameter is not a long and therefore not a valid primary key
                              logger.warn("getSingleFeed for " + strFeedId + " is not a valid key", e1);
                              return Response.serverError().build();
                    }catch (IllegalArgumentException e2){
                              //Invalid type of parameter . Should not happen normally
                              logger.warn("getSingleFeed for " + strFeedId + " gave exception", e2);
                              return Response.serverError().build();
                    }
    }
          /**
           * Initialize the persistency layer (JPA)
           *
           * @throws Exception
           */
          private void initPersistencyLayer() throws Exception  {
                    try {
                              logger.debug("Setting up persistency layer for FeedResource");
                              InitialContext ctx = new InitialContext();
                              ds = (DataSource) ctx.lookup("java:comp/env/jdbc/DefaultDB");
                              Map properties = new HashMap();
                              properties.put(PersistenceUnitProperties.NON_JTA_DATASOURCE, ds);
                              //IMPORTANT! The first parameter must match your JPA Model name in persistence.xml
                              emf = Persistence.createEntityManagerFactory("FeedModel", properties);
                    } catch (NamingException e) {
                              //TODO: Handle exception better
                              logger.error("FATAL: Could not intialize database", e);
                              throw new Exception(e);
                    }
          }
}

That's all there is to it.

Cross-origin resource sharing

In order for this service to be consumed through javascript from a html page on a different host, you need to setup cross-orign resource sharing. Basically, this means to add a few http headers to all api calls.

Jersey allows you to define filters that processed are called for all methods.

You need to create a new class and refer to this class in the deployment descriptor web.xml file as a property to the Jersey servlet.

Filter class

package no.bouvet.sap.neo.rest.filter;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerResponse;
import com.sun.jersey.spi.container.ContainerResponseFilter;
/**
* Filter for adding Access-Control-Allow-Origin headers for all API methods
*
* Is registered in the properties to Jersey in web.xml
*
* @author dagfinn.parnas
*
*/
public class ResponseCorsFilter implements ContainerResponseFilter {
    @Override
    public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
              response.getHttpHeaders().add("Access-Control-Allow-Origin", "*");
              response.getHttpHeaders().add("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE");
              //allow any request headers sent by the client
                    String requestACRHeaders = request.getHeaderValue("Access-Control-Request-Headers");
                    if(requestACRHeaders!=null && !"".equals(requestACRHeaders.trim())){
                                   response.getHttpHeaders().add("Access-Control-Allow-Headers", requestACRHeaders);
                    }
        return response;
    }
}

Web.xml configuration

 <init-param>
      <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
      <param-value>no.bouvet.sap.neo.rest.filter.ResponseCorsFilter</param-value>
</init-param>

18 Comments
Labels in this area