MI Local Application Deployer

 

This article describes a way how to temporarily deploy an MI component WITHOUT the need for a running MI WebConsole. This would enable you to check on your application-under-work on its very first stage of realization - how your UI looks like, if your front controllers events are properly called and so on. Another cool thing is that you can use this tool also to deploy your application on your PDA which doesn't have any network interface card.

Introduction

The Mobile Development Kit (MDK) offers several functionalities that help MI application developers easen their tasks. One thing that is not supported however by MDK is the temporary deployment of the application-under-work into the installed MI client. Mobile application development process thus requires a running middleware to which developers can temporarily register and upload their dummy application archive (which includes the metadata xml for SmartSync applications), assign them to their MI client thru an MI WebConsole, and deploy it.

Personally, this is somewhat undesirable. I don't think that synchronization with the middleware is already a must on the very first phase of realizing a client application. Can't I temporarily deploy my application into my MI client WITHOUT having a Web Console? What if I don't have a network connection and was asked to create to simple Generic Sync application thru the phone? Can't I develop and run a test on my application without the help of a WebConsole?

I'm pretty confident that most client-side MI application developers do have this question in mind. And my answer to this question is, YES, we can temporarily deploy our application-under-work into our MI client and run a test on it. In this way, we could initially check on the application's UI, create local data, and be able to check on the business logic as well without having the need of getting our data from the middleware. When our application is ready for the system integration (synchronization) test, this is then the phase when we need the middleware.

Target MI Version

This tool uses MI public APIs as well as some of the implementation classes to simulate the deployment process in the client. The tool was tested with MI 2.5 SP14 & SP15 client. If there are no changes in the implementation classes on the later SP versions, the code might work as well, but I would suggest you to test it and please give feedback so that I can update this article also.

The Mobile Component Descriptor

I had described regarding the Mobile Component Descriptor in my previous blog: How to access MCD info from the MI client registry. I would suggest you read this article before proceeding with this article if you like to understand how this information is used by MI.

Deployment Procedure

In the actual component deployment process, the client retrieves the registry info from the MI server during the synchronization. The MI client compares the local registry with that from the MI server. If there are components not available in the client, the framework will try to install the components by creating the corresponding MCD entry and then downloading the necessary installation archives like war or jar files basing on the MCD attributes on the archives location.

MI Local Application Deployer (LAD)

I would like to introduce a simple tool that I had been using... I'll name it as MI LAD.

The MI local application deployer described in this article simulates the handling of the said information and will try to install the application which exists in the local PC, i.e. your archive e.g. war file is located in your computer. The code also include an event to remove an application, however the application files and data are left and should be manually removed after the MI is shutdown.

I have chosen the use of JSP to make things simple. The JSP code is listed in Listing 1 at the end of this article. The code is pretty straight-forward and I guess the reader will easily understand it.

Installation Procedure

Here are the steps on how to create and install your LAD application.

  1. Create an empty JSP file and copy the JSP code in Listing 1 into that file.
  2. In your MI25 installation directory, locate <MI_DIR>\webapps\me folder and create a sub-folder named LAD.
  3. Place your JSP file into this folder and name it as index.jsp.
  4. Start MI and use a browser to access MI.
  5. Logon to MI and access http://localhost:4444/LAD

For those who don't want to be bothered of the above procedures, I had created a ZIP file which you can just extract into your <MI_DIR>\webapps\me folder. A directory will be created with the name LAD having only one JSP page under it named index.jsp. The file's location however is a free web hosting site and I cannot guarantee if it will stay for so long. Anyway you have the procedures above... You can get the file from this link.

 http://jny.50webs.com/files/LAD.zip 

MI LAD Screenshot

The code is pretty simple and I will avoid discussing it in details. The source is also available in JSP file, thus it would be very easy for you to study and experiment on it. In this section, I will just show you how it looks and how to use it.

The Front Page

The Figure 1 shows you the main screen of the MI LAD. The upper table lists the deployed components of the client device. The form on the lower portion serves as the entry dialog for the MCD information of our application to be deployed to our MI.

Figure 1. The MI Local Application Deployer (MI LAD)

MI LAD: Application Deployment

In this section I will show you how to deploy a SmartSync application WAR file which is in my local PC. Now, using MI LAD, I will try to deploy it into my MI installation, which is the same MI which runs MI LAD.

  1. First logon to MI. As shown in Figure 2 and Figure 3, there are no components deployed in the client except the framework itself.

    Figure 2. The MI main page with no deployed applications

     

    Figure 3. The MI Info page which displays only the framework

     

  2. Now, access MI LAD by entering http://localhost:4444/LAD in the address box of your browser. The same page just like what is shown in Figure 1 will appear.
  3. As shown in Figure 4, specify the MCD info and location of the WAR archive. In this example, the application to be deployed is a SmartSync application and the meRepMeta.xml is included in the archive.

    Figure 4. The MI LAD form with MCD info entries

     

  4. After entering all the necessary information, press on add button. The result will be displayed as shown in the Figure 5.

    Figure 5. The deployment result screen 

     

  5. Restart MI. And now checking on the data directory, we can see that our application's metadata was successfully serialized as shown in the Figure 6. (Take note that the deployed application was a SmartSync application)

    Figure 6. The shared data folder which contains the metadata

     

  6. And our application was successfully copied into the webapps directory as shown in Figure 7.

    Figure 7. The application archive copied in the webapps folder

     

  7. After we restart the MI client, we now have the link to our application in the MI main page (Figure 8) as well as in the MI info (Figure 9).

     Figure 8. The MI page with the locally deployed application link  

     

    Figure 9. The MI Info page with the locally deployed application MCD entry.

     

  8. Let's try running our application and create some data (Figure 10).

    Figure 10. Creating a data in the deployed application

     

  9. After we create some data, (in the example, I created 10 header entries) we can now see some data added into the data directory of our application as shown in Figure 11.

Figure 11. The separate data folder with the inserted header data

 

MI LAD: Application Removal

To remove the deployed application, select on the DEL link under the Action column of the MCD table as shown in Figure 5. This operation will only remove the application from the MI client registry. The data folders and the application files remain in the client and should be manually removed when the MI is shut down. During runtime, MI references to these files and could not be removed by our MI LAD. In the future, I will try adding some SmartSync codes that will remove all the data thru the persistence API call. The application file under the webapps directory may be removed as well if necessary.

Final Note

When you try to synchronize your MI client having the locally deployed component info, it will be removed by the framework when it tries to compare the registry in the client and in the middleware. If you want to execute the sychronization test with your middleware, then you have to register your application and deploy it thru the MI WebConsole. Your application must be assigned with a valid conversation ID that is generated by the server for it to synchronize with the middleware properly.

Summary

I had described here a very simple tool for locally deploying an application archive. I do hope that this tool will help you in developing your client application offline without a need of an MI WebConsole. In such way, one can develop the client application in parallel with the server-side developments.

The code of MI LAD uses the implementation classes of MI. And it might not work in the future releases of MI if the implementation classes are changed. Since the code is in JSP, it will be very easy for you to do the necessary code update if ever there will be changes in the invoked implementation classes.

Listings

The JSP code of the tool is described below. I put the all the functions in the lower portion of the file. If you want to use a servlet rather than the JSP, you can do it as well. Be sure also that you're using JDK instead of a JRE.

The MI Local Application Deployer


<%@ page language="java" contentType="text/html; charset=Shift_JIS"%>
<%@ page import="com.sap.ip.me.api.user.*" %>
<%@ page import="com.sap.ip.me.core.*" %>
<%@ page import="java.io.*"%>
<%@ page import="com.sap.ip.me.spi.smartsync.*"%>
<%@ page import="com.sap.ip.me.api.conf.*"%>
<%@ page import="com.sap.ip.me.smartsync.core.*"%>

<%!   
final String  _APPNAME_ = "appName";
final String  _APPVERS_ = "appVersion";
final String  _APPDESC_ = "appDescription";
final String  _APPNMSP_ = "appNamespace";
final String  _APPLOCT_ = "appLocation";
final String  _APPBKUP_ = "appBackup";
final String  _APPTYPE_ = "appType";

final String  _EVNTSTR_ = "event";
final String  _EVNTADD_ = "ADD";
final String  _EVNTDEL_ = "DEL";
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<%
 User currentUser = UserManager.getInstance().getCurrentUser();
 String msg = processEvent(request);
%>
<head>
<style type="text/css">
a {  font-family: "Tahoma", "Verdana"; font-size: xx-small; font-weight: bold; color: #FF9900; text-decoration: none}
.jgtext {  font-family: "Arial", "Helvetica", "sans-serif"; font-size: xx-small; color: #666666; text-decoration: none}
.jgheader {  font-family: "Tahoma", "Verdana"; font-size: xx-small; font-weight: bold; color: #006600; text-decoration: none}
.jgtext2 { font-family: "Arial", "Helvetica", "sans-serif"; font-size: xx-small; color: #333333; text-decoration: none }
.jgheader2 { font-family: "Arial", "Helvetica", "sans-serif"; font-size: xx-small; color: #333333; text-decoration: none ; font-weight: bolder}
.jgtext2bold {
 font-family: "Arial", "Helvetica", "sans-serif";
 font-size: xx-small;
 color: #333333;
 text-decoration: none;
 font-weight: bold;
}
.collapse { position: absolute; visibility: hidden; }
.expand { position: relative; visibility: visible; }
</style>

<% if(currentUser==null){  %>
 <meta http-equiv="refresh" content="3; URL=http://localhost:4444">
<%}%>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<title>MI Local Application Deployer (LAD) </title>
</head>

<body>

<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr class="jgheader2" >
    <td colspan="6" >
    <% if(currentUser!=null){  %>
     Deployed Applications for <%=currentUser.getUniqueName()%>
    <% }else{%>
     Please LOGON. Redirecting to ME page in 3 seconds...
    <% } %>
    </td>
  </tr>
  <tr  class="jgheader2" bgcolor="aquamarine">
    <td >Namespace</td>
    <td >Name</td>
    <td >Version</td>
    <td >Description</td>
    <td >Id</td>
    <td >Type</td>
    <td >Action</td>   
  </tr>
  <%
  if(currentUser!=null){
   MobileSolutionDescriptor[] msds = ConversationIdHandler.getInstance().getAllMobileSolutionDescriptorsForUser(currentUser);
   for(int i=0; i<msds.length; i++){
  %>
  <tr  class="jgtext2"  bgcolor=<%out.print((i%2 == 1)? "'lavender'" : "'white'");%>>
    <td ><%=msds[i].getNamespace()%></td>
    <td ><%=msds[i].getName()%></td>
    <td ><%=msds[i].getVersion()%></td>
    <td ><%=msds[i].getDescription()%></td>
    <td ><%=msds[i].getId()%></td>
    <td ><%=msds[i].getType().toString()%></td>
    <td ><%if(!"Framework".equals(msds[i].getType().toString())){%><a href="<%=createDelLink(msds[i].getName(),msds[i].getVersion())%>">DEL</a><%}%></td>
  </tr>
  <%
   }
  }
  %>
  <tr><td colspan="7" bgcolor="aquamarine" height="1pt"></td></tr>
</table>
<br><br>
<form name="tForm" id="tForm" action="?" method="post">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr >
    <td colspan="2" class="jgheader2">Deploy Application:</td>
  </tr>
  <tr >
    <td colspan="2" class="jgheader2" bgcolor="lavender" height="10pt"></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right" >Namespace:</td><td ><input size="20" name="<%=_APPNMSP_ %>" <%=_APPNMSP_ %> type="text" maxlength="30"/></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right" >Name*:</td><td ><input size="20" name="<%=_APPNAME_ %>" id="<%=_APPNAME_ %>" type="text" maxlength="30"/></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right">Version*:</td><td ><input size="10" name="<%=_APPVERS_ %>" id="<%=_APPVERS_ %>" type="text" maxlength="5"/></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right">Description:</td><td ><input size="50" name="<%=_APPDESC_ %>" id="<%=_APPDESC_ %>" type="text" maxlength="50"/></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right">Type:</td><td>
     <select name="<%=_APPTYPE_ %>" id="<%=_APPTYPE_ %>">
       <option value="APPLICATION" selected="selected">Application</option>
       <option value="FRAMEWORK">Framework</option>
       <option value="ADDON">Add-On</option>
       <option value="JVM">JVM</option>
       <option value="SSL">SSL</option>
       <option value="DRIVER">Driver Add-On</option>
      </select>
    </td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right">Archive*:</td><td ><input name="<%=_APPLOCT_ %>" id="<%=_APPLOCT_ %>" type="hidden" />
    <input name="appArchive" id="appArchive" size="40"  type="file" class="jgtext2" onchange="window.document.tForm.appLocation.value=this.value"/>
    </td> 
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td align="right">LeaveOriginal:</td><td ><input name="<%=_APPBKUP_ %>" id="<%=_APPBKUP_ %>" type="checkbox" checked="checked"/></td> 
  </tr>
  <tr >
    <td colspan="2" class="jgheader2" bgcolor="lavender" height="10pt"></td>
  </tr>
  <tr align="left"  class="jgtext2" bgcolor="lavender">
    <td ></td><td><input type="submit" value="add" class="jgtext2bold"/>
    <input type="reset" value="clear" class="jgtext2bold"/>
    </td>
  </tr>
  <tr >
    <td colspan="2" class="jgheader2" bgcolor="lavender" height="10pt"></td>
  </tr>
  <tr class="jgtext2">
    <td colspan="6"><%=msg%></td>
  </tr>
</table>
<input type="hidden" name="event" value="ADD"/>
</form>
<br>

</body>
</html>

 <! -- Functions -->
<%!  
String processEvent(HttpServletRequest request){
 String msg = "";
 String event = request.getParameter(_EVNTSTR_);
 
 if(event!=null){
  //required parameter for both add and delete
  String appName = request.getParameter(_APPNAME_);
  String appVersion = request.getParameter(_APPVERS_);
  
  msg = _APPNAME_ + " = " + appName + "<br> " +  _APPVERS_ + " = " + appVersion+"<br> " ;
  if(hasSpaceChar(appName))
   return _APPNAME_ + " should not have a character space. -> " + appName;
  if(hasSpaceChar(appVersion))
   return _APPVERS_ + " should not have a character space. -> " + appVersion;

  if(_EVNTADD_.equals(event)){
   
   String appDescription = request.getParameter(_APPDESC_);
   String appNamespace = request.getParameter(_APPNMSP_);
   String appLocation = request.getParameter(_APPLOCT_);
   boolean appBackup = request.getParameter(_APPBKUP_)!=null;
   String appType = request.getParameter(_APPTYPE_);
   
   if(!checkParameters(appName, appVersion, appLocation)){
    msg = "<font color='red'><b>Name, Version and ArchiveLocation are mandatory!</b></font>";
   }else{
    if(!checkLocation(appLocation))
     return "<font color='red'><b>" + appLocation + " is INVALID!</b></font>";
    if(appDescription == null ) appDescription = appName;
    if(appNamespace == null) appNamespace = "";
    if(appBackup){
     String newLocation = copyArchive(appName,appLocation.trim());
     if(newLocation==null)
      return "Failed to copy the archived!";
     appLocation = newLocation;
    }
    
    try{
     msg += _APPNMSP_ + " = " + appNamespace + "<br> " +
       _APPDESC_ + " = " + appDescription + "<br> " +       
       _APPLOCT_ + " = " + appLocation + "<br> " +
       _APPBKUP_ + " = " + appBackup + "<br> " +
       _APPTYPE_ + " = " + appType + "<br> ";
      
     installComponent(appNamespace,appName,appVersion,appDescription,appType,appLocation);
     
     msg += "Component successfully installed. <b> Restart MI...</b>";
    }catch(Exception e){
     e.printStackTrace();
     return e.getMessage();
    }
   }
   
  }else if(_EVNTDEL_.equals(event)){
   try{
    removeComponent(appName,appVersion);
    msg += "Component successfully removed. <b> Restart MI...</b>";
   }catch(Exception e){
    e.printStackTrace();
    return e.getMessage();
   }
  }else{
   msg = event + " is not unknown!";
  }
 }
 
 return msg;
}
String createDelLink(String name, String version){
 return "?event=DEL&"+_APPNAME_+"="+name+"&"+_APPVERS_+"="+version;
}
void removeComponent(String appName, String appVersion) throws Exception{
 ApplicationManager mgr = ApplicationManager.getInstance();
 MobileSolutionDescriptor msd = mgr.getMobileSolutionDescriptor(appName, appVersion);
 if(msd==null) return;
 mgr.unregisterMobileSolutionDescriptor(msd);
 removeConvId(msd);
 mgr.saveMobileSolutionDescriptors();
}

void removeConvId(MobileSolutionDescriptor msd) throws InstallationException{
 ConversationIdHandler hdr = ConversationIdHandler.getInstance();
 ConversationId sepId = hdr.getConversationId(msd,UserManager.getInstance().getCurrentUser());
 hdr.removeConversationId(sepId.getId());
 ConversationId shrId = hdr.getConversationId(msd,UserManagerImpl.getSharedUser());
 hdr.removeConversationId(shrId.getId());
}
//install the component
void installComponent(String appNS, String appName, String appVersion,
  String appDesc, String appType, String appLocation)
  throws Exception{
 
 //create an MSD
 MobileSolutionDescriptor msd = new MobileSolutionDescriptor(appNS,appName,appVersion);
 //set the description
 msd.setDescription(appDesc);
 //set the property type: APPLICATION etc...
 msd.setPropertyValue("TYPE",appType);
 //set the runtime propery. only JSP is supported
 msd.setPropertyValue("RUNTIME","JSP");
 //set the customizing entry for localpath
 msd.setCustomizingValue("LOCALPATH",appLocation);
 //invoke the setApplicationType method to assign the properties.
 msd.setApplicationType();
 //set this property to true
 msd.setInstallationFileAlreadyDownloaded(true);
 
 assignConvId(msd);
 
 //install the application
 msd.getType().install("
file:///" + appLocation,msd);
 //set visible to true or false if you don't want it to be visible
 msd.setVisible(true);
 //set this to false
 msd.setIncomplete(false);
 //set this to true
 msd.setInstalled(true);
 
 ApplicationManager.getInstance().registerMobileSolutionDescriptor(msd);
 ApplicationManager.getInstance().saveMobileSolutionDescriptors();
}
//assign a conversationId to the MSD
void assignConvId(MobileSolutionDescriptor msd) {
 assignConvId(msd,true);
 assignConvId(msd,false);
}
//assign a conversationId to the MSD
void assignConvId(MobileSolutionDescriptor msd, boolean isSeparated) {
    try {
      String id = isSeparated ? generateConvId("SEP") : generateConvId("SHR");
      User usr = isSeparated ? UserManager.getInstance().getCurrentUser(): UserManagerImpl.getSharedUser();
      ConversationId convId = new ConversationId(id, msd, usr, false);
      ConversationIdHandler.getInstance().addConversationId(convId);
    }
    catch (InstallationException e) {
     e.printStackTrace();
    }
}
//ConversationId string generator
String generateConvId(String PRE){
    StringBuffer idBuffer = new StringBuffer(24);
    idBuffer.append("LOCAL");
    idBuffer.append(PRE);
    String v = String.valueOf(System.currentTimeMillis());
    idBuffer.append(v);
    return idBuffer.toString();
}

//check the specified parameter for a space char
boolean hasSpaceChar(String param){
 return param.indexOf(' ') > -1;
}
//check the mandatory parameters
boolean checkParameters(String appName, String appVer, String appLocation){
 return appName!=null && appName.length() > 0 &&
   appVer!=null && appVer.length() > 0 &&
   appLocation!=null && appLocation.length() > 0;
}
//check the file existence
boolean checkLocation(String appLocation){
 if(appLocation==null) return false;
 File f = new File(appLocation);
 return f.exists() && f.isFile();
}
//copy archive utility
String copyArchive(String appName, String appLocation){
 int pIdx = appLocation.indexOf('.');
 String appExt = appLocation.substring(pIdx);
 File of = new File(appLocation);
 File nf = new File(this.getServletContext().getRealPath(""), appName + appExt);
 try{
  fileCopy(of,nf);
 }catch(IOException e){
  return null;
 }
 return nf.getAbsolutePath();
}
void fileCopy(File fromFile, File toFile) throws IOException{
    FileInputStream from = null;  // Stream to read from source
    FileOutputStream to = null;   // Stream to write to destination
    try {
      from = new FileInputStream(fromFile); 
      to = new FileOutputStream(toFile);
      byte[] buffer = new byte[4096];
      int bytes_read;
      while((bytes_read = from.read(buffer)) != -1)
        to.write(buffer, 0, bytes_read);
    }
    finally {
      if (from != null) try { from.close(); } catch (IOException e) { ; }
      if (to != null) try { to.close(); } catch (IOException e) { ; }
    }
  }
%>