Skip to Content
SBOP SDK

Navigating Enterprise Folders using the RestFul SDK

Tags:

Description

The sample below was written in Java and demonstrates how to use the Restful SDK to browse the folder structure in Enterprise and use the existing logontoken to view the reports using Opendocument.

Restful call sequence

This section will list the restful calls made by this sample in case you want to translate it into another language.

Logon to Enterprise

Type of call: POST
URL: http://localhost:6405/biprws/logon/long

Headers:
Accept: application/xml

Payload:
<attrs>
     <attr name="userName" type="string" >Administrator</attr>
     <attr name="password" type="string" >MyPassword</attr>
     <attr name="auth" type="string" possibilities="secEnterprise,secLDAP,secWinAD,secSAPR3">secEnterprise</attr>
</attrs>

Expected Response:

<entry>
     <author>
          <name>@MyServer:6400</name>
     </author>
     <id>tag:sap.com,2010:bip-rs/logon/long</id>
     <title type="text">Logon Result</title>
     <updated>2013-09-03T21:46:47.360Z</updated>
     <content type="application/xml">
          <attrs>
               <attr name="logonToken" type="string">MyServer:6400@{3&2=10209,U3&2v=MyServer:6400,UP&66=60,U3&68=secEnterprise:Administrator,UP&S9=12,U3&qe=100,U3&vz=yCFAVGKDkzcAIefcqibDE9m8WGhlPyIyPjzijsiChX8,UP}

               </attr>
          </attrs>
     </content>
</entry>

Add LogonToken

Take the Resulting logontoken and add it to the header as

X-SAP-LogonToken: "MyServer:6400@{3&2=10209,U3&2v=MyServer:6400,UP&66=60,U3&68=secEnterprise:Administrator,UP&S9=12,U3&qe=100,U3&vz=yCFAVGKDkzcAIefcqibDE9m8WGhlPyIyPjzijsiChX8,UP}"

Logoff Enterprise

Type of call: POST
URL: http://localhost:6405/biprws/logoff

Headers:
Accept: application/xml

X-SAP-LogonToken: "MyServer:6400@{3&2=10209,U3&2v=MyServer:6400,UP&66=60,U3&68=secEnterprise:Administrator,UP&S9=12,U3&qe=100,U3&vz=yCFAVGKDkzcAIefcqibDE9m8WGhlPyIyPjzijsiChX8,UP}" 

Payload:
Empty

Expected Response:

Empty

 

Retrieve an InfoObject


Type of Call: GET


URL: http://localhost:6405/biprws/infostore/5509

Headers:
Accept: application/xml
X-SAP-LogonToken: "MyServer:6400@{3&2=10209,U3&2v=MyServer:6400,UP&66=60,U3&68=secEnterprise:Administrator,UP&S9=12,U3&qe=100,U3&vz=yCFAVGKDkzcAIefcqibDE9m8WGhlPyIyPjzijsiChX8,UP}"

Payload:
None - it's a GET request

Expected Response: (This response will vary depending on the type of object.  The response here is for the World Sales Report (Crystal Report))
<entry>

<author>

<name>Administrator</name>

<uri>http://localhost:6405/biprws/infostore/12</uri>

</author>

<id>tag:sap.com,2010:bip-rs/ATtnRQbnvXpNhFVmgylzNwY</id>

<title type="text">World Sales Report</title>

<updated>2013-07-24T21:19:24.740Z</updated>

<link href="http://localhost:6405/biprws/infostore/5509/children" rel="http://www.sap.com/rws/bip#children" title="Children" />

<link href="http://localhost:6405/biprws/infostore/5407" rel="up" />

<link href="http://localhost:6405/biprws/infostore/5509/scheduleForms" rel="http://www.sap.com/rws/bip#schedule" title="Scheduling forms" />

<content type="application/xml">

<attrs>

<attr name="id" type="int32">5509</attr>

<attr name="cuid" type="string">ATtnRQbnvXpNhFVmgylzNwY</attr>

<attr name="description" type="string">Top 5 Countries' Sales with pie chart. Drill down on Country of interest to view Country's Top 5 Regional Sales with pie chart. Drill down on Region of interest to view Region's City Sales with pie chart. Drill down on City of interest to view City's Company Sales.</attr>

<attr name="name" type="string">World Sales Report</attr>

<attr name="type" type="string">CrystalReport</attr>

</attrs>

</content>

<link href="http://bipw08r2:8080/BOE/OpenDocument/opendoc/openDocument.jsp?sIDType=CUID&iDocID=ATtnRQbnvXpNhFVmgylzNwY" rel="http://www.sap.com/rws/bip#opendocument" title="OpenDocument" />

<link href="http://localhost:6405/biprws/infostore/7451" rel="http://www.sap.com/rws/bip#latest-instance" title="Latest instance" />

</entry>

 

Instructions

To run this sample on a BI 4.1 system:

  1. Stop Tomcat
  2. Copy XercesImpl.jar from C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\BOE\WEB-INF\eclipse\plugins\webpath.PlatformServices\web\WEB-INF\lib to C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\AdminTools\WEB-INF\lib
  3. Go to http://hc.apache.org/downloads.cgi and download one of the binary zip files (This sample was original coded with version 4.2.5).  Then extract and add the jar files in it to the folder C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\AdminTools\WEB-INF\lib
  4. Start Tomcat
  5. Browse to http://localhost:8080/AdminTools/YourApp.jsp

Notes:

Possible Issues

  • "org.apache.http.client.HttpResponseException: Unauthorized"
    This error can happen if you forget to include the "Accept" or "Content-Type" headers for your get / post requests
    It can also happen if you pass in the wrong Enterprise Password.
  • Notice that there is no CMS name variable - that is because it is assumed that you are connecting to the system who's restful services you are using

Source Code

Source code for Navigate Enterprise Folders

<%// Created by Shawn Penner 2013 %>

<% // These imports are for the Apache HttpComponents %>
<%@ page import = "org.apache.http.client.ResponseHandler"%>
<%@ page import = "org.apache.http.client.HttpClient"%>
<%@ page import = "org.apache.http.client.methods.HttpGet"%>
<%@ page import = "org.apache.http.client.methods.HttpPost"%>
<%@ page import = "org.apache.http.impl.client.BasicResponseHandler"%>
<%@ page import = "org.apache.http.impl.client.DefaultHttpClient"%>
<%@ page import = "org.apache.http.entity.StringEntity"%>

<% // These imports are for the XML DOM Parser %>
<%@ page import = "javax.xml.parsers.DocumentBuilderFactory"%>
<%@ page import = "javax.xml.parsers.DocumentBuilder"%>
<%@ page import = "org.w3c.dom.*"%>
<%@ page import = "org.w3c.dom.Node.*"%>
<%@ page import = "org.w3c.dom.Element"%>
<%@ page import = "com.sun.org.apache.xerces.internal.parsers.*"%>
<%@ page import = "org.xml.sax.InputSource"%>

<% // These imports are for transforming the DOM Parser back into a string %>
<%@ page import = "javax.xml.transform.Transformer"%>
<%@ page import = "javax.xml.transform.TransformerFactory"%>
<%@ page import = "javax.xml.transform.OutputKeys"%>
<%@ page import = "javax.xml.transform.dom.DOMSource"%>
<%@ page import = "javax.xml.transform.stream.StreamResult"%>

<% // Imports for URL Encoding %>
<%@ page import = "org.apache.http.client.entity.UrlEncodedFormEntity"%>
<%@ page import = "org.apache.http.client.utils.URLEncodedUtils"%>
<%@ page import = "org.apache.http.message.BasicNameValuePair"%>
<%@ page import = "org.apache.http.NameValuePair"%>

<% // Generic Java Imports %>
<%@ page import = "java.io.*"%>
<%@ page import = "java.util.List"%>
<%@ page import = "java.util.ArrayList"%>

<%
//
// Instructions:
// 1. Stop Tomcat
// 2. Copy XercesImpl.jar from C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\BOE\WEB-INF\eclipse\plugins\webpath.PlatformServices\web\WEB-INF\lib to C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\AdminTools\WEB-INF\lib
// 3. Go to http://hc.apache.org/downloads.cgi and download one of the binary zip files (This sample was original coded with version 4.2.5).  Then extract and add the jar files in it to the folder C:\Program Files (x86)\SAP BusinessObjects\tomcat\webapps\AdminTools\WEB-INF\lib
// 4. Start Tomcat
// 5. Browser to http://localhost:8080/AdminTools/testRest.jsp
//
// Info:
// The Apache Http Components come from here: http://hc.apache.org/httpclient-3.x/
// You can find javadocs on them here: http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/index.html
// Another good javadocs: http://xerces.apache.org/xerces2-j/javadocs/api/org/w3c/dom/Element.html
//
// XercesImpl is used for the DOM XML parser.  For larger XML documents - you may prefer to use a SAX implementation instead.
//
// Possible Issues:
//
//"org.apache.http.client.HttpResponseException: Unauthorized"
// This error can happen if you forget to include the "Accept" or "Content-Type" headers for your get / post requests
// It can also happen if you pass in the wrong Enterprise Password.
//
// Notice that there is no CMS name variable - that is because it is assumed that you are connecting to the system who's restful services you are using

String redirectURL = "testRest.jsp";

// Enterprise Authentication Credentials
String boUsername = "Administrator";
String boPassword = "Password1";
String boAuthType = "secEnterprise";

// Restful URL's
final String baseURL = "http://BIPW08R2:6405/biprws";
final String logonURL = baseURL + "/logon/long";
final String logoffURL = baseURL + "/logoff";
String infoStoreQueryURL = baseURL + "/infostore";

// Test Variables
int logonMethod = 1;  // 1 = Retrieve XML, populate it, send it 2 = Assume Structure of XML, send it

try {

String xmlString = "";
String logonToken = "";

// First check to see if we have a logonToken stored in session.  If so, use it.
if (session.getAttribute("logonToken") != null) {
  logonToken = (String)session.getAttribute("logonToken");
 
  // Now check if a Query URL was passed in the query string.  If so - use it.
  if (request.getParameter("queryURL") != null) {
   infoStoreQueryURL = request.getParameter("queryURL");
  }
} else {

  // No logontoken detected - so create one using the desired logon method.

  // Logon Method 1
  // Retrieve the XML, populate it, and send it
  if (logonMethod == 1) {

   out.println("Using Logon Method 1 - Retrieve the XML, Populate it, Send it </br>");
   out.println("Sending Get request to retrieve logon XML </br>");
   xmlString = restGet(logonURL,"","","",""); // // Get the XML string for logging on.
   out.println("XML Received - populating with logon Information </br>");
   xmlString = addLogonInfoToXML(xmlString, boUsername, boPassword, boAuthType);
   out.println("Logon Information Added - Posting it to get logonToken </br>");
   String logonXML = restPost(logonURL, xmlString, "","","","");
   out.println("Logged on Succesfully - extracting logonToken </br>");
   logonToken = getLogonTokenFromXML(logonXML);
   out.println("LogonToken Retrieved: " + logonToken + "</br></br>");

  // Logon Method 2
  // Assume that you already know the XML structure, populate it, and send it
  } else if (logonMethod == 2) {
   out.println("Using Logon Method 2 - Create pre-made XML for logon, Send it </br>");
   xmlString = "<attrs><attr name=\"userName\" type=\"string\" >" + boUsername + "</attr><attr name=\"password\" type=\"string\" >" + boPassword + "</attr><attr name=\"auth\" type=\"string\" possibilities=\"secEnterprise,secLDAP,secWinAD,secSAPR3\">" + boAuthType + "</attr></attrs>";
   out.println("XML String Created - Posting it to get logonToken </br>");
   String logonXML = restPost(logonURL, xmlString, "","","","");
   out.println("Logged on Succesfully - extracting logonToken </br>");
   logonToken = getLogonTokenFromXML(logonXML);
   out.println("LogonToken Retrieved: " + logonToken + "</br></br>");
  }
  // Now that we have a logonToken - it must be included in the header for a future RestFul calls
  session.setAttribute("logonToken", logonToken);
}

// If the logout link was clicked
if (infoStoreQueryURL.equals("logout")) {
  xmlString = restPost(logoffURL, "", "X-SAP-LogonToken",logonToken,"","");
  session.removeAttribute("logonToken");
  out.println("Logged out.  Click " + buildEncodedURLString(redirectURL, baseURL + "/infostore", "here") + " to log back in. </br>");
} else {
  // Submit the InfoStore Query
  xmlString = restGet(infoStoreQueryURL, "X-SAP-LogonToken",logonToken,"","");
 
  // Parse it
  out.println(parseInfoStoreQuery(baseURL, xmlString, redirectURL, logonToken));
 
  // For debug purposes
  //printXML(out, xmlString);
}

} catch (IOException eIO) {
    out.println("IO Exception: " + eIO);
} catch (Exception ex) {
out.println("Exception: " + ex);
}

%>


<%!

// This is for debugging purposes.  The xmp tag tells the browser you want to see the XML on screen
public void printXML(JspWriter out, String msg) throws Exception {
out.println("<xmp>" + msg.replaceAll("><", "></xmp><xmp><") + "</xmp>");
}

public Document convertStringToDom(String domXMLSTring) throws Exception {
DOMParser parser = new DOMParser();
parser.parse(new InputSource(new java.io.StringReader(domXMLSTring)));
return (parser.getDocument());
}

public String convertDomToString(Document doc) throws Exception {
// Now convert the document back to a string
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
String output = writer.getBuffer().toString().replaceAll("\n|\r", "");
return(output);
}

public String buildEncodedURLString(String basePage, String url, String urlTitle) throws Exception {
String returnURL = "";

if (url.equals("")) {
  returnURL = urlTitle;
} else {
  List< NameValuePair > qparams = new ArrayList< NameValuePair >();
  qparams.add( new BasicNameValuePair( "queryURL", url ) );
  returnURL = "<a href=\"" + basePage + "?" + URLEncodedUtils.format(qparams, "UTF-8") + "\">" + urlTitle + "</a>";
}
return(returnURL);
}

// Take a infostore query and extract the InfoObjects, their associated URL's, and the navigation URL's as well.
// The parameter baseURL is only used to generate the "Home" link on the nav bar and the token url is used to generate
// the opendocument links
public String parseInfoStoreQuery(String baseURL, String xmlString, String basePage, String logonToken) throws Exception {

String navFirstPage = "";
String navLastPage = "";
String navCurrentPage = "";
String navNextPage = "";
String navPrevPage = "";
String navParent = "";
String infoObjectContent = "";

// First turn it into a DOM document
Document doc = convertStringToDom(xmlString);

// Extract the navigation links at the top.
NodeList navNodes = doc.getElementsByTagName("link");

for (int i = 0; i < navNodes.getLength(); i++) {
  Element element = (Element) navNodes.item(i);
 
  // Is this the correct XML token
  if (element.getAttribute("rel").equals("self")) {
   navCurrentPage = element.getAttribute("href");
  }
  // Is this the correct XML token
  if (element.getAttribute("rel").equals("first")) {
   navFirstPage = element.getAttribute("href");
  }
 
  // Is this the correct XML token
  if (element.getAttribute("rel").equals("next")) {
   navNextPage = element.getAttribute("href");
  }
 
  // Is this the correct XML token
  if (element.getAttribute("rel").equals("previous")) {
   navPrevPage = element.getAttribute("href");
  }
  // Is this the correct XML token
  if (element.getAttribute("rel").equals("last")) {
   navLastPage = element.getAttribute("href");
  }
 
  // This parent nav link will only be available if you retrieve an individual object.  If you get a list of objects in a folder (children)
  // the parent link will not be available.
  if (element.getAttribute("rel").equals("up")) {
   navParent = element.getAttribute("href");
  }
}

// Now build the Nav Bar
infoObjectContent = infoObjectContent + buildEncodedURLString(basePage, baseURL + "/infostore", "Home") + " " + buildEncodedURLString(basePage, navFirstPage, "First Page") + " " + buildEncodedURLString(basePage, navPrevPage, "Previous Page") + " " + buildEncodedURLString(basePage, navNextPage, "Next Page") + " " + buildEncodedURLString(basePage, navLastPage, "Last Page");

// If we are viewing a single object add the parent link
if (!navParent.equals("")) {
  infoObjectContent = infoObjectContent + " " +  buildEncodedURLString(basePage, navParent +  "/children", "Parent");
} else {
  // otherwise let's get the individual object so we can add the parent link
 
  // First need to parse the self link (which should always exist) to extract the query for the individual item that we are looking at.

  String getCurrentObject = navCurrentPage.substring(0,navCurrentPage.indexOf("children")-1);
  String tmpString2 = restGet(getCurrentObject, "X-SAP-LogonToken",logonToken,"","");
  
  // Turn it into a DOM document
  Document doc3 = convertStringToDom(tmpString2);
  
  NodeList upNavNodes = doc3.getElementsByTagName("link");
  for (int k = 0; k < upNavNodes.getLength(); k++) {
   Element upElement = (Element) upNavNodes.item(k);
   // Find the up link
   if (upElement.getAttribute("rel").equals("up")) {
  
    // If this is the parent query - we can't use the children tag or it will fail.
    // We will know that the parent is the root folder if we see "infostore" as the end of the string


    String upURL = upElement.getAttribute("href");
  
    if ((upURL.indexOf("infostore") + 9) == upURL.length()) {
     infoObjectContent = infoObjectContent + " " +  buildEncodedURLString(basePage, upElement.getAttribute("href"), "Parent");
    } else {
     infoObjectContent = infoObjectContent + " " +  buildEncodedURLString(basePage, upElement.getAttribute("href") + "/children", "Parent");
    }
   }
  }
}
infoObjectContent = infoObjectContent + "     " + buildEncodedURLString(basePage, "logout", "Logout") + "</br></br>";

// Now Loop through all the InfoObjects that were returned
NodeList infoNodes = doc.getElementsByTagName("entry");
for (int j = 0; j < infoNodes.getLength(); j++) {
  Element element = (Element) infoNodes.item(j);
  Element tmpElement;
 
  // Get the SI_NAME of the object
  tmpElement = (Element)element.getElementsByTagName("title").item(0);
  String infoName = (String)tmpElement.getTextContent();
 
  // First get the Nav URL for this object
  tmpElement = (Element)element.getElementsByTagName("link").item(0);
  String infoNavLink = (String)tmpElement.getAttribute("href");
 
  // Now get the content section of this object
  Element contElement = (Element) element.getElementsByTagName("content").item(0);
 
  // And extract the type
  String infoType = "";
  NodeList contNodes = contElement.getElementsByTagName("attr");
  for (int k = 0; k < contNodes.getLength(); k++) {
   Element infoContElement = (Element) contNodes.item(k);
   if (infoContElement.getAttribute("name").equals("type")) {
    infoType = infoContElement.getTextContent();
   }
  }
 
  // If this is a folder - we want to get the children of the folder - not the folder object.
  if (infoType.equals("Folder")) {
   infoNavLink = infoNavLink + "/children";
  
   // We now have the Name, Type, and NavLink, so build a navURL and add it to the infoObjectContent string
   infoObjectContent = infoObjectContent + buildEncodedURLString(basePage, infoNavLink, infoName) + " <b>Type:</b> " + infoType + "</br>";
  } else if (infoType.equals("CrystalReport") || infoType.equals("Webi")) {
 
   // Since this is a crystal report - lets use the opendocument link
   // Because the children query doesn't give us the opendocument link - we need to retrieve the actual object.
  
   String tmpString = restGet(infoNavLink, "X-SAP-LogonToken",logonToken,"","");
  
   // Turn it into a DOM document
   Document doc2 = convertStringToDom(tmpString);
  
   NodeList itemNodes = doc2.getElementsByTagName("entry");
   Element itemElement =(Element) itemNodes.item(0);
  
   NodeList crystalNodes = itemElement.getElementsByTagName("link");
   for (int k = 0; k < crystalNodes.getLength(); k++) {
    Element crystalElement = (Element) crystalNodes.item(k);
    // Find the opendocument link
    if (crystalElement.getAttribute("title").equals("OpenDocument")) {
     List< NameValuePair > qparams = new ArrayList< NameValuePair >();
     qparams.add( new BasicNameValuePair( "token", logonToken));
     infoObjectContent = infoObjectContent + "<a href=\"" + crystalElement.getAttribute("href") + "&" + URLEncodedUtils.format(qparams, "UTF-8") + "\" target=\"_blank\">" + infoName + "</a>  <b>Type:</b> " + infoType + "</br>";
    }
   }
  
  } else {
   // We now have the Name, Type, and NavLink, so build a navURL and add it to the infoObjectContent string
   //infoObjectContent = infoObjectContent + buildEncodedURLString(basePage, infoNavLink, infoName) + " <b>Type:</b> " + infoType + "</br>";
   infoObjectContent = infoObjectContent + infoName + " <b>Type:</b> " + infoType + "</br>";
  }
 
 
}
return (infoObjectContent);
}


// Takes the returned XML string from the logon URL and adds logon info to it
public String addLogonInfoToXML(String xmlString, String username, String password, String authType ) throws Exception {
DOMParser parser = new DOMParser();
parser.parse(new InputSource(new java.io.StringReader(xmlString)));
Document doc = parser.getDocument();
NodeList nodes = doc.getElementsByTagName("attr");

for (int i = 0; i < nodes.getLength(); i++) {
  Element element = (Element) nodes.item(i);
 
  // Is this the correct XML token
  if (element.getAttribute("name").equals("userName")) {
   element.setTextContent(username);
  }
  // Is this the correct XML token
  if (element.getAttribute("name").equals("password")) {
   element.setTextContent(password);
  }
  // Is this the correct XML token
  if (element.getAttribute("name").equals("auth")) {
   element.setTextContent(authType);
  }
}
return (convertDomToString(doc));
}

public String getLogonTokenFromXML(String xmlString) throws Exception {
DOMParser parser = new DOMParser();
parser.parse(new InputSource(new java.io.StringReader(xmlString)));
Document doc = parser.getDocument();
NodeList nodes = doc.getElementsByTagName("attr");

for (int i = 0; i < nodes.getLength(); i++) {
  Element element = (Element) nodes.item(i);
 
  // Is this the correct XML token
  if (element.getAttribute("name").equals("logonToken")) {
   return(element.getTextContent());
  }
}
return("");
}

public static String restGet(String urlStr, String param1Name, String param1Value, String param2Name, String param2Value ) throws Exception {

HttpClient httpclient = new DefaultHttpClient();
try {
  HttpGet httpget = new HttpGet(urlStr);
  httpget.addHeader("Accept", "application/xml");
  httpget.addHeader("Content-Type", "application/xml");
 
  if (!param1Name.equals("")) {
   httpget.addHeader(param1Name, param1Value);
  }
  if (!param2Name.equals("")) {
   httpget.addHeader(param2Name, param2Value);
  }
  
  ResponseHandler<String> responseHandler = new BasicResponseHandler();
  String responseBody = httpclient.execute(httpget, responseHandler);
  return(responseBody);
} finally {
  // When HttpClient instance is no longer needed,
        // shut down the connection manager to ensure
        // immediate deallocation of all system resources
        httpclient.getConnectionManager().shutdown();
}
}

// Allow for two parameters to be passed to the post request along with the XML string.
// The most common one will be the "X-SAP-LogonToken" parameter
public static String restPost(String urlStr, String XMLString, String param1Name, String param1Value, String param2Name, String param2Value ) throws Exception {

HttpClient httpclient = new DefaultHttpClient();
try {
  HttpPost httpPost = new HttpPost(urlStr);
  httpPost.addHeader("Accept", "application/xml");
  httpPost.addHeader("Content-Type", "application/xml");
 
  if (!param1Name.equals("")) {
   httpPost.addHeader(param1Name, param1Value);
  }
  if (!param2Name.equals("")) {
   httpPost.addHeader(param2Name, param2Value);
  }
 
  httpPost.setEntity(new StringEntity(XMLString));
  
  ResponseHandler<String> responseHandler = new BasicResponseHandler();
  String responseBody = httpclient.execute(httpPost, responseHandler);
  return(responseBody);
} finally {
  // When HttpClient instance is no longer needed,
        // shut down the connection manager to ensure
        // immediate deallocation of all system resources
        httpclient.getConnectionManager().shutdown();
}
}


%>

Related Content

Related Documents

RESTful APIs Wiki