MicroStrategy Administrator iPhone App: Part 2

Following up on Part 1 that talked about the idea for an Administrator iPhone app and showed a demo of the latest build, I’d now like to dive into the backend Java Web Service that does all of the heavy lifting for the application.  There will be lots of useful code in here to perform all kinds of tasks in MicroStrategy that could be extracted out for their own purposes.  I’ve used a few snippets already to answer direct questions on the forums.  I hope you find this code useful or at least interesting.

DISCLAIMER: I’m not a professional programmer.  I’m a hobbyist who tries to build useful things for day to day use.  Any code that I share here is code that I use for non-Production tasks, and you should use them for informational purposes and not straight copy/deploy.

Why a Java Web Service?
I touched on this in the original post, but I think it warrants some more consideration.  The obvious choice would be to have the phone application connect direct to the IServer and just do it’s magic.  Unfortunately at present time, the MicroStrategy SDK lacks the necessary APIs to support any of those tasks.  Currently, they only exist in the Java SDK, so some kind of middle tier is going to be required.  My first iterations was just to make a pass through Socket Server to handle the requests, but it was too much trouble to juggle those connections on the phone application.  Specifically, when the app is sent to the background or supporting multiple users.  Having to manage whether you’re connected to the Socket Server at any moment is more trouble than you’ll want to undertake, trust me.

Web Services is a natural choice since the workflow is already tightly integrated into Flash Builder.  It was a simple drag and drop to wire my application up to a Web Service.  Since I didn’t want a 2nd middle tier in the form of a .NET bridge, the logical choice was just to convert my Java Server into a Web Service.  This turned out to be much harder than I expected (not a real Java Programmer) but in the end it was worth it because the final result was something very easy to work with.  The phone application just makes blind calls to the Web Service and waits for the reply.  The Web Service is always running and just has to check to see if it’s IServer session is active (and open a new one if it’s not).  This model also easily supports multiple users since each request doesn’t depend on future or past requests.

Source Code
I’m sorry for the ugly code formatting on this site.  Honestly, I just haven’t put the effort in to find a good formatting solution.  I’m still holding out hope that Google will update their software with a built in solution.  In the mean time, you can download the code from here if you don’t want to read this uglyness.  The only real reason I even paste it here to begin with is to help the Googlers.


package com.MSTRJavaWS;

import com.microstrategy.web.objects.WebFolder;
import com.microstrategy.web.objects.WebIServerSession;
import com.microstrategy.web.objects.WebObjectSource;
import com.microstrategy.web.objects.WebObjectsException;
import com.microstrategy.web.objects.WebObjectsFactory;
import com.microstrategy.web.objects.WebProjectInstance;
import com.microstrategy.web.objects.WebProjectInstances;
import com.microstrategy.web.objects.WebProjectSource;
import com.microstrategy.web.objects.WebScheduleTrigger;
import com.microstrategy.web.objects.WebSubscriptionsSource;
import com.microstrategy.web.objects.admin.WebObjectsAdminException;
import com.microstrategy.web.objects.admin.monitors.*;
import com.microstrategy.webapi.EnumDSSXMLLevelFlags;
import com.microstrategy.webapi.EnumDSSXMLObjectTypes;

public class MSTRAdminMobile {

private static WebObjectsFactory factory = null;
private static WebIServerSession serverSession = null;
private static JobSource source = null;
private static String _authKey = "placeholder";


public MSTRAdminMobile() {}


The Way I’ve done this is broken out into a series of functions that are exposed to the Web Service.  Each call requires an _authKey, which ideally would just be some unique identifier, more than likely the Device ID you’re calling from.  Initial setup would require that you provide the authorized ID to the code here (or in the future, a configuration file).  This prevents someone from stumbling across your web service and controlling your IServer.  It’s loose, but probably effective enough (at least for these sample purposes).

This first function is for the Job Monitor and returns any Jobs that are currently running on the IServer, including tasks like Waiting for Prompt.  The | (pipe) formatted return string is just the delimiter I use on the Phone app to parse the return string.

public static String[] getJobDetails(String authKey) {
say("getJobs");

String[] returns = new String[1];
returns[0] = "||No Jobs Running|";

if (!checkAuthKey(authKey))
returns[0] ="||invalid authKey|";
else {
checkSession();

try {
     // Sends the request to Intelligence Server to retrieve job information
source.setLevel(EnumDSSXMLLevelFlags.DssXmlBrowsingLevel);
JobResults results = source.getJobs();

if (results.getCount() != 0) {

returns = new String[results.getCount()];
     // Loop through the results to print out browsing information, such as
     // the job ID, the user that owns the job, and duration, in seconds, of the job
     for (int i=0; i < results.getCount(); i++) {
          Job singleJob = results.get(i);
          returns[i] = singleJob.getJobID() + "|" + singleJob.getUserName() + "|" + singleJob.getDescription().replaceAll("Running report ", "") + "|" + singleJob.getDuration();
     }
}
} catch (WebObjectsAdminException woae) {
         woae.printStackTrace();
}
}
return returns;

}

When the user on the Phone touches a Job, one of the options is to display the SQL.  Here, we pass the jobID of the selected Job to retrieve the SQL that is currently executing.

public static String getSQL(String authKey, int jobID) {
say("getSQL" + jobID);
String returns = "SQL Not Available for job " + Integer.toString(jobID);
if (!checkAuthKey(authKey)) {
returns ="invalid authKey";
}
else {
try {
source.setLevel(EnumDSSXMLLevelFlags.DssXmlDetailLevel);
JobResults results = source.getJobs();

for (int x=0;x<results.getCount();x++) {
Job job = results.get(x);
if (job.hasDetails() && job.getJobID() == jobID) {
JobDetails jobDetails = (JobDetails) job;
returns = jobDetails.getSQL();
returns = returns.substring(0, returns.length()/2).trim();
break;
}
}
} catch (WebObjectsAdminException ex) {
ex.printStackTrace();
}
}
return returns;
}

Another possibility is that we want to Cancel the Job.  This function works similar to the previous one by accepting a jobID parameter and sending a cancellation request to the IServer.

public static void killJob(String authKey, int jobID) {
say("killJob" + jobID);

if (checkAuthKey(authKey)) {
checkSession();
try {
     JobResults results = source.getJobs();
   
     // Check whether we have any result returned
     if (results.getCount() > 0) {
         // Obtain the job manipulator object
         JobManipulator jobManipulator = source.getManipulator();

         // Adds the job IDs to the deletion task for batch operation
         jobManipulator.addDeletionTask(jobID);
         try {
              // Submit the request to delete jobs
              jobManipulator.submit();            
              jobManipulator.clear();
             
         } catch (MonitorManipulationException mme) {                            
              for (MonitorManipulationFailure mmf : mme.getFailures())
              say(mmf.getMessage());
         }
     }
} catch (WebObjectsAdminException woae) {
         woae.printStackTrace();
}
}
}

This next function returns a list of Users that are currently connected.  It’ll return separate entries for Server Connections and Project Connections.

public static String[] getUsers(String authKey) {
say("getUsers");
String[] returns = new String[1];
returns[0] = "None||";

if (!checkAuthKey(authKey)) {
returns[0] ="invalid authKey||";
}
else {
checkSession();
UserConnectionSource userSource = (UserConnectionSource) factory.getMonitorSource(EnumWebMonitorType.WebMonitorTypeUserConnection);
UserConnectionResults results;
userSource.setLevel(EnumDSSXMLLevelFlags.DssXmlBrowsingLevel);

try {
results = userSource.getUserConnections();
returns = new String[results.getCount()];
for (int i=0; i < results.getCount(); i++) {
          UserConnection singleConn = results.get(i);
          returns[i] = singleConn.getUserName() + "|" + singleConn.getProjectName() + "|" + singleConn.getSessionID();
     }
} catch (WebObjectsAdminException e) {
e.printStackTrace();
}
}
return returns;
}

As expected, this accompanying function will disconnect the User.  Since there are separate entries for the Server and Project Connections (you can see those by viewing the User Connection Monitor via Desktop), you can disconnect a user from a single Project or from everything depending on which one you choose to kill.  This function requires the sessionID of the user you wish to disconnect.  Disconnecting them will also terminate any open Jobs they have.

public static void killUser(String authKey, String sessionID) {
say("killUser " + sessionID);

if (checkAuthKey(authKey)) {
// Get the user connection source
UserConnectionSource userSource = (UserConnectionSource)    
factory.getMonitorSource(EnumWebMonitorType.WebMonitorTypeUserConnection);

// Get the user connection manipulator
UserConnectionManipulator userManipulator = userSource.getManipulator();

// Add another batch task to disconnect all projects associated with the second session
userManipulator.addDisconnectionTask(sessionID);

try {
     userManipulator.submit();
} catch (WebObjectsAdminException woae) {
         woae.printStackTrace();
} catch (MonitorManipulationException mme) {
         // We have failure in disconnecting user connection, so we display the failure
         MonitorManipulationFailure[] mmf = mme.getFailures();
         System.out.println(mmf.toString());
}
}
}

This next function returns a list of all of the Schedules on the IServer.

public static String[] getSchedules(String authKey) {
say("getSchedules");
String[] returns = new String[1];
returns[0] = "0|No Schedules";

if (!checkAuthKey(authKey)) {
returns[0] ="0|invalid authKey";
}
else {
try {
checkSession();
WebFolder triggers;
WebSubscriptionsSource wss = factory.getSubscriptionsSource();

triggers = wss.getTriggers();

returns = new String[triggers.getChildCount()];
for (int x=0;x<triggers.getChildCount();x++) {
returns[x] = triggers.get(x).getID() + "|" + triggers.get(x).getDisplayName();
}
} catch (WebObjectsException e) {
e.printStackTrace();
}
}
return returns;
}

MicroStrategy doesn’t have a mechanism to actually “disable” a schedule, but there is a little trick for Time Based schedules.  This function will set the Start Time of the Schedule to a point in the future, effectively disabling by forcing it to miss what would otherwise be it’s next scheduled execution.  Since there’s also no indication that the Schedule is “disabled”, we tag the description with the text “Disabled” so that it’s apparent from within MicroStrategy Desktop.

public static void disableSchedule(String authKey, String scheduleGUID) {
say("disableSchedule " + scheduleGUID);

if (checkAuthKey(authKey)) {

try {
checkSession();
WebObjectSource wos = serverSession.getFactory().getObjectSource();
  WebScheduleTrigger trig = (WebScheduleTrigger) wos.getObject(scheduleGUID, EnumDSSXMLObjectTypes.DssXmlTypeScheduleTrigger);
  trig.populate();

  // Set Description and Start Date of this trigger
  trig.setDescription("DISABLED by AdminMobile: " + trig.getDescription());
  trig.setStartDate("12/21/2020");

  // Saving Trigger object
  wos.save(trig);

} catch (WebObjectsException e) {
e.printStackTrace();
}
}
}

This function returns a list of all of the Caches on the server, ideally allowing you to delete individual caches or clear the entire project cache.  Unfortunately at the time of this writing, this code is not operational.  I’ve got an open case with Technical Support for the issue, and I expect a future hotfix will resolve it without the need for a code change.

public static String[] getCaches(String authKey, String project) {
say("getCaches");
String[] returns = new String[1];
returns[0] = "No Caches Found";

if (!checkAuthKey(authKey)) {
returns[0] ="invalid authKey";
}
else {
checkSession();
// Get the cache source object
CacheSource source = (CacheSource) factory.getMonitorSource(EnumWebMonitorType.WebMonitorTypeCache);

try {
     // Sends the request to Intelligence Server to retrieve cache information
     CacheResults results = source.getCaches();

     // Loop through the results to print out information of caches
     for (int j = 0; j < results.size(); j++) {
          // Caches are group on project level, so get the cache collection for each project
          Caches result = results.get(j);

          if (result.getProjectName() == project) {
          returns = new String[result.getCount()];
          for (int i = 0; i < result.getCount(); i++) {
               Cache cache = result.get(i);
               returns[i] = cache.getID() + "|" + cache.getCacheSourceName() +"|" + cache.getCreationTime();
          }
          }
     }
}
catch (WebObjectsAdminException woae) {
        woae.printStackTrace();
}
}
return returns;
}

This function goes along with the Cache function above to provide a list of Project to choose which Caches you want to see.  It works, though in the context of this application it doesn’t do much with the Cache function not working.

public static String[] getProjects(String authKey) {
say("getProjects");

String[] returns = new String[1];
returns[0] = "No Projects Found";

if (!checkAuthKey(authKey)) {
returns[0] ="invalid authKey";
}
else {
try {
WebProjectSource oProjectSource = factory.getProjectSource();
WebProjectInstances oPInstanceList = oProjectSource.getAccessibleProjectsInCluster();
WebProjectInstance oPInstance = null;

returns = new String[oPInstanceList.size()];

for(int i=0;i<oPInstanceList.size();i++){
oPInstance = oPInstanceList.get(i);
returns[i] = (oPInstance.getProjectName());
}
} catch(Exception e){
say("Error: "+ e.getMessage());
}
}
return returns;
}

You may have noticed every function above starts off by calling the checkSession() function.  This function simply makes sure that there is a currently active session for the above functions to execute against.  First we make sure a session has been defined (== null), then even if we’ve defined it we have to make sure it’s active and hasn’t timed out (isActive), and even then sometimes it can be stale (isAlive). If any of those checks fail, then we create a new session and proceed to executing the request.  This is the design that I was saying works really well in this context.  Normally, the Phone application would have to handle this session juggling in addition to making sure network connectivity was up.  In this scenario though, we can easily create sessions at will and share them among lots of Phone connections in the event we have multiple users.

private static void checkSession() {
if (serverSession == null)
getSession();
else if (!serverSession.isActive())
getSession();
else
try {
if (!serverSession.isAlive())
getSession();
} catch (WebObjectsException e) {
e.printStackTrace();
}
}

private static void getSession() {
System.out.println("Connecting to MicroStrategy");
factory = WebObjectsFactory.getInstance();
serverSession = factory.getIServerSession();
serverSession.setServerName("ISERVER");
serverSession.setServerPort(0);
//serverSession.setProjectName("PROJECT");
serverSession.setLogin("LOGIN");
serverSession.setPassword("PASSWORD");
try {
serverSession.getSessionID();
source = (JobSource) factory.getMonitorSource(EnumWebMonitorType.WebMonitorTypeJob);
} catch (WebObjectsException ex) {
System.out.println("Error: " + ex.getMessage());
}
}

And finally we have the last few functions that hold everything together.  The getSession() function that actually logs in and creates your connection to the IServer for manipulations, the checkAuthKey() function for our pedestrian security, and a convenience function that I like to use, say(), because I hate typing out System.out.println() :).  It also is a nice way to quickly enable logging, since I know all debug chatter will come through this function, so I can add logging to a single place and affect the entire application.

private static boolean checkAuthKey(String authKey) {
if (_authKey.equals(authKey))
return true;
else {
say("invalid authKey: " + authKey);
return false;
}
}

private static void say(String words) {
System.out.println(words);
}
}

Software and Deployment
I used Eclipse Indigo Java EE to create and deploy this.  There are lots of flavors of Eclipse with various tools, so I’ll help you out in that regard to point you to the right one.  The Enterprise Edition includes some of the Web Services testing and integration tools that you’ll need.

I also used Tomcat 7 as the Web Server to host this (to my knowledge, you can’t run Java Web Services using IIS) and Axis 2 for the Web Services plugin to Tomcat.  The nice thing is that all of this software is open source and hence free.  The bad thing is that all of this software is open source and hence convoluted :).  Setup was incredibly difficult for me, and it literally took me days to get an environment up and running.  I can’t possibly recite all of the steps and trials it took me, but these three links should greatly help out anyone who is trying:

http://www.softwareagility.gr/index.php?q=node/29

http://recluze.files.wordpress.com/2008/02/ws-tutorial.pdf

http://www.google.com

Conclussion
This was a fun project and I learned a lot.  A lot of this code can be reused for other purposes like more specific applications or to just quickly perform a task that would otherwise be manual.

Next up is a code walkthrough of the Phone application, so check back!

You may also like...