You might have watched the first DevTalks episode about Identity Management on SAP NetWeaver Cloud.
Soon you'll get the next episode. It'll be around the Document Service. Until that happens I'd like to share with you some experience I've made and at the end also share some code with you to, hopefully, make it easier for you to use the document service in SAP NetWeaver Cloud.
An OASIS In The Document Service?
Essentially the Document Service of SAP NetWeaver Cloud is a Content Management System based on the OASIS standard CMIS. SAP is part of the CMIS working group at OASIS and the colleague I'll be talking to in the DevTalks interview will be Florina Mueller, who's currently working in the working group to finalize version 1.1 of CMIS.
Having this open standard for our Document Service allows you to use other CMIS compliant tools, too, to access your documents stored in the cloud. This gives you the choice which tools you'd like to use to access your data stored in the cloud. And there are a lot of CMIS compliaant products out there for all kind of different devices that you can simply re-use to access your valuable data in your SAP NetWeaver Cloud account.
First Contact
As always I started on the developer guide for SAP NetWeaver Cloud and looked for the Document Service documentation. I know it's not always the best approach, but I just skipped the intro and started directly with the section about "Using the Document Service in a Web Application" because that's what I wanted to do.
The Borg - Resistance Is Futile?
When looking into the sample code I was a little bit scared. It really looked complex and I felt that a few highly sophisticated Borg drones developed it.
But when looking into it again you can see that the colleagues tried to provide a pretty comprehensive example in these few lines of code.
Not only does it show how you can create your own repository for your documents. The code shows you also how to create folders and documents in these folders and how to retrieve all that information back to your application.
The MyDocs Example
To give people an easier access to the document service I've started encapsulating the functionality into a helper class to help create an application that can up- and download documents to and from the document service of SAP NetWeaver Cloud. So you'll be able to store your own files in the cloud with SAP NetWeaver Cloud.
Please be aware that the code I'm putting here might be buggy and might not work on your system although I did my best to make it stable. So please use it at your own risk and don't blame me if it didn't work Image may be NSFW.
Clik here to view..
Oh. And by the way: I'm still building the app and it's not ready, yet. So you might feel some comments are missing in the code. In case you have that feeling you might be right Image may be NSFW.
Clik here to view.Once I finalize the app and fixed all issues I'll post it on the SAP NetWeaver Cloud Github page.
Of course I've simplified the use case a lot. You only use one repository and one folder where you can dump your files. For some this might be a restriction, but if you see it from the perspective of trying to make use of the Document Service as quickly as possible you can start small to find out if you can make good use of the service. Once you want more you can think of other use cases and let the application grow with your needs.
But now, let's get started! For those of you who'd like to watch a video in parallel how to create this app in the Eclipse environment - here it is:
The CMIS Helper Class
This class encapsulates the low level functionality of the application. If you want to copy-and-paste it into your own application don't forget to adapt the package name at the top.
This helper class provides you with 5 public methods that you can use:
- addDocument
- getDocumentsList
- deleteDocument
- uploadDocument
- streamOutDocument
These methods will help you later on in your servlet to add, delete and download files from your own document repository in SAP NetWeaver Cloud. To make it easier in this example I've re-used two libraries. Please download these two libraries and copy the corresponding jar files into the WebContent/WEB-INF/lib directory of your "Dynamic Web Project" in Eclipse.
There are newer versions available for these two libraries, but these are the ones that I've used for this example and which worked.
And here comes the actual code. Just copy and paste it into the src directory of the Java Resources of your Dynamic Web Project. Ideally the name of the class should be CmisHelper under the package com.sap.cebit2013.sapnwcloud.doc.helper.
package com.sap.cebit2013.sapnwcloud.doc.helper; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URLConnection; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.naming.InitialContext; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.chemistry.opencmis.client.api.CmisObject; import org.apache.chemistry.opencmis.client.api.Document; import org.apache.chemistry.opencmis.client.api.Folder; import org.apache.chemistry.opencmis.client.api.ItemIterable; import org.apache.chemistry.opencmis.client.api.Property; import org.apache.chemistry.opencmis.client.api.Session; import org.apache.chemistry.opencmis.commons.PropertyIds; import org.apache.chemistry.opencmis.commons.data.ContentStream; import org.apache.chemistry.opencmis.commons.enums.VersioningState; import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException; import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException; import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import com.sap.ecm.api.EcmService; import com.sap.ecm.api.RepositoryOptions; import com.sap.ecm.api.RepositoryOptions.Visibility; public class CmisHelper { public Session cmisSession = null; // The folder name for the repository of all documents of your application static private String REPO_APPFOLDER = "MyDocuments"; // This is the unique name of that repository static private String REPO_NAME = "com.sap.cebit2013.sapnwcloud.docs"; // This is the secret key that your application uses to access the repository. static private String REPO_PRIVATEKEY = "thisIsThSsecretKeyForYourApp0"; // Maximum size of a file (in Bytes) that you can upload at a time to your repository static private int MAX_FILE_SIZE = 10000000; // The prefix for the download URL of a file static private String SERVLET_DOWNLOAD_PREFIX = "?action=getfile&docid="; // The directory to store the uploaded files temporarily static private String TEMP_UPLOAD_DIR = "uploads"; /** * @param file * the file to be imported e.g. /Users/me/Documents/myfile.txt */ public void addDocument(File file) { // create a new file in the root folder Map<String, Object> properties = new HashMap<String, Object>(); properties.put(PropertyIds.OBJECT_TYPE_ID, "cmis:document"); properties.put(PropertyIds.NAME, file.getName()); // Try to identify the mime type of the file String mimeType = URLConnection.guessContentTypeFromName(file.getName()); if (mimeType == null || mimeType.length() == 0) { mimeType = "application/octet-stream"; } byte[] content = readFile(file); if (content != null) { InputStream stream = new ByteArrayInputStream(content); Session session = getRepositorySession(); ContentStream contentStream = session.getObjectFactory().createContentStream(file.getName(), content.length, mimeType, stream); try { Folder yourFolder = getFolderOfYourApp(REPO_APPFOLDER); yourFolder.createDocument(properties, contentStream, VersioningState.NONE); } catch (CmisNameConstraintViolationException e) { // Document exists already, nothing to do } } } /** * * @return a list of all documents that are in the folder of your repository */ public List<MyDocsDTO> getDocumentsList() { // Get the repository folder of your app @SuppressWarnings("unused") Session session = getRepositorySession(); Folder yourFolder = getFolderOfYourApp(REPO_APPFOLDER); ItemIterable<CmisObject> children = yourFolder.getChildren(); List<Document> docs = new ArrayList<Document>(); for (CmisObject o : children) { if (o instanceof Folder) { // Do nothing if the loop found a subfolder } else { Document doc = (Document) o; docs.add(doc); } } if (docs != null && docs.size() > 0) { return convertCmisDocsToMyDocsDTOs(docs); } else { return null; } } /** * This method deletes a file via it's document id * * @param documentId * the document id of the file in the CMIS system */ public void deleteDocument(String documentId) { // Get the session Session session = getRepositorySession(); if (session == null) { throw new IllegalArgumentException("Session must be set, but is null!"); } Document doc = getDocumentById(documentId); doc.delete(true); } /** * * @param documentId * the document id of the file in the CMIS system * @return The document as a CMIS Document object */ private Document getDocumentById(String documentId) { Session session = getRepositorySession(); if (session == null) { throw new IllegalArgumentException("Session must be set, but is null!"); } if (documentId == null) { return null; } try { Document doc = (Document) session.getObject(documentId); return doc; } catch (CmisObjectNotFoundException onfe) { return null; // throw new Exception("Document doesn't exist!", onfe); } catch (CmisBaseException cbe) { // throw new Exception("Could not retrieve the document:" + // cbe.getMessage(), cbe); return null; } } /** * @param documentId * the document id of the file in the CMIS system * @return the content of the file */ private ContentStream getDocumentStreamById(String documentId) { ContentStream contentStream = null; Document doc = getDocumentById(documentId); if (doc != null) { contentStream = doc.getContentStream(); } return contentStream; } /** * Initializes the document service repository * @return the corresponding CMIS session */ private Session getRepositorySession() { try { try { // Only connect to the repository if a session hasn't been opened, yet if (cmisSession == null) { InitialContext ctx = new InitialContext(); String lookupName = "java:comp/env/" + "EcmService"; EcmService ecmSvc = (EcmService) ctx.lookup(lookupName); cmisSession = ecmSvc.connect(REPO_NAME, REPO_PRIVATEKEY); } else { return cmisSession; } } catch (CmisObjectNotFoundException e) { // repository does not exist, so try to create it RepositoryOptions options = new RepositoryOptions(); options.setUniqueName(REPO_NAME); options.setRepositoryKey(REPO_PRIVATEKEY); options.setVisibility(Visibility.PROTECTED); InitialContext ctx = new InitialContext(); String lookupName = "java:comp/env/" + "EcmService"; EcmService ecmSvc = (EcmService) ctx.lookup(lookupName); ecmSvc.createRepository(options); // should be created now, so connect to it cmisSession = ecmSvc.connect(REPO_NAME, REPO_PRIVATEKEY); } // String id = openCmisSession.getRepositoryInfo().getId(); return cmisSession; } catch (Exception e) { System.out.println("Exception found when initializing the database. Error message:\n" + e.getMessage()); return null; } } /** * Creates a folder. If the folder already exists nothing is done * * @param folderName * the name of the root folder in your app * @return returns the Folder object of your root folder */ private Folder getFolderOfYourApp(String folderName) { Session session = getRepositorySession(); Folder rootFolder = session.getRootFolder(); // create a new folder Map<String, String> newFolderProps = new HashMap<String, String>(); newFolderProps.put(PropertyIds.OBJECT_TYPE_ID, "cmis:folder"); newFolderProps.put(PropertyIds.NAME, folderName); try { rootFolder = rootFolder.createFolder(newFolderProps); } catch (CmisNameConstraintViolationException e) { // Folder exists already, nothing to do } return rootFolder; } /** * * @param file * the file you want to read-in * @return the byte stream of the file */ private byte[] readFile(File file) { if (file.length() > MAX_FILE_SIZE) { System.out.println("File is too big (exceeds size limit of " + MAX_FILE_SIZE + ")"); return null; } byte[] buffer = new byte[(int) file.length()]; try { InputStream ios = null; try { ios = new FileInputStream(file); if (ios.read(buffer) == -1) { throw new IOException("EOF reached while trying to read the whole file"); } } finally { try { if (ios != null) ios.close(); } catch (IOException e) { } } } catch (Exception e) { System.out.println("Exception found when reading the file '" + file + "'. Error message:\n" + e.getMessage()); return null; } return buffer; } /** * * @param docs * the CMIS documents that you want to convert * @return the representation of the documents as a List of MyDocsDTO */ private List<MyDocsDTO> convertCmisDocsToMyDocsDTOs(List<Document> docs) { List<MyDocsDTO> result = null; if (docs != null) { result = new ArrayList<MyDocsDTO>(); for (int i = 0; i < docs.size(); i++) { MyDocsDTO pc = convertCmisDocToMyDocsDTO(docs.get(i)); result.add(pc); } } return result; } /** * * @param doc * the CMIS document that you want to convert * @return the MyDocsDTO representation of the document */ private MyDocsDTO convertCmisDocToMyDocsDTO(Document doc) { MyDocsDTO result = null; if (doc != null) { result = new MyDocsDTO(); // System.out.println("Properties of file" + doc.getName()); List<Property<?>> properties = doc.getProperties(); // This loop will go through all properties of the file // and we use it to assign specific properties to our MyDocsDTO representation for (Property<?> p : properties) { String type = p.getDefinition().getDisplayName(); if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:creationDate")) { try { DateFormat formatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); Date date = (Date) formatter.parse(p.getValueAsString()); result.setCreationDate(date); } catch (Exception e) { e.getMessage(); } } if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:name")) { result.setFilename(p.getValueAsString()); } if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:contentStreamMimeType")) { result.setMimeType(p.getValueAsString()); } if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:createdBy")) { result.setAuthorName(p.getValueAsString()); } if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:objectId")) { result.setId(p.getValueAsString()); result.setDownloadLink(SERVLET_DOWNLOAD_PREFIX + p.getValueAsString()); } if (type != null && type.length() > 0 && type.equalsIgnoreCase("cmis:contentStreamLength")) { String value = p.getValueAsString(); result.setFileLength(Long.parseLong(value)); } } } return result; } /** * Enables the upload of a file from the client computer to the server * * @param realPathOfApp * The physical path of the application on the server * @param request * The HttpServletRquest * @return A file object is stored on the server in the TEMP_UPLOAD_DR */ public File uploadDocument(String realPathOfApp, HttpServletRequest request) { File uploadedFile = null; boolean isMultipart = ServletFileUpload.isMultipartContent(request); String uploadPath = realPathOfApp + File.separator + TEMP_UPLOAD_DIR; File path = new File(uploadPath); // If the path doesn't exist, yet, create it if (!path.exists()) { path.mkdir(); } if (isMultipart) { FileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); try { List<?> items = upload.parseRequest(request); Iterator<?> iterator = items.iterator(); while (iterator.hasNext()) { FileItem item = (FileItem) iterator.next(); if (!item.isFormField()) { String fileName = item.getName(); uploadedFile = new File(path + File.separator + fileName); item.write(uploadedFile); } } } catch (FileUploadException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } return uploadedFile; } /** * * @param response * The HttpServletResponse * @param docId * The document id of the document in your repository */ public void streamOutDocument(HttpServletResponse response, String docId) { try { Document doc = getDocumentById(docId); response.setHeader("Content-Disposition", "attachment; filename=\"" + doc.getName() + "\";"); response.setContentType("application/x-unknown"); ContentStream fileStream = getDocumentStreamById(docId); BufferedInputStream bufStream = new BufferedInputStream(fileStream.getStream()); ServletOutputStream responseOutputStream = response.getOutputStream(); int data = bufStream.read(); while (data != -1) { responseOutputStream.write(data); data = bufStream.read(); } bufStream.close(); responseOutputStream.close(); } catch (Exception e) { e.getMessage(); } } }
The MyDocsDTO Class
In the same package you copy-and-paste this DTO class that I use to access a reduced version of all the properties that the Document class of CMIS provides.
package com.sap.cebit2013.sapnwcloud.doc.helper; import java.util.Date; public class MyDocsDTO { public String authorName; public String authorEmail; public String id; public long fileLength; public Date creationDate; public String filename; public String downloadLink; public String stream; public String mimeType; public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } public String getAuthorName() { return authorName; } public void setAuthorName(String authorName) { this.authorName = authorName; } public String getAuthorEmail() { return authorEmail; } public void setAuthorEmail(String authorEmail) { this.authorEmail = authorEmail; } public String getId() { return id; } public void setId(String id) { this.id = id; } public long getFileLength() { return fileLength; } public void setFileLength(long fileLength) { this.fileLength = fileLength; } public Date getCreationDate() { return creationDate; } public void setCreationDate(Date creationDate) { this.creationDate = creationDate; } public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } public String getDownloadLink() { return downloadLink; } public void setDownloadLink(String downloadLink) { this.downloadLink = downloadLink; } public String getStream() { return stream; } public void setStream(String stream) { this.stream = stream; } }
The HelloWorld Servlet
So let's make use of the helper class with a servlet that can add files and show a list of all files in your document repository. This is the servlet you can use. Just copy and paste it into a different package called com.sap.cebit2013.sapnwcloud.doc.servlets and name this servlet HelloWorld.
package com.sap.cebit2013.sapnwcloud.doc.servlets; import java.io.File; import java.io.IOException; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.sap.cebit2013.sapnwcloud.doc.helper.CmisHelper; import com.sap.cebit2013.sapnwcloud.doc.helper.MyDocsDTO; /** * Servlet implementation class HelloWorld */ public class HelloWorld extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public HelloWorld() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Read-in the action parameter String action = request.getParameter("action"); // Show the list of all files if (action == null || action.equalsIgnoreCase("list")) { response.getWriter().println("<html><head><title>SAP NW Cloud MyDocs App</title></head><body>"); CmisHelper cmisHelper = retrieveCmisHelperClass(request); List<MyDocsDTO> docs = cmisHelper.getDocumentsList(); response.getWriter().println(buildHtmlPageForShowingAllDocuments(docs)); response.getWriter().println("</body></html>"); } // If the user wants to get the file out of the repository... if (action != null && action.equalsIgnoreCase("getfile")) { String docId = request.getParameter("docid"); CmisHelper cmisHelper = retrieveCmisHelperClass(request); cmisHelper.streamOutDocument(response, docId); } } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String realPathOfApp = getServletContext().getRealPath(""); CmisHelper cmis = new CmisHelper(); File file = cmis.uploadDocument(realPathOfApp, request); cmis.addDocument(file); // Delete the file as it is in the temporary folder on your server file.delete(); // Print out the list of all files // After having add the file above that file should now appear in the list response.getWriter().println("<html><head><title>SAP NW Cloud MyDocs App</title></head><body>"); CmisHelper cmisHelper = retrieveCmisHelperClass(request); List<MyDocsDTO> docs = cmisHelper.getDocumentsList(); response.getWriter().println(buildHtmlPageForShowingAllDocuments(docs)); response.getWriter().println("</body></html>"); } /** * * @param docs * @return HTML string with the field for the file upload and the lkist of all files */ private String buildHtmlPageForShowingAllDocuments(List<MyDocsDTO> docs) { String result = ""; // Print the "Upload File" field prior to the list of files, so that the end user can upload files result += "<fieldset><legend>Upload File</legend><form action='HelloWorld'"; result += "method='post' enctype='multipart/form-data'>"; result += "<label for='filename'>File: </label><input id='filename' type='file' name='filename' size='50'/><br/>"; result += "<input type='submit' value='Upload File'/></form></fieldset>"; // And now print the list of files result += "<h1>List of all files</h1>"; result += "<ul>"; if (docs != null && docs.size() > 0) { for (int i = 0; i < docs.size(); i++) { MyDocsDTO doc = docs.get(i); String row = "<li>" + "<a href=" + '"' + doc.getDownloadLink() + '"' + ">" + doc.getFilename() + "<a> (" + doc.getFileLength() + " bytes)</li>"; result += row; } } result += "</ul>"; return result; } /** * This method ensures that an existing CmisHelper class that was saved in the HttpServletRequest is re-used whenever * possible instead of creating new instances of CmisHelper classes everytime you call the doGet or doPost from the * method * * @param request * The HttpServletRequest * @return The CmisHelper class */ private CmisHelper retrieveCmisHelperClass(HttpServletRequest request) { CmisHelper result = null; if (request != null) { HttpSession httpSession = request.getSession(); if (httpSession != null) { CmisHelper cmisHelperHttpSession = (CmisHelper) httpSession.getAttribute("myCmisHelper"); // If an instance of CmisHelper is already there, use it if (cmisHelperHttpSession != null) { result = cmisHelperHttpSession; } // If there isn't one, create a new one and store it in the session of the HttpServletRequest so that it // can be re-used the next time else { result = new CmisHelper(); httpSession.setAttribute("myCmisHelper", result); } } } return result; } }
The web.xml
Under your WebContent/WEB-INF directory you'll find the web.xml file of your application. Please add the xml snippet below into it. Ideally just before the closing web-app tag.
<resource-ref> <res-ref-name>EcmService</res-ref-name> <res-type>com.sap.ecm.api.EcmService</res-type> </resource-ref></web-app>
Publish on your own SAP NetWeaver Cloud account
Now that the code is there you add this project either to your local SAP NetWeaver Cloud server or you deploy it e.g. to your SAP NetWeaver Cloud trial account. In case you want to run it on your localhost first, you'll have to follow the instructions on the top of official documentation page "Using the Document Service in a Web Application" and install the MongoDB database on your machine (section "Infrastructure Prerequisites".
If you've sticked to my propals regardign the names and if you deployed the app to your localhost you'll find your application under http://localhost:8080/cebit2013/HelloWorld
Image may be NSFW.
Clik here to view.
If you click on the Browse... button a window will open up where you can select the file from your computer that you'd like to upload. Once selected click on "Upload File" and your file will show up in the list of files that are already there.
What Comes Next?
Now you can start playing around with the UI. Maybe you feel challenged and would like to implement a means to delete a file? Go ahead.
Or instead of streaming out an html page, why not creating a jQuery mobile output for your mobile device so that you can access your documents you've stored on your SAP NetWeaver Cloud trial account wherever you are?
The upcoming DevTalk Interview
I hope you liked to play with the document service a bit. I'll be posting the next DevTalk interview with Florian Mueller next week.
Maybe until then you already got some questions that you'd like to ask. Write them down and add them as a comment to the blog that will come along with the video. Looking forward to it.
Best,
Rui