Introduction To The Client Side Java API

Ideas and tips for enhancing your TM1 application
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 21 - Getting The Collection Of Stocks

Meanwhile waiting for us back in the main() method is the code to call to get the collection of stock prices from Yahoo Finance.

Code: Select all

		// Create the hashmap that will hold the values returned from the Yahoo API.
		Map<String, Stock> stocks = null;
		
		// Get the information about the current stock prices.
		try {
			stocks = StockReader.getStocksFromArray(stocksArray,goLogWriter,iDebug );
		} catch (Exception e) {
			bContinue = false;
			goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": " + e.getMessage());
			goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": Unable to do anything without stock data; aborting.");
		}
Right, what is "Map"? It's one of Java's collection objects, known as a hashmap. It is, as the capital letter suggests, a complex object. It works on a key / value pair methodology. That is to say, for each item you store in the map, you have the value itself and you also have a key which uniquely identifies it, and which you can use to access it. The Map is an unordered collection, meaning that the values stored in it are not stored in a particular order and so you can't say "get me item 2 from the Map" because you can never be sure which one that might be. To access values you need to either use the key, or use another object called an Iterator to move through the Map (as we shall see shortly).

The key can be pretty much anything (String, integer even another object), just as the value can.

In this case:

Code: Select all

Map<String, Stock> stocks = null;
I'm defining it to have a String as the key, and a class from the Yahoo Finance library called Stock as the value. That's because this is what the Yahoo Finance library requires for the class method that it uses to populate the values. I'm initially setting the value to null and doing it outside the try block so that I can be sure that there will be no problems with scope when it comes to accessing it. (Remember, a variable that is defined within a code block is visible inside that code block only.)

To populate the Map with actual data, I make a call to the static .getStocksFromArray method of my StockReader class, passing in:
  • stocksArray, which is the name that I gave to the String array that is passed into main(); as I said previously while the String array is mandatory for the main() function, the use of the name args is not and I've elected to give it a more descriptive name;
  • The LogWriter object that I created in the preceding lines so that .getStocksFromArray can use it if it needs to write any output. There is no point in creating and initialising such an object for each and every method; you just create it once and share it around.
  • iDebug, being the integer representing the debug mode.
That method will then return a populated Map object, one hopes. Let's take a look at it.

The StockReader.getStocksFromArray Method

Code: Select all

package com.yourcompany.stockreader;

import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import com.yourcompany.logwriter.LogWriter;

import yahoofinance.Stock;
import yahoofinance.YahooFinance;
import yahoofinance.quotes.stock.StockQuote;

public class StockReader {

/**
 * This process takes a string array of stock codes, passes them to the YahooFinance API
 * and, we hope, returns an array of YahooFinance Stock objects embedded in a
 * hashmap collection.
 * @param stocksArray - A string array of stock codes. Non US codes must be suffixed by a country/exchange code like AX for Australia or PA for Paris. 
 * @param goLogWriter - A reference to a LogWriter object that outputs error logs to the log file folder of the server.
 * @param debugMode - Set to a non-zero value to output debugging information to the console.
 * @return - Returns a hashmap of stock codes, and Stock objects which contain data about those stocks.
 * @throws Exception
 */
	public static Map<String, Stock> 
	  getStocksFromArray(String[] stocksArray, 
		LogWriter goLogWriter, int debugMode) throws Exception {

		// The YahooFinance.get method should return a HashMap of
		// Stock objects which, in theory, contain all of the 
		// current data for a given stock.
		
		String SC_PROCEDURE_NAME = "getStocksFromArray"; 
		String sMsgErr ="";
		String sKeyCurrent = "";
		
		if(stocksArray.length==0){
			throw new Exception("The length of the array which passes the stock codes cannot be zero.");
		}		

		// Create the return object. This is a hashmap
		// with a key / value pairing; the key is the stock code
		// that we pass to Yahoo, and the value is a stock
		// object from the Yahoo class library which will (we hope)
		// be filled with juicy data.
		Map<String, Stock> stocks = null;
		try {
			// We pass the array of strings containing the stock codes
			// to the YahooFinance library (calling its .get method), 
			// and hopefully it sends us back the populated hashmap. 
			stocks = YahooFinance.get(stocksArray);
			if(stocks.size()==0){
				throw new Exception ("No stock data was returned.");
			}
			if(debugMode!=0){ System.out.println("Stocks has " + stocks.size() + " elements");}
		
		    // I'm now going to check the returned stock objects for
			// suspect data. One of the easiest ways is to check the
			// last trade time, since it's an object.
			// Primitive values may have a legitimate value of 0.
			// But an object will have a null value if it can't
			// be populated.
			Iterator<Entry<String, Stock>> it = stocks.entrySet().iterator();
		    while (it.hasNext()) {
		        try {
					Entry<String, Stock> pair = it.next();
					Stock stockCurrent = (Stock) pair.getValue();
					sKeyCurrent = (String) pair.getKey();
					StockQuote sqCurrent = stockCurrent.getQuote();

					if (debugMode!=0) {
						System.out.println(sqCurrent.getSymbol() + " " 
						  + sqCurrent.getBid() + " " + sqCurrent.getPrice()
						  + " " + sqCurrent.getLastTradeSize() + " " 
						  + sqCurrent.getVolume());
					}
					Calendar calLastTrade = sqCurrent.getLastTradeTime();
					String sDateTime =  String.format("%04d", calLastTrade.get(Calendar.YEAR))
					 + "-" + String.format("%02d", (calLastTrade.get(Calendar.MONTH) + 1)) 
					 + "-" + String.format("%02d", (calLastTrade.get(Calendar.DAY_OF_MONTH)))
					 + " " + String.format("%02d", (calLastTrade.get(Calendar.HOUR_OF_DAY)))
					 + ":" + String.format("%02d", (calLastTrade.get(Calendar.MINUTE)))
					 + ":" + String.format("%02d", (calLastTrade.get(Calendar.SECOND)));

					if(debugMode!=0){ System.out.println( "Last Trade = " + sDateTime); }
				} catch (NullPointerException e) {
					// Any other errors are unexpected and and will spit out to the
					// exception handler outside of the loop.
					// We do NOT re-throw this one because it's specific to this
					// stock; we want to move along the lines and test the
					// remaining ones.
					if(debugMode!=0){
						System.out.println("Null pointer exception in " + SC_PROCEDURE_NAME);
						e.printStackTrace();
					}
					goLogWriter.outputToLog(SC_PROCEDURE_NAME 
							+ ": Null exception in stock " + sKeyCurrent 
							+ ". This suggests that the code is invalid, "
							+ "and the stock has been removed from the results list.");
					it.remove();
					//stocks.remove(sKeyCurrent);
				}		        
		    } // End of while loop.
		    
		}catch (UnknownHostException e){
			// Generally this will occur if the network connection is down.
			if(debugMode!=0){
				System.out.println(SC_PROCEDURE_NAME + "Unknown Host exception.");
				e.printStackTrace();
			}			
			sMsgErr = SC_PROCEDURE_NAME + ": Unknown host exception. This suggests that your internet connection is down. "; 
			goLogWriter.outputToLog(sMsgErr );
			throw new Exception (sMsgErr);

		}catch (Exception e) {
			if(debugMode!=0){
				System.out.println("Generic exception in " + SC_PROCEDURE_NAME);
				e.printStackTrace();
			}			
			sMsgErr = SC_PROCEDURE_NAME + ": Unexpected error getting stock array data: " + e.toString() ;
			goLogWriter.outputToLog(sMsgErr);
			// The errors that aren't important have already been dealt with.
			throw new Exception(sMsgErr);
		} // End of catch block outside the while loop.

		// The following will occur if we have removed all of
		// the apparently corrupt elements from the hashmap.
		if (stocks.size() == 0){
			throw new Exception(SC_PROCEDURE_NAME + ": There are no valid stocks in the returned data. Aborting load.");
		}
		
		// Otherwise, we return the populated hashmap object.
		if(debugMode!=0){ System.out.println("Stocks is returning " + stocks.size() + " elements");}
		return stocks;		
	}
	
}
There's a lot of commentary already built in here so hopefully I don't have to explain much more outside of that. But a few key points:
  • YahooFinance.get is obviously a static method; we don't need to create an instance of the YahooFinance object for this. We pass in the String array containing the stock codes that were originally passed into main().
  • The first if() test (to check that there is something in the stockArray String Array) isn't inside a try/catch block, so the exception blows straight back up to the main() method. (Again, don't use generic Exception objects as I've done here; this is just for demonstration.) That's why the method definition has to include the throws Exception statement.
  • Because the primary block of code uses nested try/catch blocks it can be a little hard to pick up on where the exceptions end up unless you have the code in Eclipse and are using the bracket matching options that I described earlier, or paste the code into Notepad++ which does have full folding options for Java code. There are two catch blocks for the first try. The first one comes into play if anything goes wrong with the following line of code:

Code: Select all

stocks = YahooFinance.get(stocksArray);
If your network connection is down, that will throw an UnknownHostException. Consequently the first catch block checks for that exception:

Code: Select all

catch (UnknownHostException e)
If that occurs it will write a message to the error log (using the goLogWriter object's .outputToLog method), then throw a generic Exception (again, don't do that) back to main() to let it know that there was no joy in getting the stock data. (If you refer back to the code for that method you'll see that it catches the error in its own catch block, sets the bContinue flag to false so that no further code blocks in main() are executed, and writes the fact that it is not continuing out to the log file.)

An Exception is also thrown if the .size method of the Map returns a zero size, meaning that no data came back. That would pass through the second of the catch blocks, which takes care of any exceptions other than the UnknownHostException one.

Other matters:
  • As I mentioned, to go through a Map object you need an Iterator object. It would be drifting too far from topic (something that we've already done too many times) for me to go into the syntax of the diamond ( that is, <> ) operators used in these types of collections, but take it as read that what the code does is to create an Iterator object which we assign to a variable named it. The Iterator will move through the Map returning Entry objects which consist of the combination of the value (which, as I said, is a Stock object from the Yahoo Finance library), and its key (a String representing the stock code). It will do this while the it.hasNext method returns true; that is, while there is an item to move to. The Entry object we assign to a variable named pair.
  • From the Entry object named pair, I get the Stock object by calling the .getValue method. I get the key (that is, the stock code) by calling the .getKey method.
  • From inside the Stock object I get another object called a StockQuote by calling the Stock's .getQuote method.
OK, let's take a breath and recap, because even from this distance I can feel one or two heads spinning.
  • The Yahoo Finance library has given us a collection of Stock objects in the form of a hashmap. This consists of a key (unique identifier), which is a String representing the stock code, and a value, which is a Stock object.
  • Those two combined represent an Entry object.
  • We're using an Iterator object to get each Entry in turn, then pulling out the Stock object into a variable, and the key value (the stock code used on that stock exchange) into another String variable.
  • The Stock object itself has some information about the stock listing, but one of the Stock object's fields is another object called StockQuote which has all of the prices that we're after. So we put that into a variable as well.
Is the dizziness passing?

Now, the StockQuote object obviously has a number of fields of its own. If we're in debug mode we'll get some of those by using methods like .getBid to punt them out to the console.

But the one that we're really interested in here is called .getLastTradeTime which returns a Calendar object, like the one that we saw in the LogWriter class. (That's why I used the Calendar object there as well.) However this time instead of the current time, the Calendar object will be populated with the date and time of the last trade.

I extract a String to get a timestamp string similar to the way I did in the LogWriter class. But why, why do I need it?

I don't.

If you pass an invalid code to the Yahoo Finance library, one that represents a stock that does not exist, it will not return an Exception. It will instead return a Stock object, but it will be a Stock object that contains rubbish data. And which includes a null for the LastTradeTime value in its StockQuote property.

Note that I wrapped the code inside the while loop in its own try/catch block. That's because if one of the Stock object contains invalid data I don't want to bounce straight back out to main() and quit; instead I want to log the problem, remove the object from the Map and continue on. Essentially the while loop is simply a way of validating the data that I receive.

If the LastTradeTime is invalid then my attempt to extract that string will result in a NullPointerException; that is, the object points to a null. The first catch block looks only for that exception. (Any other Exception is something that I don't expect, and will just flow out to the outer try/catch block to be logged and returned to the main() method.)

If the NullPointerException occurs then the catch block will write the details to the log, then remove that Entry from the Map.

Very important: You may be tempted to remove it from the Map using the syntax that I have put in commented out; that is, by calling the Map's own .remove method. (Remembering that "stocks" is the variable that holds our Map.) Under no circumstances should you do that. It will appear to work, but the next time you try to iterate through the Map you'll get an error. You need to do the removal using the Iterator's own .remove method as shown.

After we've iterated through the Map I do one final test of the Map's size (just in case all of the Stock objects were invalid and needed to be removed). If that's zero I throw an Exception. (I won't say "don't do that" again; by now you should know that this is for simplicity and that you should use a specific Exception object.) Otherwise the Map, in the form of the variable named stocks, is passed to the return statement and sent back up to the main() method.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 22 - Making A Connection

We now return to the main() method. Now that we have some data to load to the TM1 server (specifically, the Map object stored in the stocks variable), it's time to connect to the TM1 Server.

This is some of the earliest code that I wrote for the example and if I had my time over again I probably wouldn't have put that code into its own class, much less its own package. But at the time it seemed a useful way to demonstrate creating an object and calling its methods.

Code: Select all

		if (bContinue){
			TM1Connector oTM1Conn = new TM1Connector();
			try {
				oTM1Bean = oTM1Conn.getTM1Bean(gsAdminServer);
				oTM1Server = oTM1Conn.getServerConnection(oTM1Bean, "RPO", "Admin", "apple");
				if(oTM1Server.isError()){
					bContinue = false;
					goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": " + oTM1Server.getErrorMessage());
					goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": Unable to do anything without getting a reference to the server; aborting.");
				}

			} catch (Exception e) {
				bContinue = false;
				goLogWriter.outputToLog(e.getMessage());
				goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": Failed to connect to server; aborting.");
			}
		}
The code block is only executed if there was no error previously and bContinue is true.

We begin by creating a new TM1Connector object and assigning it to a variable, which in this case I call oTM1Conn. That of course necessarily involves doing an import statement for that class, but as you should know by now Eclipse can deal with that for you.

Let's go to the code of the TM1Connector...

Code: Select all

package com.yourcompany.tm1writer;

import com.applix.tm1.TM1Bean;
import com.applix.tm1.TM1Server;

public class TM1Connector {

	/**
	 * Create a TM1Bean and assign an Admin Host to it.
	 * @param adminHostName
	 * @return
	 * @throws Exception 
	 */
	public TM1Bean getTM1Bean(String adminHostName) throws Exception{
		
		TM1Bean oTM1Bean = new TM1Bean();
		
		oTM1Bean.setAdminHost(adminHostName);
		oTM1Bean.refreshServers();
		// This is the SSL port.
		oTM1Bean.setAdminPort(5498);
		
		int iServerCount = oTM1Bean.getNumberOfServers(); 
		if ( iServerCount == 0){
			throw new Exception(
					"There are no servers for the admin host specified.");
		}
		else{
			return oTM1Bean;
		}				
		
	}

	/**
	 * Returns a connection to the TM1 server using mode 1 login.
	 * @param beanMain
	 * @param serverName
	 * @param serverLogin
	 * @param serverPassword
	 * @return
	 * @throws Exception
	 */
	public TM1Server getServerConnection(
			TM1Bean beanMain, String serverName, 
			String serverLogin, String serverPassword) throws Exception{

		if ( serverName.equals("")){
			throw new Exception("The serverName argument cannot be empty.");
		}
				
		TM1Server oTM1Server = beanMain.openConnection(serverName,
				serverLogin,serverPassword);

		if ( oTM1Server.isError() ){			
			throw new Exception("Error connecting to server " + serverName 
					+ ". Error: " + oTM1Server.getErrorMessage());			
		}

		// getCubeCount is a value capsule. You therefore need to 
		// extract that value via the getInt method.
		if (oTM1Server.getCubeCount().getInt()==0 ){
			throw new Exception("There are no cubes on the server " 
					+ serverName + ".");						
		}		
		
		return oTM1Server;
	}	
}
TM1Connector does not have any explicitly defined constructor methods. As previously mentioned in the real world I'd include one even if it did nothing, just to make the point that it did indeed do nothing. But then, in the real world this class wouldn't exist. Or if it did the methods would be static.

Mr. (TM1) Bean
The TM1Bean class (obviously imported from the TM1 Java API library named com.applix.tm1) is the cornerstone of your TM1 connection. It serves roughly the same purpose as the user handle in the classic API. In that you would call:

Code: Select all

'Gets the user handle, a reference to your current TM1 session.
hUser = TM1SystemOpen()

'And you would then set the Admin Host name by referring to the user handle.
TM1SystemAdminHostSet hUser, "AdminHostName"
In Java, you use the new keyword to create the bean:

Code: Select all

TM1Bean oTM1Bean = new TM1Bean();
The next thing you do, which should feel familiar in light of the above, is to pass in the Admin Host's name. In my case the main() function passed this to the .getTM1Bean method. (Also in my case, the admin host was on the box that I was developing on... most of the time at least. I cut out the code for when I was working on other machines because it added no value. You would of course need to find a way to pass this to your code, whether it be by .ini file, Registry, argument or whatever.) You do this by calling the .setAdminHost method.

Code: Select all

oTM1Bean.setAdminHost(adminHostName);
In the classic API the next thing you should do is refresh the server list for the Admin Host that you just set, and hey, guess what, in Java...

Code: Select all

oTM1Bean.refreshServers();
I'm not sure whether this is essential, but I highly recommend it; that's setting the Admin Port. The SSL one is 5498. I do have the non-SSL one buried in my notes somewhere but in the interests of not encouraging bad habits I'm not going to go looking for it.

Code: Select all

oTM1Bean.setAdminPort(5498);
OK, now we should be all set up to have a chat to the Admin Host about what's on offer. And of course there is a method to do that. Let's determine how many servers there are:

Code: Select all

int iServerCount = oTM1Bean.getNumberOfServers(); 
If that's zero then we have a problem, and an exception is thrown.

Important thing to note here: The .getNumberOfServers() method returns an integer, which is why I can use it directly in the above assignment. This is not true of a lot of .getNumberOfWhatever or .getWhateverObjectCount methods, which often return value capsules in the form of a TM1Val object. The "true" value then needs to be obtained. As we'll see, this is typically done by calling the TM1Val object's .getInt (or similar) method. We'll see this later.

Now, I just realised that there is something that I didn't demonstrate in the example code (I did originally but then took it out thinking that I had demonstrated it elsewhere, which I hadn't), and it's how to loop through a TM1 object collection. So while this isn't in the code above, you wanna loop through the servers and print their names out to the console? Sure, here's how you do it:

Code: Select all

for (int i = 0; i < iServerCount; i++) {
	System.out.println(oTM1Bean.getServerName(i));			
}
Note that we start at 0, and run while it is less than the server count since it's a zero based system.

Who, me, sitting here wearing an evil grin? Why would I be doing that?

You'll see shortly. Just sear into your brain for a moment that that collection was zero based.

(Oh, alright, you've already figured out that this isn't consistent, haven't you?)

Feeling The Connection (the getServerConnection method)
Now it's time to connect to a specific server. I'm using mode 1 security, so the core function that I'll be using is the .openConnection method from the TM1Bean object. There are however a bunch of different connection methods, and if you aren't on mode 1 I suggest taking a look at the JavaDocs that I referred to at the foot of Part 18. Find the TM1Bean object entry in the object list, then take a look at all of the "Connection" methods available.

In my case the server name, client and password are hard coded, but once more, in real life that would be something that you would want to come up with a way to feed to the application. (.ini file, Registry, parameter, some other means, and it would be worth looking into the encryption classes of Java (which would be way, way too far off topic to discuss here) to avoid having the password passed in plain text.)

To make the connection you simply pass all of the relevant values to the TM1Bean object's .openConnection method (in my case) like so:

Code: Select all

TM1Server oTM1Server = beanMain.openConnection(serverName, serverLogin,serverPassword);
Now, you may or may not remember that way, way back when in this article I mentioned that if something goes wrong (wrong password, mistyped user name, whatever) TM1 API objects don't generally throw Exceptions. Instead they have .isError fields that you should check after you perform any function that gets or sets a value in the object.

.isError is a simple Boolean; it does not tell you what the error is, simply that one exists. One way of checking the error description is via the .getErrorMessage method which is shown below:

Code: Select all

if ( oTM1Server.isError() ){			
	throw new Exception("Error connecting to server " + serverName 
	+ ". Error: " + oTM1Server.getErrorMessage());			
Now not being an exceptionally trusting individual, I don't trust the absence of an error as definitive so I check that there are cubes on there as well:

Code: Select all

// getCubeCount is a value capsule. You therefore need to 
// extract that value via the getInt method.
if (oTM1Server.getCubeCount().getInt()==0 ){
	throw new Exception("There are no cubes on the server " + serverName + ".");			
As the comments note, the value that comes back from .getCubeCount is a value capsule. (As opposed to the .getNumberOfServers from the TM1Bean which comes back as an int.) Specifically it's a TM1Val object. That means that you can use the methods of that object, including .getInt to suck the number out. The class documentation referred to at the foot of Part 18 provides you with information about the data type returned by each method.

Oh, you want to loop through the cubes? Sure, we can do that. We could try this:

Code: Select all

for (int i = 0; i < oTM1Server.getCubeCount().getInt(); i++) {
	System.out.println(oTM1Server.getCube(i));
}
Except that for cube zero you would get:

Code: Select all

Error: ObjectPropertyNotList
And you would be missing the last cube in the list, which you may not notice because in this output the system cubes are listed last unlike in Server Explorer where they simply should be listed last but aren't. ( See: http://www.tm1forum.com/viewtopic.php?f=19&t=11888 )

The solution is of course to tweak the for loop like so:

Code: Select all

for (int i = 1; i <= oTM1Server.getCubeCount().getInt(); i++) {
	System.out.println(oTM1Server.getCube(i));
}
Though in a production application I would probably put the value of oTM1Server.getCubeCount().getInt() into a variable and use that as the second argument to the for condition to avoid hammering the methods constantly.

Anyway, the key point is that in Java everything is zero based... err, except when it isn't.

We'll see another example of this when it comes to creating a TM1Val array for a view that we can use to update values.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 23 - If You Build It, They Will Come: Creating Dimensions And Cubes

As I said earlier it's questionable whether you would include code to build dimensions and cubes into what is predominantly a data loader application in a production environment. However it's been done here to show how more than anything else. The call from main() is:

Code: Select all

      if(bContinue){
         try {
            TM1StockCubeCreator.createStockDataCube(oTM1Server, goLogWriter,iDebug);
         } catch (Exception e) {
            bContinue = false;
            goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": " + e.getMessage());
            goLogWriter.outputToLog(SC_PROCEDURE_NAME + ": " + "Error occurred when creating cube; aborting.");
         }         
      }
That is, we call the static method .createStockDataCube from the TM1StockCubeCreator class, passing in the TM1 server object that we're connected to (oTM1Server), the goLogWriter object, ad the iDebug value.

The createStockDataCube Method

In the TM1StockCubeCreator class, two constants are declared that we'll use in the method:

Code: Select all

public static final String SC_NAME_STOCKCUBE = "StockPriceCube";
public static final String SC_NAME_UPLOADTOTALELEMENT = "Total Loads";
The following two variables are declared in the procedure:

Code: Select all

String sErrMsg = "";
String SC_PROCEDURE_NAME = "TM1StockCubeCreator";
We begin inside a try block with:

Code: Select all

TM1Cube oTM1Cube = oTM1Server.getCube(SC_NAME_STOCKCUBE);
			
if(oTM1Cube.isError()){
// Etc
So we use the server object's .getCube method to try to get the cube that we're writing to. As I mentioned previously there are two overloaded forms of this method name, one accepting an integer (which we used to loop through the cubes previously) and one accepting a String (representing the name of the cube).

This is followed by an if() test, with the condition checking whether the cube's .isError method returns true. If it doesn't, meaning that there is no error because the cube is there and we now have a reference to it, then there is no code to execute; the method just exits returning true. Obviously there's no point in creating the cube if it's already there.

If there is an error, though, we need to know what kind of error it is. There is a method for TM1 objects named .getErrorCode which returns one of the values defined in the TM1ErrorCode class. We need to determine whether it's the ObjectPropertyNotList one which indicates that the object that we tried to get doesn't exist; that it's "not in the list":

Code: Select all

// This error is OK; we just need to create it.
if(oTM1Cube.getErrorCode()==TM1ErrorCode.ObjectPropertyNotList){
    //Etc
The else condition for that if() is to throw a new Exception object back; the cube has an error but we can't anticipate what type it is. Granted there are other types that we could check for like security access. If you type TM1ErrorCode followed by a period, a humongously long list pops up:
000580_TM1ErrorCodes.jpg
000580_TM1ErrorCodes.jpg (119.77 KiB) Viewed 19557 times
However I cannot think why security would be an issue because non-Admins should not be running code to create cubes in the first place. And I can't think of any others that are worth specifically processing, so the Not In List one is the only one that I'm looking for. Any others just get logged and bounced back to main().

If the not in list error occurs then we move into a new try/catch block and call an internal process to try to create the dimensions of the cube. That process is passed the same arguments as the createStockDataCube method itself is:

Code: Select all

createStockDataDimensions(oTM1Server, goLogWriter, iDebugMode);
Private Method createStockDataDimensions
This is the complete code for that method:

Code: Select all

	/**
	 * 
	 * @param oTM1Server
	 * @return The number of dimensions created.
	 * @throws Exception
	 */
	private static int createStockDataDimensions(TM1Server oTM1Server, 
			LogWriter goLogWriter, int debugMode) throws Exception{
		
		String sDimName = "";
		String sEltDeft = "";
		String sErrMsg = "";
		int i = 0;
		int iReturn = 0;
		String SC_PROCEDURE_NAME = "createStockDataDimensions";

		// You may think that you set the value of a Real using setReal(double val).
		// But if you do that and pass in a constant, the application doesn't merely
		// error, it heads for the Bermuda Triangle and vanishes.
		// Consequently we'll force a double (the Java equivalent of Real) and pass
		// that to the constructor method TM1Val(java.lang.Double val) 
		double dblWeight = Double.valueOf(1).doubleValue();
		
		//If this cannot be completed, we need to throw an exception back
		// to the calling process to stop it from trying to create the cube.
		
		// For consistency's sake I'll make this 0 to 5 given that
		// the array of these elements needs to be zero based for the
		// ValArray used in creating the cube.
		for (i = 0; i <= 5; i++) {
			switch (i) {
			case 0:
				sDimName = "SP_Exchange";
				sEltDeft = "No Exhange";
				break;
			case 1:
				sDimName = "SP_StockCode";
				sEltDeft = "No StockCode";
				break;
			case 2:
				sDimName = "SP_DateCode";
				sEltDeft = "2000-01-01";
				break;
			case 3:
				sDimName = "SP_TimeCode";
				sEltDeft = "EOD";
				break;
			case 4:
				sDimName = "SP_UploadTime";
				sEltDeft = "Latest";
				break;
			case 5:
				sDimName = "SP_Measure";
				sEltDeft = "Volume";
				break;
			} // End switch block
			
			if(debugMode!=0){System.out.println("Initiating loop " + i + ", Dim " + sDimName + ", Element " + sEltDeft);}
			
			try {
				TM1Dimension oTM1Dimension = oTM1Server.getDimension(sDimName);

				if(debugMode!=0){System.out.println("Getting Dim object " + sDimName);}
				
				if(oTM1Dimension.isError()){

					// This is OK, we need to create it.
					if(oTM1Dimension.getErrorCode()==TM1ErrorCode.ObjectPropertyNotList){

						// Do the creation and check the result.
						if(debugMode!=0){System.out.println("Creating Dim object " + sDimName);}
						oTM1Dimension = oTM1Server.createDimension();
						if (oTM1Dimension.isError()){
							sErrMsg = SC_PROCEDURE_NAME + ": Failed to create " + sDimName + "; Error " + oTM1Dimension.getErrorMessage();
							// Critical error.
							throw new Exception(sErrMsg);
						}
						
						// Create the default element.
						if(debugMode!=0){System.out.println("Creating Element " + sEltDeft + " in Dim object " + sDimName);}
						TM1Element oTM1Element;
						oTM1Element = oTM1Dimension.insertElement(TM1Element.NullElement, sEltDeft, TM1ObjectType.ElementSimple);
						if (oTM1Element.isError()) {
							sErrMsg = SC_PROCEDURE_NAME + ": Failed to insert element " + sEltDeft + " into dimension " + sDimName;
							// Also a critical error. We'll try to kill the dimension so that it doesn't clutter up memory.
							try {
								//You "destroy" an unregistered object, but you "delete" a registered one.
								//Unless of course it's a TI function like CubeDestroy which deletes registered cubes.
								oTM1Dimension.destroy();
							} catch (Exception e) {
								// Hey, we tried... I find that with the TM1 API e.getMessage is often NULL
								// so I prefer to go for .toString 
								sErrMsg = sErrMsg + ". Also failed to destroy temporary dimension object: " + e.toString();
							}
							goLogWriter.outputToLog(sErrMsg);
                            throw new Exception(sErrMsg);
						} // End of if() checking insertion of element
						if(i==4){
							try {
								if(debugMode!=0){System.out.println("Creating consolidation element " + SC_NAME_UPLOADTOTALELEMENT + " in Dim object " + sDimName);}
								TM1Element oTM1EltConsol = oTM1Dimension.insertElement(TM1Element.NullElement, SC_NAME_UPLOADTOTALELEMENT, TM1ObjectType.ElementConsolidated);
								// Errors relating to the consolidation aren't critical, just annoying
								if( oTM1EltConsol.isError()){
									sErrMsg = SC_PROCEDURE_NAME + ": Error creating the consolidation element " + SC_NAME_UPLOADTOTALELEMENT
									  + " in dimension " + sDimName + ": " + oTM1EltConsol.getErrorMessage();
									goLogWriter.outputToLog(sErrMsg);
								}else{
									TM1Val oValBoolResult = new TM1Val();
									
										if(debugMode!=0){System.out.println("Adding element to the consolidation in Dim object " + sDimName);}
										oValBoolResult = oTM1EltConsol.addComponent(
												oTM1Element, new TM1Val(dblWeight));						

										if(debugMode!=0){System.out.println("Testing whether element was added successfully to the consolidation in Dim object " + sDimName);}
										if(!oValBoolResult.getBoolean()){
											sErrMsg = SC_PROCEDURE_NAME + ": Error inserting element " + sEltDeft
													+ " into consolidation element " 
													+ SC_NAME_UPLOADTOTALELEMENT  + " in dimension " + sDimName;  
											goLogWriter.outputToLog(sErrMsg);
										}else{
											if(debugMode!=0){System.out.println("No error adding to the consolidation in Dim object " + sDimName);}											
										}
									}
									//TODO: Set the sort order property for this dimension.
									//Optional, since the MDX subsets take care of this.
							} catch (Exception e) {
								if(debugMode!=0){System.out.println("In error block for adding element to the consolidation in Dim object " + sDimName);}
								sErrMsg="Error inserting the consol element for " + sDimName 
										+ " dimension; " + oTM1Dimension.getErrorMessage();
								goLogWriter.outputToLog(sErrMsg);
							}
						} // End if i=4, the creation of the consolidation for SP_UploadTime

						if(i==5){
							oTM1Dimension.insertElement(TM1Element.NullElement, "Day Low", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Day High", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Day Close", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Day Open", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Last Bid", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Last Offer", TM1ObjectType.ElementSimple);
							oTM1Dimension.insertElement(TM1Element.NullElement, "Last Price", TM1ObjectType.ElementSimple);						
							oTM1Dimension.insertElement(TM1Element.NullElement, "Last Volume", TM1ObjectType.ElementSimple);
						} // End i=5, extra element insertion onto SP_Measure

						// When I didn't do the assignment on the .register method line (that is, 
						// when I called the method without assigning the return value to anything) 
						// in the early days of development, the last dim wasn't created. It's possible
						// that the reassignment is needed to prevent the unregistered dim being held onto.
						// (Aside from which I needed to assign the result to a TM1Dimension variable 
						//  to do the isError check anyway.)
						if(debugMode!=0){System.out.println("Checking consistency of Dim object " + sDimName);}
						if ( oTM1Dimension.check().getBoolean() ){
							oTM1Dimension = oTM1Dimension.register(oTM1Server, sDimName);
							if(oTM1Dimension.isError()){
								sErrMsg = SC_PROCEDURE_NAME + ": Error registering the " + sDimName + " dimension; " + oTM1Dimension.getErrorMessage();
								throw new Exception(sErrMsg);
							} else{
								iReturn++;
								if(debugMode!=0){System.out.println("Created the " + sDimName + " dimension loop " + i);}	
							}
						}else{
							sErrMsg = SC_PROCEDURE_NAME + ": Dimension " + sDimName + " failed its consistency check; " 
									+ oTM1Dimension.getErrorMessage();
							goLogWriter.outputToLog(sErrMsg);
							throw new Exception(sErrMsg);							
						} // End of nre dimension consistency .check block.
					}else{
						sErrMsg = SC_PROCEDURE_NAME + ": Error accessing dimension " + sDimName + oTM1Dimension.getErrorMessage();
						goLogWriter.outputToLog(sErrMsg);
						throw new Exception(sErrMsg);
					} //End of check whether the error was a Not In List type. 
				}else{
					if(debugMode!=0){System.out.println("Dim object " + sDimName + " was successfully retrieved.");}
				} // End of if/elseif checking whether the object had an error.

			} catch (Exception e) {
				sErrMsg = SC_PROCEDURE_NAME + ": Error accessing dimension " + sDimName + ". " + e.toString();
				goLogWriter.outputToLog(sErrMsg);
				throw new Exception(sErrMsg);
			}
		} // End of for loop.
		return iReturn;
	}
So what is it doing?

First, we need a value which allows us to specify the default element weighting of 1 for any consolidations. That weight should be a TM1 "Real" value which is of course the type of floating point value that is used to store N element values in a TM1 cube. (As I said in the inline comments, beware of the setReal method, whose presence may lead you down the wrong path here.) However Java knows nothing of TM1 or its Real values, so we have to use the Java equivalent; specifically, the primitive type double. The problem is that 1 by itself is not something that Java would think of as a double (it would typically treat a constant of 1 as an int), so we need to force the issue.

We declare the variable dblWeight as a variable with a data type of double. Then we call upon the double data type's helper class; you may recall that I said that each primitive data type has a corresponding complex object (its "helper class") which contains a bunch of methods to manipulate values. Accordingly I call the Double class, tell it that I want a .valueOf 1 using the method of the same name, and that I emphatically want it as a .doubleValue() by calling that method:

Code: Select all

double dblWeight = Double.valueOf(1).doubleValue();
Most of the new dimensions will have a single element (to begin with, at least), and there will be 6 of them. There's therefore no point repeating the same code 6 times; instead we loop through it, feeding in different variable values each time. To honour Java's "zero based" inclinations I run the counter from 0 to 5:

Code: Select all

for (i = 0; i <= 5; i++) {
	switch (i) {
	case 0:
		sDimName = "SP_Exchange";
		sEltDeft = "No Exhange";
		break;
	case 1:
		sDimName = "SP_StockCode";
		sEltDeft = "No StockCode";
		break;
	//Etc
The for() loop we've seen a few times already but I think this is the first time we've met the switch block in Java aside from one "as an aside" mention of a change to it that came in with Java 7. It's the approximate equivalent to the Select Case block in VBA but with a substantial difference. In this one I'm populating two variables in each loop depending on the counter number; the name of the dimension and the name of the element (the default element) that I'll be inserting. After each case block's code, though, I have to add a break; statement. If you fail to do that then if case 0 is true, the code will keep flowing down and executing case 1's block then case 2's block then case 3's block (etc). What would end up happening is that on each of the 6 loops I'd be trying to create the dimension name and element specified in case 5. It's a really stupid design that Java inherited from the C language. Thankfully the code template in Eclipse comes with the break; statements built in.

A couple of other things to be aware of with switch:
  • Naturally the code for a switch block needs to be enclosed in braces after the switch(i) test, where the control value (i in this case) is contained inside round brackets. (As you can see, Java is reasonably consistent about this type of structure in its various control statements.) With a number of the code blocks I've used the technique that I referred to earlier of using an in-line comment after the closing brace of each substantial block of code, so that we have some idea of where we are.
  • Prior to version 7 you could only use numerics for the case tests. In version 7 onwards you can use Strings. You shouldn't be targeting Java version 6 any longer but I give you fair warning just in case.
  • There is a default: case that can be used if none of the other options apply, though I won't be using it here.
The next bit should be familiar enough. Inside a try block, we use the .getDimension method of the TM1Server class object to try to get a reference to the dimension, just as we used a similar syntax to try to get a reference to the data cube. We're trying to get a reference to the dimension for the same reason; that is, to see whether it exists:

Code: Select all

try {
	TM1Dimension oTM1Dimension = oTM1Server.getDimension(sDimName);
	//Etc
We then check the .isError method of the resulting object, and again check whether it's a Not In List error.

Code: Select all

if(oTM1Dimension.isError()){

	// This is OK, we need to create it.
	if(oTM1Dimension.getErrorCode()==TM1ErrorCode.ObjectPropertyNotList){
	//Etc
If it's any other type of error then we log it, and throw an Exception back... you get the idea by now. And if oTM1Dimension.isError() is false, meaning that there's no error at all (and therefore meaning that the dimension already exists), we just skip on to the next one. The only else code that's executed in such a case is a debug statement. But that's of no value to us as a learning tool, so let's continue with the assumption that the dimension was not in the list. What do we do then?

We call the TM1Server object's .createDimension method, and assign the result to a variable that we've declared as a TM1Dimension type.

Code: Select all

oTM1Dimension = oTM1Server.createDimension();
Note that no name is specified here. This dimension is what's known as an unregistered dimension. There's space made for it in memory, but it has no name yet and only you and the server know of its existence. The server will not make it publicly known until you successfully register it, at which time it will be given a name.

Obviously we check the new dimension for any errors:

Code: Select all

if (oTM1Dimension.isError()){
	sErrMsg = SC_PROCEDURE_NAME + ": Failed to create " + sDimName + "; Error " + oTM1Dimension.getErrorMessage();
	// Critical error.
	throw new Exception(sErrMsg);
	// Etc
There are no fixable errors that I can think of here, so we abort the whole process and spit an Exception back up the chain.

Otherwise, our new dimension needs some elements. We begin by declaring a variable which will hold a reference to the element that we create. I could of course have done this earlier in the code, but this variable doesn't have to live for very long and, more importantly, re-declaring the variable here ensures that we don't have anything left over from a previous loop. I'm not initialising it with any value; there's no point:

Code: Select all

TM1Element oTM1Element;
We then call the .insertElement method of our new unregistered dimension to insert a new element.

Code: Select all

oTM1Element = oTM1Dimension.insertElement(TM1Element.NullElement, sEltDeft, TM1ObjectType.ElementSimple);
We need to supply three arguments:
  • The element that we want to insert the new element ahead of. Not just the name of it; this needs to be a variable holding another TM1Element object. In this case we don't have one, so we use the TM1Element class' .NullElement field. That allows us to say that we don't want to specify an element, as a result of which the new one will go to the first position in the dimension.
  • The name of the new element as a String. This is a variable that was populated in the switch block earlier.
  • The type of the element. This needs to be specified as one of the constant values supplied by the TM1ObjectType class, in this case .ElementSimple. (Which, of course, is one of the many aliases for "an N element".)
Then we test whether the element was inserted correctly. In this case if it didn't we want to at least try to get rid of the temporary dimension as well. (There's no point in keeping the dimension if it has no elements.) Don't confuse the .destroy method, which is used on unregistered objects, with .delete, which is used on registered ones. (As I mentioned in the comments, this distinction isn't followed consistently in TM1 given that "destroy" is used in the TI functions to get rid of an existing, registered object on the server.) You can check this in the documentation if in doubt. Both the .destroy and .delete methods are inherited from the TM1Object class.

Code: Select all

if (oTM1Element.isError()) {
	sErrMsg = SC_PROCEDURE_NAME + ": Failed to insert element " + sEltDeft + " into dimension " + sDimName;
	// Also a critical error. We'll try to kill the dimension so that it doesn't clutter up memory.
	try {
		//You "destroy" an unregistered object, but you "delete" a registered one.
		//Unless of course it's a TI function like CubeDestroy which deletes registered cubes.
		oTM1Dimension.destroy();
	} catch (Exception e) {
		// Hey, we tried... I find that with the TM1 API e.getMessage is often NULL
		// so I prefer to go for .toString 
		sErrMsg = sErrMsg + ". Also failed to destroy temporary dimension object: " + e.toString();
	}
	goLogWriter.outputToLog(sErrMsg);
                            throw new Exception(sErrMsg);
} // End of if() checking insertion of element
A couple of the dimensions need some additional elements; one needs a consolidation and the other a few extra N elements. We use an if() block to identify the number of the dimension; we could have also checked against the dimension name of course.

The same .insertElement method is used. The only difference (aside from using a "constant" for the name) is that in this case we use the .ElementConsolidated value from the TM1ObjectType class.

Code: Select all

if(i==4){
	try {
		TM1Element oTM1EltConsol = oTM1Dimension.insertElement(TM1Element.NullElement, SC_NAME_UPLOADTOTALELEMENT, TM1ObjectType.ElementConsolidated);
If the consolidation element can't be created... eh, who cares? We'll log it but it's not something to worry about.

Code: Select all

if( oTM1EltConsol.isError()){
	sErrMsg = SC_PROCEDURE_NAME + ": Error creating the consolidation element " + SC_NAME_UPLOADTOTALELEMENT
	  + " in dimension " + sDimName + ": " + oTM1EltConsol.getErrorMessage();
	goLogWriter.outputToLog(sErrMsg);
If there is no error in creating the consolidation element, we need to add the N element that we created earlier to that consolidation. This is done through the .addComponent method of the TM1Element class; specifically that method of our newly minted consolidation element. We need to pass in:
  • A TM1Element object representing the element that we are going to add to the dimension; and
  • A TM1Val object that has the weighting. We'll create the TM1Val object on the fly using the new keyword. And we'll pass the value that we want the resulting TM1Val object to have to the class' constructor method. (If you look at the documentation for the TM1Val class, you'll see that it has constructor methods with a huge range of arguments.) As mentioned previously, we need to pass it a "real" value, which means a double.
The method will return another TM1Val value capsule object, so we declare a variable first (oValBoolResult) to hold that result.

To check the result we call the TM1Val class' .getBoolean method. See how we're using the bang (!) operator in the if() block? That means that we execute that block if the oValBoolResult value is not true. (That is, if the result of the operation is false.) Again we don't care that much and just log the error.

Code: Select all

}else{
	TM1Val oValBoolResult = new TM1Val();
	
		oValBoolResult = oTM1EltConsol.addComponent(
				oTM1Element, new TM1Val(dblWeight));

		if(!oValBoolResult.getBoolean()){
			sErrMsg = SC_PROCEDURE_NAME + ": Error inserting element " + sEltDeft
					+ " into consolidation element " 
					+ SC_NAME_UPLOADTOTALELEMENT  + " in dimension " + sDimName;  
			goLogWriter.outputToLog(sErrMsg);
		}else{
			if(debugMode!=0){System.out.println("No error adding to the consolidation in Dim object " + sDimName);}			
		}
	}
For dimension 5 (the measures dimension) we just add 8 extra N elements. This is really just a repeat of the code that we used to add the first one. I don't check the result of the insertions, but would if this was a production system. I elected not to here because it would just clutter the code rather than provide any further enlightenment given that you've already seen how to do this.

Before you try to register a server you should always run the TM1Dimension class' .check method to confirm that it passes a consistency check. (Such as checking that it has elements, no duplicate elements, etc, etc.) That method returns a TM1Val object which contains a Boolean so we can check the value by calling the TM1Val's .getBoolean method. If that is false, obviously we throw an Exception.

But if the check is passed, we proceed to register the dimension by calling the TM1Dimension object's .register method, passing:
  • A reference to the server that the dimension will be registered on (the oTM1Server variable in our case); and
  • A String representing the name of the object.
The .register method, if successful, will return a reference to the newly registered dimension. This is important; that newly registered dimension is effectively a different object from the unregistered one that you created. So the oTM1Dimension variable on the left of the expression below is not the same as the one on the right; if the registration succeeds, the one that was on the right effectively ceases to exist. I'm using the same variable for both to make sure that the code no longer tries to retain any reference to the old one. (As noted in the comments I did have a problem with the last dimension in the loop registering properly before I did that.)

The last thing to do is check the .isError property of the oTM1Dimension variable which will, of course, be holding the new registered dimension at this point. Then it's the usual; if it's an error then throw an Exception back up the stack (I don't bother trying to .destroy it as I have, in theory, lost the reference to the unregistered dimension anyway and if it fails to register after a check there may be something very, very wrong), but if it works then I simply increment the return value counter by 1. ( iReturn++; )

Code: Select all

if ( oTM1Dimension.check().getBoolean() ){
	oTM1Dimension = oTM1Dimension.register(oTM1Server, sDimName);
	if(oTM1Dimension.isError()){
		sErrMsg = SC_PROCEDURE_NAME + ": Error registering the " + sDimName + " dimension; " + oTM1Dimension.getErrorMessage();
		throw new Exception(sErrMsg);
	} else{
		iReturn++;
//etc
At the end of this process we return the iReturn value indicating the number of new dimensions created though I don't actually do anything with that. (Though obviously I could log it or report it some way if I wanted to.)

Let's Recap
Let's briefly recap the methods that we used here:
  • Dimensions live on servers, so to get a dimension we must obviously use a method of a server object. In this case we use the TM1Server class' .getDimension method.
  • Most TM1 objects, including dimensions, have an .isError field, which we can check to see whether the last action was completed successfully; such as getting a reference to a TM1Dimension.
  • They also have .getErrorCode methods, which we can check for specific error codes that correspond to values in the TM1ErrorCode object. In the case that was illustrated here we are checking for an .ObjectPropertyNotList error to see whether the object that we tried to get actually exists.
  • To create a new dimension we use the TM1Server object's .createDimension method. There are of course other .createXxx methods for other objects. The objects created by such methods are unregistered and have no name until you register them. You should again check the .isError field to confirm that this has worked.
  • Elements live in dimensions, so we need a variable containing a TM1Dimension object first. We then use the .insertElement method to create the element, specifying its name and calling one of the values from the TM1ObjectType class to specify what type of element it is. Again this new element's .getError field should be checked. (Notwithstanding that I didn't in some cases, but for a trainer I can get away with it.)
  • You create consolidation elements in the same way but need to specify a different TM1ObjectType. To add components you need to use the consolidation element's .addComponent method, passing a TM1Element instance containing the element that you want to add to it, and a TM1Val object containing a double value stating the new element's weight. This function returns a TM1Val object containing a Boolean value. You use the .getBoolean method of TM1Val to see whether the operation completed successfully.
  • Once the elements have been added you call the new TM1Dimension object's .check method to validate the dimension.
  • Finally you register the object by calling the new object's .register method, passing in a reference to the TM1Server object that you're registering it on, and the name that you want to use for it.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 24 - Building The Cube

Once the createStockDataDimensions method is done creating and registering all of the new dimensions, control returns to the TM1StockCubeCreator method. That will then call another private process named AssignDimensionsToCube, again passing the oTM1Server variable, the goLogWriter variable, and the debug mode variable. That method returns a simple true or false Boolean value to indicate success or failure, which again I don't use but could if I wanted to.

To create a cube we need to define an array of the dimensions that will make up the cube. The first thing that we need is a TM1Val object which holds such an array. It is important to note that we are now talking about a TM1 API array, rather than a Java array. Nonetheless, like a Java array declaration the syntax below declares the number of elements that it will contain (and by "elements", I obviously mean array elements, not dimension elements. The array elements are of course dimensions):

Code: Select all

TM1Val oDimArray = TM1Val.makeArrayVal(6);
In this case the value passed is 6 because that's the number of dimensions in the cube that we'll be creating. Had you written a generic Java procedure to create cubes you would of course have to make that value dynamic.

Since we never have to refer to the index number of the dimensions (unlike the code that I demonstrated for looping through the cubes on a server) I can still use zero base for the loop to add the dimensions to the array, and indeed I do:

Code: Select all

for (i = 0; i <= 5; i++) {

	switch (i) {
	case 0:
		sDimName = "SP_Exchange";
		break;
	case 1:
		sDimName = "SP_StockCode";
		break;
	case 2:
		sDimName = "SP_DateCode";
		break;
	case 3:
		sDimName = "SP_TimeCode";
		break;
	case 4:
		sDimName = "SP_UploadTime";
		break;
	case 5:
		sDimName = "SP_Measure";
		break;
	}
For each case, we of course have to get a reference to the TM1Dimension object of that name. In this case I'm doing this as a one step process, declaring the variable and populating it in a single step. Again this ensures that there is no way that an object reference can be left over from a previous loop. Obviously the method that we use to do this is the .getDimension one from the oTM1Server object instance:

Code: Select all

TM1Dimension oTM1Dimension = oTM1Server.getDimension(sDimName);
Naturally, the .isError field of oTM1Dimension needs to be checked to make sure that the reference is valid.

If everything is OK, we add the dimension to the array using the TM1Val object's .addToArray method:

Code: Select all

oDimArray.addToArray(oTM1Dimension);
.AddToArray has two alternative signatures; one in which you specify the index position of the item to be inserted, and one to just add it to the end. The second of those is the one that we're using here. The .addToArray method returns void, so there is nothing to check.

Cubes live on servers, so as you would expect we need to call a method of the oTM1Server instance to create the new cube. Again I create a variable to hold the returned cube on the fly. And once again this is an unregistered object; it has no name yet:

Code: Select all

TM1Cube oTM1Cube = oTM1Server.createCube(oDimArray);
The new oTM1Cube object's .isError method is checked (of course). If everything is good, we register it:

Code: Select all

oTM1Cube= oTM1Cube.register(oTM1Server, SC_NAME_STOCKCUBE);
And that's it; we have our new dimensions and we have our new cube.

Of course, if the cube already exists then all of the code in the last two parts is skipped over, and we just move on to processing the current data load.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 25 - Updating The Dimensions

There is a possibility that our latest data load will contain elements that we haven't used before, and which therefore don't exist in the dimensions; new stock codes, new stock exchanges, new date and time elements. We need to add them. And we need to do it as quickly as possible because updating dimensions can lead to server locks, which are Not A Good Thing. This process is different to creating a dimension from scratch in the way that we've seen previously, but it shares some similarities.

The Java API uses the "classic" way of updating dimensions, which is to duplicate the dimension, do the changes on the duplicate, and then update the original dimension:
000590_UpdatingADimension.jpg
000590_UpdatingADimension.jpg (64.29 KiB) Viewed 19556 times
I should mention for the sake of completeness that when Hubert Hijkers conducted his session on the new REST API immediately after mine, he mentioned that you can't do that in REST because it is, as the name suggests, stateless. Accordingly each command stands on its own and cannot refer to a temporary dimension. However that is something for the future; it is unlikely to be something that you'll see introduced into the Java API.

All of this is handled by the TM1StockWriter class.

I begin this class by declaring some variables that I use as constants to ensure that, for example, if I ever change the name of a dimension I need only do so in one place. (In reality, I would probably create a large number of constants like these in a separate class so that they could be used throughout the project.)

Code: Select all

// Constant values which can be used in multiple methods.
public static final String gSC_NAME_STOCKCUBE = "StockPriceCube";
public static final String gSC_NAME_UPLOADTOTALELEMENT = "Total Loads";
public static final String gSC_NAME_UPLOADTIMEDIM = "SP_UploadTime";
public static final String gSC_NAME_COUNTRYDIM = "SP_Exchange";
public static final String gSC_NAME_STOCKCODEDIM = "SP_StockCode";
public static final String gSC_NAME_DATECODEDIM = "SP_DateCode";
public static final String gSC_NAME_TIMECODEDIM = "SP_TimeCode";
public static final String gSC_NAME_MEASUREDIM = "SP_Measure";	

// Variable values which can be used in multiple methods.
public String gsSP_UploadTime = "";
There are four principal methods in this class, two of which are public, and two of which are private and called by the public methods:
  • prepareDimensions is used to do the primary update of the dimensions. It iterates through the stocks collection, looking for any elements that don't already exist. One element (the upload time element) will not be in the stocks collection and is therefore created by calling one of the private methods, addUploadTimeElement.
  • writeToCube is the public process that writes the values up to the cube. Since it needs to create a TM1Val array for each write, most of the code for doing so is outsourced to the private process createInputValArray.
The prepareDimensions Method
The first thing it does is to check these stocks Map object to confirm that it has data:

Code: Select all

if (stocks.size() == 0) {
(and obviously throw an Exception if it doesn't), and create TM1Dimension object variables to hold both the registered dimensions, and the duplicate dimensions:

Code: Select all

TM1Dimension oTM1DimCountry = null;
TM1Dimension oTM1DimCountryCopy  = null;
TM1Dimension oTM1DimStockCode  = null;
TM1Dimension oTM1DimStockCodeCopy  = null;
TM1Dimension oTM1DimDateCode  = null;
TM1Dimension oTM1DimDateCodeCopy  = null;
TM1Dimension oTM1DimTimeCode  = null;
TM1Dimension oTM1DimTimeCodeCopy  = null;
Why both? Because as you will see we will need a reference to the currently registered dimension when it comes time to update it from the copy. We may as well get that reference here.

It then gets references (within a try block obviously) to each registered dimension, and creates a duplicate of it:

Code: Select all

oTM1DimCountry = oTM1Server.getDimension("SP_Exchange");
oTM1DimCountryCopy = oTM1DimCountry.duplicate();
oTM1DimStockCode = oTM1Server.getDimension("SP_StockCode");
oTM1DimStockCodeCopy = oTM1DimStockCode.duplicate();
oTM1DimDateCode = oTM1Server.getDimension("SP_DateCode");
oTM1DimDateCodeCopy = oTM1DimDateCode.duplicate();
oTM1DimTimeCode = oTM1Server.getDimension("SP_TimeCode");
oTM1DimTimeCodeCopy = oTM1DimTimeCode.duplicate();
Note that we are doing this for only four of the six dimensions. Obviously there is no need to update the measures dimension, and the new element of the upload time dimension is not driven from the stocks Map but rather from the current time.

Checking The Dimension References
It then does a check to confirm that all eight of the TM1Dimension objects were created correctly. Rather than returning an exception on the first one with an error, the following blocks of code check all eight dimensions, aggregating the error information into a String variable. If the String is not empty at the end of that check then we will throw an Exception:

Code: Select all

if (oTM1DimCountry.isError()) {sMsgErr = sMsgErr + "ExchangeCode dimension error: " + oTM1DimCountry.getErrorMessage();}
if (oTM1DimStockCode.isError()) {sMsgErr = sMsgErr + "StockCode dimension error: " + oTM1DimStockCode.getErrorMessage();}
if (oTM1DimDateCode.isError()) {sMsgErr = sMsgErr + "DateCode dimension error: " + oTM1DimDateCode.getErrorMessage();}
if (oTM1DimTimeCode.isError()) {sMsgErr = sMsgErr + "TimeCode dimension error: " + oTM1DimTimeCode.getErrorMessage();}

if (oTM1DimCountryCopy.isError()) {sMsgErr = sMsgErr + "ExchangeCode dimension copy error: " + oTM1DimCountryCopy.getErrorMessage();}
if (oTM1DimStockCodeCopy.isError()) {sMsgErr = sMsgErr + "StockCode dimension copy error: " + oTM1DimStockCodeCopy.getErrorMessage();}
if (oTM1DimDateCodeCopy.isError()) {sMsgErr = sMsgErr + "DateCode dimension copy error: " + oTM1DimDateCodeCopy.getErrorMessage();}
if (oTM1DimTimeCodeCopy.isError()) {sMsgErr = sMsgErr + "TimeCode dimension copy error: " + oTM1DimTimeCodeCopy.getErrorMessage();}

if (!sMsgErr.equals("")) {throw new Exception(sMsgErr);}
Note that I've used a single line syntax, incorporating the braces immediately after the if() test, purely for space saving reasons.

Four variables are then declared to hold the information from each Stock class as we loop through the collection. Remember that when the stocks Map was first returned, it was checked for invalid Stock objects so we can trust that the members of the Map should be valid.

Code: Select all

String sSP_Exchange = "";
String sSP_StockCode = "";
String sSP_DateCode = "";
String sSP_TimeCode = "";
Getting The Stock Codes; Parsing Strings
Once again, we need an Iterator object to loop through the stocks Map's entry set, and return each Entry. This code is similar to the code that we saw when I first validated the stocks data received, so I won't put any commentary about it here:

Code: Select all

try {
	Iterator<Entry<String, Stock>> it = stocks.entrySet().iterator();
	while (it.hasNext()) {
		
		bContinue = true;
		sMsgErr = "";
		Entry<String, Stock> pair = it.next();
		sKeyCurrent = pair.getKey();
	    Stock stockCurrent = (Stock) pair.getValue();
	    StockQuote sqCurrent = stockCurrent.getQuote();

It is now time to obtain the stock code, and the exchange code. This is done within a try block:

Code: Select all

    try {
	    // ............................................................
    	// First get the stock and country code elements.
    	// For US exchange elements this will be a stock code only.
    	// For any other exchanges it'll have a period followed by
    	// a country or exchange code.
		int iExchangeSeparator = sqCurrent.getSymbol().indexOf(".");
		// Remember that this return is INDEX based, so position 1 will be 0.
		// If there is no separator it will return -1.
		if (iExchangeSeparator < 0){
				sSP_StockCode = sqCurrent.getSymbol();
				sSP_Exchange= "NYSE";
		}else{
				sSP_StockCode = sqCurrent.getSymbol().substring(0,iExchangeSeparator);
				sSP_Exchange= sqCurrent.getSymbol().substring(iExchangeSeparator+1);		        	
		}
		if(sSP_StockCode.equals("")){sMsgErr = sMsgErr + "Failure to read a stock code for value " + sKeyCurrent + ". ";}
		if(sSP_Exchange.equals("")){sMsgErr = sMsgErr + "Failure to read an exchange code for value " + sKeyCurrent + ". ";}
		
		if(!sMsgErr.equals("")){throw new Exception(sMsgErr);}
The structure of the code will be different depending on whether it is a United States stock, or a stock listed on another exchange. The ones listed in the United States (which we will take to be on the New York Stock Exchange for the purposes of this exercise) will have no exchange code; a short alphabetical code which is usually specified after a period.

To determine whether there is a period, we can use the .indexOf method of the String class:

Code: Select all

int iExchangeSeparator = sqCurrent.getSymbol().indexOf(".");
So in this line, we are declaring an integer variable named iExchangeSeparator. We are using the .getSymbol() method of the StockQuote object, which is a member of the Stock object that we're currently on.

That will return something like either BHP.AX (BHP Billiton Ltd) for an Australian code, or BA (Boeing) for a US code. The .indexOf method looks for the period and returns its location. If a period is not found, then unlike the stupid functions in Excel which return an error, the value -1 is returned. Remember that the value cannot be zero because the array of characters which make up a String is considered to have a zero base, and so zero would indicate that the period had been found at the first character.

Consequently if the iExchangeSeparator variable's value is less than zero, then the period was not found and it must be a United States code. Therefore we assign the exchange as NYSE. The stock code itself will be the entire value returned from the .getSymbol method.

Code: Select all

if (iExchangeSeparator < 0){
	sSP_StockCode = sqCurrent.getSymbol();
	sSP_Exchange= "NYSE";
If a period is found, then we need to split out the components into the text before the period (which represents the stock code) and the text after the period (which represents the exchange). Thankfully Java has some powerful and flexible substring functions (far better than the one you will find in rules/TI) to do this for us.

Code: Select all

sSP_StockCode = sqCurrent.getSymbol().substring(0,iExchangeSeparator);
sSP_Exchange= sqCurrent.getSymbol().substring(iExchangeSeparator+1);
In the first syntax I state that I want the string starting at position zero, and running up to but not including the period, and in the second syntax I state that I want the string starting at one position after the period and, since I've omitted any other arguments, running to the end.

If one or both codes is empty, an Exception is thrown. Otherwise, we use the .insertElement method to insert the element into the duplicate dimension. An example of this code is:

Code: Select all

    if(bContinue){
		    		try {
		    		    TM1Element oTM1EltNew =  oTM1DimCountryCopy.insertElement(
		    		      TM1Element.NullElement, sSP_Exchange, 
		    		      TM1ObjectType.ElementSimple);

		    		    if(oTM1EltNew.isError()){
		    		        sMsgErr = "Error inserting exchange code " 
		    		        + sSP_Exchange + " for stock " + sKeyCurrent + ": "
		    		        + oTM1EltNew.getErrorMessage() ;

		    		        throw new Exception(sMsgErr);
		    		    }
		    		} catch (Exception e) {
		    		    // Not a deal breaker. Log it and move on.
		    		    sMsgErr = SC_PROCEDURE_NAME + " error: " + e.toString();
		    		    goLogWriter.outputToLog(sMsgErr);
		    		}
Lather, rinse, repeat for the stock code entry.

Getting The Date And Time Elements
If that's all OK, the next thing is to get the trade time. We know that there should be a valid Calendar class entry in that, because if there hadn't been then the Stock would have been removed from the Map during the validation sweep:

Code: Select all

Calendar calLastTrade = sqCurrent.getLastTradeTime();
sSP_DateCode = "";
sSP_TimeCode = "";
sSP_DateCode = String.format("%04d", calLastTrade.get(Calendar.YEAR)) + "-"
		+ String.format("%02d", (calLastTrade.get(Calendar.MONTH) + 1)) + "-"
		+ String.format("%02d", (calLastTrade.get(Calendar.DAY_OF_MONTH)));

sSP_TimeCode = String.format("%02d", (calLastTrade.get(Calendar.HOUR_OF_DAY))) + ":"
		+ String.format("%02d", (calLastTrade.get(Calendar.MINUTE))) + ":"
		+ String.format("%02d", (calLastTrade.get(Calendar.SECOND)));
Then we try to insert those values into the Date Code and Time Code dimensions, using code that we should be well familiar with by now. The code for the Date Code dimension, for instance, is:

Code: Select all

try {
	TM1Element oTM1EltNew =  oTM1DimDateCodeCopy.insertElement(
	   TM1Element.NullElement, sSP_DateCode, 
	   TM1ObjectType.ElementSimple);

	if(oTM1EltNew.isError()){
		sMsgErr = "Error inserting date code " 
		+ sSP_StockCode + " for stock " + sKeyCurrent + ": "
		 + oTM1EltNew.getErrorMessage() ;
		throw new Exception(sMsgErr);
		}
} catch (Exception e) {
	sMsgErr = SC_PROCEDURE_NAME + " error: " + e.toString();
	   goLogWriter.outputToLog(sMsgErr);
}
And so it goes for the Time Code dimension as well.

Checking And Updating The Dimensions
The last thing to do is a consistency check on each of the dimension copies. This is done using the same .check method that we used on the unregistered dimensions. If everything is OK, you call the .updateDimension method of the currently registered dimension, not of the copy. Rather, you pass a reference to the copy in to that method:

Code: Select all

sMsgErr = "";
try {
	if(oTM1DimCountryCopy.check().getBoolean()){
		TM1Dimension oTM1DimNew = oTM1DimCountry.updateDimension(oTM1DimCountryCopy);
		if (oTM1DimNew.isError()){
			sMsgErr = sMsgErr + "Exchange dimension: " + oTM1DimNew.getErrorMessage();
		}
	} // End consistency check.	
} catch (Exception e) {
	sMsgErr = sMsgErr + "Exchange dimension: " + e.toString() + ". ";
} // End try for dimension update.
The Call To The addUploadTimeElement Method
After the loop to update the four dimensions whose elements come from the stocks Map, prepareDimensions calls the addUploadTimeElement private method. I'm not going to include a lot of code here because in terms of coding principles there's absolutely nothing that we haven't already seen already. The name of the element is a string which is derived from the current time:

Code: Select all

// This will get us a Calendar object populated with the current date.
Calendar cal = Calendar.getInstance();

// Format that as a date string. This will be our new element.
gsSP_UploadTime = cal.get(Calendar.YEAR)
	+ String.format("%02d", (cal.get(Calendar.MONTH) + 1)) 
	+ String.format("%02d", (cal.get(Calendar.DAY_OF_MONTH)))
	+ "_" + String.format("%02d", (cal.get(Calendar.HOUR_OF_DAY)))
	+ String.format("%02d", (cal.get(Calendar.MINUTE)))
	+ String.format("%02d", (cal.get(Calendar.SECOND)));
We call the .getDimension method of the oTM1Server instance (which was passed as an argument, of course) to get a TM1Dimension variable which has a reference to the SP_UploadTime dimension, and we call the .duplicate method of that dimension.

We then:
  • Insert the new element using the duplicate dimension's .insertElement method;
  • Get a reference to the Total Uploads element by using the duplicate dimension's .getElement method;
  • Add the new element to the Total Uploads consolidation by using the consolidation element's .addComponent method, passing in the new element that we've created and a TM1Val object containing a double with the element's weighting (which in this case is zero rather than one);
  • Do a consistency check of the updated dimension duplicate; and finally...
  • Call the original dimension's .updateDimension method.
So, essentially the same sequence of events as applied to the other dimensions.

The reason that I rushed through that last couple of paragraphs isn't because I'm sick of writing this. Okay, maybe it's a little bit of that. But primarily it is to make a point. And that point is that there is a consistency to how things are done not just in Java (or any other modern language like the.Net ones come to that), but also in the TM1 APIs. Once you understand the pattern of doing things, you will find that most other things are done in a fairly consistent way.

Real Life Design Considerations
Is this the best way of doing it? Maybe. On the one hand you are making duplicates of four dimensions regardless of whether you are going to be inserting any new elements into those dimensions or not. You won't know whether you need to until you have iterated through the stocks Map. That means that you may in fact be locking the server unnecessarily. One obvious way around this is to do a primary pass through the stocks and see whether there are any new elements to be added, then only update the affected dimensions.

On the other hand with a small number of stocks this process runs too quickly to time. Is it therefore worthwhile writing all of the code needed to do a primary pass if the lock is infinitesimal anyway? But what if we start doing a download of a massive number of stocks, which may take longer?

As with everything in TM1, this is a case of working out the pluses and minuses that apply to your particular situation and balancing them up to arrive at an optimal conclusion. And, incidentally, what is optimal one year may not be optimal in a later year. The way illustrated here is just one possibility out of many.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 26 - Updating The Values

(Note: Some of the code below still uses references to "Country" rather than "Exchange". I'm not going to bother changing it at this point, just know that the terms are synonymous for the purposes of this exercise.)

Now it's time to start loading the values into the cube. The method to do that is:

Code: Select all

public int writeToCube(Map<String, Stock> stocks, 
	TM1Server oTM1Server, LogWriter goLogWriter, 
	int iDebugMode) throws Exception{
It begins by declaring:
  • Strings to store the element names that are derived from iterating through the stocks Map.
  • A TM1Cube object variable to store a reference to the cube that we will be writing to.
  • A TM1Val object variable which will store an array that defines the element combination that we want to write to.
  • TM1Element object variables for each of the measures in the measures dimension.
  • double values for the values that we will be writing to the cube.

Code: Select all

String sSP_StockCode = "";
String sSP_Exchange = "";
String sSP_DateCode = "";
String sSP_TimeCode = "";

// Reference to the cube.
TM1Cube oTM1CubeStocks = null;

// The array that contains the elements.
TM1Val oTM1ValArray = null;
TM1Element oTM1EltUploadTimeCurrent = null;
TM1Element oTM1EltUploadTimeLatest = null;
TM1Element oTM1EltMeasureUploadPrice = null;
TM1Element oTM1EltMeasureLastOffer = null;
TM1Element oTM1EltMeasureLastBid = null;
TM1Element oTM1EltMeasureVolume = null;

//The values that are contained in the current stock
double dblPrice = 0;
double dblLastOffer = 0;
double dblLastBid = 0;
double dblVolume = 0;
The process verifies that the stocks argument does not have a zero size. (Not that it shouldn't need to by this point, of course, but there's no harm in doing it.)

We then get a reference to the cube, and by this time there should be no surprises in how we go about doing that:

Code: Select all

oTM1CubeStocks = oTM1Server.getCube(gSC_NAME_STOCKCUBE);
if(oTM1CubeStocks.isError() ){
	sMsgErr = SC_PROCEDURE_NAME + ": Error getting a reference to the "
	  + gSC_NAME_STOCKCUBE + " cube. Aborting.";
	throw new Exception(sMsgErr);
}
Some of the elements we will be writing to are of course constant; the upload time elements (one being a timestamp representing the current time, and the other being the element "Latest") will be the same each time, as will all of the measures elements. Accordingly we might as well get references to them right now. (The process that created the timestamp had stored the timestamp string in the variable gsSP_UploadTime for just this reason.)

And again, there should be no surprises in this code save for the fact that we are doing it in one step; that is, we getting a reference to the dimension, then immediately using that reference to get a reference to the element:

Code: Select all

oTM1EltUploadTimeCurrent = oTM1Server.getDimension(
gSC_NAME_UPLOADTIMEDIM).getElement(gsSP_UploadTime);
oTM1EltUploadTimeLatest = oTM1Server.getDimension(
gSC_NAME_UPLOADTIMEDIM).getElement("Latest");

// Get the element references for the measures.
oTM1EltMeasureUploadPrice = oTM1Server.getDimension(
	gSC_NAME_MEASUREDIM ).getElement("Last Price");
oTM1EltMeasureLastOffer = oTM1Server.getDimension(
	gSC_NAME_MEASUREDIM ).getElement("Last Offer");
oTM1EltMeasureLastBid = oTM1Server.getDimension(
	gSC_NAME_MEASUREDIM ).getElement("Last Bid");
oTM1EltMeasureVolume = oTM1Server.getDimension(
	gSC_NAME_MEASUREDIM ).getElement("Volume");
Naturally, we do an .isError check on all of those elements, and throw an Exception if any of them don't exist. That should be nothing more than a precaution, however.

Then it's time to meet our old friend the Iterator object again. Everything in the below fragment of code we have already seen elsewhere:

Code: Select all

try {
	Iterator<Entry<String, Stock>> it = stocks.entrySet().iterator();
	while (it.hasNext()) {

		try {
			sMsgErr = "";
			Entry<String, Stock> pair = it.next();
			sKeyCurrent = pair.getKey();
			Stock stockCurrent = (Stock) pair.getValue();
			StockQuote sqCurrent = stockCurrent.getQuote();
	// etc

From the StockQuote object we can obtain the symbol using the .getSymbol method, then parse out both the exchange name and the stock code as we've seen previously. We can call the .getLastTradeTime() method to obtain a Calendar object and split out the formatted date and time components to obtain those elements, and we can obtain the data that we need to write to the cube through a few simple method calls:

Code: Select all

dblPrice = sqCurrent.getPrice().doubleValue();
dblLastOffer = sqCurrent.getAsk().doubleValue();
dblLastBid = sqCurrent.getBid().doubleValue();
// This comes back as a long integer; Java is OK
// with an implicit conversion here.
dblVolume = sqCurrent.getVolume();
Just one thing on that last call which I don't think I've mentioned previously; if you are writing a value from a smaller data type variable to a larger data type variable, Java will do an implicit conversion of the data. That's what's happening here. If you are going the other way, however, you need to explicitly declare that you are changing the type by using a casting statement that we won't be looking at in this article. In that way you tell Java that you understand the implications and the possibility that your data will be truncated. That of course isn't an issue when going from a smaller type to a larger type such as, in this case, a long integer to a double floating point.

We then need to get TM1Element references to the exchange, stock code, date code and time code elements that we have extracted from the StockQuote object. By now this should be so familiar as to be routine:

Code: Select all

TM1Element oTM1EltCountry = oTM1Server.getDimension(
	gSC_NAME_COUNTRYDIM ).getElement(sSP_Exchange);

TM1Element oTM1EltStockCode = oTM1Server.getDimension(
	gSC_NAME_STOCKCODEDIM ).getElement(sSP_StockCode);

TM1Element oTM1EltDateCode = oTM1Server.getDimension(
	gSC_NAME_DATECODEDIM ).getElement(sSP_DateCode);

TM1Element oTM1EltTimeCode = oTM1Server.getDimension(
	gSC_NAME_TIMECODEDIM ).getElement(sSP_TimeCode);
Now that we've gathered all of the information that we need to write, we need to:
  • Create an array of elements that we need to write to;
  • Call the .setCellValue method of the cube that we're writing to to populate or update the value.
Now we need to do that for both the "Latest" element and the current date and time element of the SP_UploadTime dimension, and we have to do it for four measures elements. Since we have to do essentially the same thing over and over again, rather than repeat the code we'll create two loops to do this; an outer loop to do each of the SP_UploadTime elements, and an inner loop to do each of the measures elements.

The outer loop is dead easy to follow:

Code: Select all

for (int iUploadElement = 0; iUploadElement < 2; iUploadElement++) {

	TM1Element oTM1EltUploadTime = null;
	switch (iUploadElement) {
		case 0:
			oTM1EltUploadTime = oTM1EltUploadTimeCurrent;
			break;
		case 1:
			oTM1EltUploadTime = oTM1EltUploadTimeLatest;
			break;							
		default:
			break;
	}
	// etc
We've previously assigned the element that represents the current timestamp to the variable oTM1EltUploadTimeCurrent, and a reference to the element "Latest" to the variable oTM1EltUploadTimeLatest. Here we simply assign whichever one applies to the current loop to another variable named oTM1EltUploadTime. We can then use that variable in everything that we need to do.

The inner loop to specify the measure element should be equally easy to follow:

Code: Select all

for (int iMeasure = 0; iMeasure <= 3; iMeasure++) {
	String sMeasureElement = "";
	double dblValueToLoad = 0;
	switch (iMeasure) {
	case 0:
		sMeasureElement = "Last Price";
		dblValueToLoad = dblPrice; 
		break;
	case 1:
		sMeasureElement = "Last Offer";
		dblValueToLoad = dblLastOffer; 
		break;
	case 2:
		sMeasureElement = "Last Bid";
		dblValueToLoad = dblLastBid; 
		break;
	case 3:
		sMeasureElement = "Volume";
		dblValueToLoad = dblVolume; 
		break;

	default:
		break;
	}
We pass the name of the measures element that we're writing to, along with variables containing references to all of the other elements, to a private method that constructs the array for us; createInputValArray.

The call to that is:

Code: Select all

oTM1ValArray = createInputValArray(
	oTM1CubeStocks, oTM1EltUploadTime, 
	oTM1EltCountry, oTM1EltStockCode, 
	oTM1EltDateCode, oTM1EltTimeCode,
	sMeasureElement, 
	goLogWriter, iDebugMode);
The createInputValArray Method
When we write the value we need to pass a TM1Val object representing an array of the elements that will be written to. That array is created in this method.

Again the array is declared as being of a size equal to the number of dimensions in the cube... and its elements will run from 1 (not 0) to that number.

As you would expect there is a method of the TM1Cube class that allows us to get the number of dimensions, which we can use since we passed in a TM1Cube object in the form of the oTM1CubeStocks variable:

Code: Select all

iCubeTabDimCount = oTM1CubeStocks.getDimensionCount().getInt();
The .getDimensionCount() method returns a value capsule, so we use the .getInt method to return the actual value.

Now we create a TM1Val array, just as we did when creating the cube:

Code: Select all

oTM1ValArray = TM1Val.makeArrayVal(iCubeTabDimCount);
The one element we don't have at this point is the measure one, so we need to get that. (An alternative approach would obviously have been to store a references to each measure in a set of variables in the calling procedure, and pass the relevant one through with each call the way we've done with the Upload Time element. However part of the intent here is to demonstrate different methods.)

Again this can be done in a single step:

Code: Select all

TM1Element oTM1EltMeasure = oTM1CubeStocks.getDimension(gSC_NAME_MEASUREDIM).getElement(sMeasureElement);
After obviously checking the element's .isError value, it's time to populate the array. This is a simple case of assigning the elements in the appropriate order:

Code: Select all

oTM1ValArray.addToArray(oTM1EltCountry);
oTM1ValArray.addToArray(oTM1EltStockCode);
oTM1ValArray.addToArray(oTM1EltDateCode);
oTM1ValArray.addToArray(oTM1EltTimeCode);
oTM1ValArray.addToArray(oTM1EltUploadTime);
oTM1ValArray.addToArray(oTM1EltMeasure);
The resulting array is returned to the (writeToCube) calling procedure.

Writing The Value
And last of all, we set the value for the array of elements that we've defined, and check the resulting TM1Val capsule for errors.

Code: Select all

TM1Val oTM1ValReturn = oTM1CubeStocks.setCellValue(oTM1ValArray, 
	new TM1Val(dblValueToLoad) );

if(oTM1ValReturn.isError()){
	sMsgErr = "Error setting value: " + oTM1ValReturn.getErrorMessage();
	throw new Exception(sMsgErr);
	//etc
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 27 - Packaging It, Running It

Let's first look at the cube as it was before doing anything:
000595_BeforeLoad.jpg
000595_BeforeLoad.jpg (276.03 KiB) Viewed 19556 times
The last thing to do is to package the project into a form that can be run outside of Eclipse. I'm going to create it as a self-running .jar package, though there are other alternatives which are beyond the scope of this document.
000600_ExportToRunnableJar.jpg
000600_ExportToRunnableJar.jpg (100.78 KiB) Viewed 19556 times
In this case I'm going to write it out to a Scripts folder in the data directory of the server. I'm also going to elect to package the required libraries into the .jar file.
000610_ExportToRunnableJar02.jpg
000610_ExportToRunnableJar02.jpg (131.56 KiB) Viewed 19556 times
With that .jar in place, it's possible to run it from a TI ExecuteCommand statement that calls a command line via cmd /c. In this case I've hard coded the list of stocks, but they could just as easily be read from a cube. Note that one (ABC.AX) is a new stock code (in the sense that we haven't used it before; the company itself (Adelaide Brighton Ltd) dates back to 1882), and another (RIO.AY) is an invalid one. (RIO is Rio Tinto Ltd, but .AY is an invalid exchange code.)
000630_Command.jpg
000630_Command.jpg (103.87 KiB) Viewed 19556 times
So let's run it and see what happens:
000640_AfterLoad.jpg
000640_AfterLoad.jpg (197.49 KiB) Viewed 19556 times
I took a couple of minutes out of my working day to run this while the ASX was still open. This was just on 14:00, so the new entries for 1 September have the expected time of 13:40ish. Note that the new code (ABC = Adelaide Brighton, in green) has been inserted. As this was the first run for that code, we have no prices for it for the dates that the earlier runs (seen in BHP, for example) had been done. (The Yahoo Finance API can provide historical values (but not by minute as far as I know), but I'm not querying those.)

And the invalid RIO.AY code? As we'd expect, in the error log we have:
000650_ErrorLog.jpg
000650_ErrorLog.jpg (71.01 KiB) Viewed 19556 times
That's it, we now have a working application.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 28 - Some More Eclipse Tricks

Editing
  • To duplicate the line you're on:
[Ctrl]+[Alt]+[Down] or [Up], depending on whether you want to duplicate it below or above the current line.
  • To move the current line below or above the next one respectively:
[Alt]+[Down] or [Up]
  • To delete the current line:
[Ctrl]+[D]

Fixing References
As previously mentioned, [Ctrl] + [Space] is probably the most important shortcut in the application. It loads the list of available objects, properties, methods, code templates, etc, depending on context. For a class, it will also either create the Import statement, or offer you a selection of matching classes to choose from.

[Ctrl]+[Shift]+[O] will clean up your references.

If you have, say,

Code: Select all

import com.applix.tm1.TM1Cube;
at the head of your class but have moved any reference to the TM1Cube object in your code, [Ctrl]+[Shift]+[O] will remove it automatically.

Similarly it's possible to declare an Import statement using wildcard characters; for example you could import references to any TM1 objects that you may use by declaring the Import as:

Code: Select all

import com.applix.tm1.*;
I'm told that this has no impact on performance. Maybe, maybe not. But it does make your code more opaque than it needs to be to those who follow you.

Consequently [Ctrl]+[Shift]+[O] will convert this:

Code: Select all

import com.applix.tm1.*;

public class MainCode {

	public static void main(String[] args) {
			
			TM1Server oTM1Svr;
			TM1Cube oTM1Cube;
			
	}
// Etc
Into this:

Code: Select all

import com.applix.tm1.TM1Cube;
import com.applix.tm1.TM1Server;

public class MainCode {

	public static void main(String[] args) {
			
			TM1Server oTM1Svr;
			TM1Cube oTM1Cube;
			
	}
//Etc
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 29 - Summary

Java is syntactically very different from VBA, as we have seen. Yet the raw syntaxes that exist within it are relatively few, and relatively consistent. If blocks, for and while loops all have the keyword followed by a condition in round brackets, followed by a block of code to be executed within curly braces. Switch statements are similar.

Lines of code end with a semi-colon, both methods and classes begin and end with braces.

The hardest (least familiar) thing to get your head around if coming from "some knowledge of VBA" is the syntax relating to generic collections like hashmaps; the diamond operators and the Iterator objects.

But as with all modern languages, the bulk of Java isn't in the language syntax; it's in the methods and classes that are in the core libraries, and in the tens of thousands of third party libraries out there like the TM1 Java API and Yahoo Finance ones. The number of classes is overwhelming. The methods even more so.

Contrary to what some may believe this article and its accompanying talk were never intended to let you "Learn Java Programming". Not only was that never the intent (plainly stated at the start of the talk, incidentally), it was never possible. Because to do that you would need to be across a reasonable range of at least the core libraries, and that's knowledge that will take time to acquire.

However to simply "get something done" you don't need to know all of them, just some of the key ones as this article has demonstrated. It was all about giving you a starting point and a local road map, not an atlas.

The TM1 Java API library is relatively straightforward if you understand what objects exist on a server. The code becomes repetitive and familiar, which is why as we went through I added less and less "whole code" and more fragments with descriptions. In terms of the API library itself, you need only look at the online help to search out the methods and fields that you need. The help is woeful if you need guidance of where to start, but once you know that it will keep you moving in the right direction.

Although It's a distinctly different animal to VBA, anyone who has an idea of what they are doing in that language should be able to pick up enough Java to get by... which, I hope, is where this article and the talk that it was developed with come in.
User avatar
Alan Kirk
Site Admin
Posts: 6606
Joined: Sun May 11, 2008 2:30 am
OLAP Product: TM1
Version: PA2.0.9.18 Classic NO PAW!
Excel Version: 2013 and Office 365
Location: Sydney, Australia
Contact:

Introduction To The Client Side Java API

Post by Alan Kirk »

Part 30: Glossary

A quick reference guide to some of the terms used in this article. As each instalment of the article is published it will keep being pushed to the bottom for easy location. qv is Latin for "which see"; in other words, refer to the glossary item under that name for more details.

;: see semi-colon.

{}: See Braces.

API: Application Programming Interface. A method by which an application (in this case TM1) allows you to write programming code to automate and extend it using an interface that is defined for that application. TM1 has a number of different APIs including the classic one for VB6 and C, the now unsupported .Net one, the Java one which is the subject of this article, and the new RESTful one which looks from here to be the one that I've been dreaming of for some time.

Array: An array is an ordered group of data values, all of the same data type. Think of them as a collection of boxes into which values can be placed. By "ordered" I mean that each box has a number given by its index;, its location in the array. That number never changes. Arrays are immutable; once you create an array you can't resize it, although in VBA there is a method (Redim Preserve) which will create a new array and populate it with the contents of the old one's contents at substantial processing overhead cost. In VBA, the number of the first "box" could vary. In Java, the first box is always zero. Arrays can have multiple dimensions; so you can have an array which is 6 spaces long and two spaces wide like a carton for a dozen eggs, though this would be defined as an array size of 5 and 1 because of the zero base. Java arrays have some interesting tricks (such as an ability to dimension each second element of a 2D array as an array of varying sizes), but overall even in Java arrays are quick and easy, but not very flexible. There are many collection objects like ArrayLists and Hash Maps that provide more power and flexibility.

Binary: Content which is in computer-speak rather than plain text. To the enquiring eyes of a bipedal life form binary content looks like just a bunch of gibberish made up of unprintable computer codes and random text and number values that don't really represent that text or those numbers. But to a computer, binary content is the stuff of poetry. (Sometimes, anyway. The original Rome: Total War is poetry. Photoshop is poetry. The TM1 server is usually poetry. Camtasia Studio is usually poetry. Cognos Configuration for TM1 and Performance Muddler make you want to rip every last byte of the application out of the computer and into the real world and then pretend that your name is Vlad The Impaler for the next 12 hours. So, less poetic, but still binary.)

Boolean: A value of true or false. Boolean is typically one of the primitive (qv) data types of any programming language, though it's represented differently in each of them. In VBA and VB6, for instance, a Boolean value of True is represented by -1 and a value of False by 0. (This caused some consternation when True was changed to +1 in VB.Net.) In Java you should not assume that the boolean primitive data type (always spelt with a lower case b in Java, but see also "Helper Classes" below) has any specific numeric value; always check Java boolean values against the keywords true and false (also lower case) to determine its value.

Braces: The { and } characters, called brackets in some parts of the world but down here they're called braces and dammit that's what we'll call them in this article. All blocks of code in Java need to be surrounded by braces. That includes (see the individual entries for each of the following):
  • Classes;
  • Methods;
  • If blocks;
  • For loops and While loops.
Branching Statement: See If Block.

Build Path: If you are going to be using .jar (qv) libraries (other than the Java core libraries) as the source of some of your code, you need to add them to your project's build path, the path(s) that the compiler searches to find the code that it needs to incorporate into your output. In Eclipse this can be as simple as right clicking on the .jar library and selecting Build Path -> Add To Build Path.

Bytecode: A form of binary content that the Java compiler (qv) creates from the plain text contained in your .java class (qv) definition files. Bytecode cannot be read directly by a computer processor, but rather is read by the Java Virtual Machine (JVM) (qv) which then translates it into something that can be used on that specific computer processor and operating system combination.

Chamberlain, Joshua: Colonel of the 20th Maine Regiment at the Battle Of Gettysburg and possessor of one of the most impressive moustaches I've ever seen. I will have to grow such a moustache if I am ever to sneak incognito into another Cubewise Conference. (See Conference, Cubewise.) Failing that (and in the interests of Civil War even handedness) I may grow a beard like Gen. James Longstreet though I suspect that it may take a few too many years for that plan to work.

Class: The definition (blueprint, template, plan, call it what you will) of an object. It describes what fields the objects which are created from the class will have, what methods they will have, and the program code that will run for each method. Objects are then created (instantiated) from the class definition for use in storing and manipulating data. Classes in Java are plain text files with a .java extension. The text file requires that you use the keyword class followed by the class name. This is followed by opening and closing braces. Within those braces you define all of the fields and methods that objects created from the class will have.

Compiler: A program which takes your source code (written as plain text in .java files in the case of Java) and converts it into binary (qv) code. In older style languages like C and VB6 the end result would be an executable file (.exe) that could be run by the type of computer that the code was targeted to, or possibly a .dll (dynamic link library) which contained libraries of program code that could be used by other programs. (You would need different source code for different computer operating systems and sometimes for different processors using the same operating system.) In the case of Java the compiler (an executable named javac.exe which is part of the Java Development Kit (JDK)) compiles your plain text .java code to bytecode (qv), which is then run by the JRE rather than being executed directly.

Complex object: See "Object". The term "complex" is merely used to distinguish such objects from primitive (qv) data types.

Condition: Typically some kind of expression which returns a Boolean (qv) value of either true or false. It is commonly used to control what a computer program does. An expression like (x < 7 ) will return true if the variable x is less than 7, false if it is 7 or higher. You can use this to direct the program to do one thing if the expression is true, and another thing if it's false. See also If blocks, For loops and While loops.

Conditional Test: An expression, typically enclosed in round brackets, which determines whether a condition (qv) is true or false. Conditional tests are used for If blocks, While loops and For loops (qqv).

Conference, Cubewise: An annual TM1 user conference that is of massive value to the TM1 user community except for one session on the Java API at the Sydney 2015 one where the presenter mistimed the whole presentation. This article would have been based on that session, if that session had actually happened. Which it didn't. I repeat... It. Never. Happened, and I was never there. See also Chamberlain, Joshua.

Eclipse: A very fine IDE (qv) which is available for various computer languages, including Java.

EE: Java Enterprise Edition. The version of Java that you use for creating server side code to run web servers and the like. It's essentially the same as SE (as I understand it) but with a bunch of extra server-specific features.

Exception: A runtime error in Java code. For example trying to read a text file that doesn't exist, or connect to the internet when the network is down when you run the code would both throw exceptions. Exceptions are complex objects which contain a lot of information about the runtime error. Java is very fastidious about exception handling and if you don't do it correctly your code won't compile. You need to either handle the exception in a Try/ Catch block (qv) or Throw (qv) it back to the calling procedure and handle it there. The compiler will not allow you to leave unhandled exceptions. The Exception class is a superclass; other, more specific Exception classes exist which are based on it, and you can create your own exception classes as well.

Field: The equivalent of a Property in VBA. It's a variable that forms part of any objects that are created from a class. The variables (fields) allow the objects to store multiple values of various different data types. It's possible to have static (qv) fields which can return values from a class definition without an instance of the class being created as an object.

For loop: A block of code that is to be run a specified number of times while a variable value steps from its lower limit to its upper limit. As with other blocks you specify the conditions of running the code first, then specify the code to be executed within opening and closing braces.

Helper Classes: In Java each primitive (qv) data type has a corresponding "helper class", typically with the same name as the primitive data type but with a capital rather than a lower case letter. The helper class is a complex object which contains methods that help you manipulate or take some kind of action on the data type that it relates to; for example, converting the value to a formatted string. There are a few exceptions to the "same name" rule; for example the helper class for the int (integer) data type is called Integer. Helper classes can in fact store the values that they help with as well, so you can often use a helper class object when you mean to use a primitive data type and still get away with it.

IDE: Integrated Development Environment. A programming environment which allows you to create, edit and compile code with a lot of the back end maintenance being handled for you without you needing to bother with passing obscure and cryptic command lines to the compiler. The one that I'm using in this article is called Eclipse.

If block: A method for directing the flow of a computer program, sometimes called a branching statement because of the way it can send the flow of the code down different branches or paths. It consists of a condition (qv) test to see whether a particular condition is true or not, and if it is executing a particular block of program code. If statements (always lower case in Java) can be accompanied by else if statements which test for alternative conditions, as well as (optionally) an else statement that applies if none of the preceding conditions do. In Java each of the conditions must be followed by opening and closing braces which define the block of code to be run if the condition is true.

Instance: An object (qv) in a computer's memory which has been created from a class, often (but not always) by using the new keyword in Java to create the object.

Instantiation: Creating an instance (qv) of a class (qv).

Immutable: An object which can't be changed after it has been created. That doesn't mean that the value can't be changed... but it does mean that if the value is changed then the original object is destroyed and a new one created to hold the updated value. Strings are the most notable objects which are immutable in Java. Array (qv) sizes are also immutable.

Import: If you refer to another class in your code, and that class is in a different package (aside from classes in core libraries like java.lang) you need to include an "import" statement at the head of your class file to tell the compiler where to go to find that code. Provided that you have added the .jar library to your build path (qv), Eclipse will take care of this for you automatically when you press [Ctrl]+[Space] after making the first reference to the class.

.jar Files: Java Archive files. When the compiler converts your source code into binary (bytecode) .class files, it packages them into .jar files. These are simply archive files in WinZip format which can be passed to the java.exe JRE (qv) application to be run. Some .jar files can be made self-executing, as is the case with the example in this article.

Java: An object oriented programming language. Java programming code is not compiled to a form which can be run directly by a computer processor. Rather, it's compiled to an intermediate binary format called bytecode in files which have a .class filename extension. That bytecode is run by an application named java.exe (sometimes javaw.exe for Windows) which is called the Java Virtual Machine (JVM) or Java Runtime Engine (JRE). Because the code does not have to be rewritten for each operating system / processor combination (only the JRE needs to be), Java is theoretically cross-platform; it can run in any environment for which there is a JRE.

JDK: Java Development Kit. This is a package which is downloadable from Oracle. It contains the JRE plus various development tools, including the javac.exe compiler (qv) which converts your plain text .java files to .class bytecode (binary) files.

JRE: Java Runtime Engine. This is an executable file (typically either java.exe or javaw.exe for Windows) within which your compiled Java bytecode (qv) is run. Because the JRE is the program that interacts with the operating system rather than your code, in theory your code can be run on any computer system for which a JRE exists. Sometimes referred to as the Java Virtual Machine or JVM. (There is technically a difference between the two that I could bang on about for a few paragraphs, but for the purposes of most end users it's one of those "nobody cares" things.)

JVM: Java Virtual Machine. See Java Runtime Engine.

Keyword: A word that has a special meaning to a programming language. In Java, for instance, the keyword "new" is used to tell Java that you want to instantiate (qv) and object (qv) from the class (qv) definition. You cannot use keywords as variable or (generally) procedure names.

Library: Code that has already been written and that you draw upon the classes, methods and fields of to avoid reinventing the wheel. Libraries in Java typically come in .jar (qv) packages. The core libraries are already added to your application, but any others that you use need to be added to your build path (qv).

Method: A procedure which runs code to "do stuff". Methods are part of a class definition, and become available when you instantiate (create) an object from that class. Static (qv) methods are available even without instantiating an object.

new: A keyword in Java (always in lower case, as most Java keywords are) which is a command to instantiate a new object from a class definition.

Object: A named "thing" stored in the computer's memory which typically contains a mixture of data (stored in fields) and programming code (contained within methods). The fields and methods that an object has when it is created is defined by a class (qv).

Package: A method of grouping related .class bytecode files together. A package is in fact a series of folders within your operating system, inside which the source (plain text) .java files and the output (binary bytecode) .class files will be stored. (Typically it’s actually two identical series of folders, one under the src folder that stores your source files, and one under the bin folder where your compiled .class files are stored.) When referred to in your code a package will normally have each sub-folder delimited by periods rather than slashes. The use of packages makes sure that any classes that you create with the same name as classes from a different source can be distinguished from each other. Package names are all lower case by convention, to avoid confusion with class names. See also Reverse Domain Notation.

Primitive: A simple data type of variable (such as a single numeric value, a single character of text, a Boolean value, etc) which has two main characteristics. First, it only ever represents a single value, as opposed to an Object (qv) which can store multiple values in its fields. Second, it is data only; unlike an Object it has no methods to execute code. Be aware that in Java each primitive data type has a corresponding Helper Class which is an Object.

public: A keyword that indicates that the class, method or field is visible to the entire Java project. This is said to be the class, field or method's "scope".

Refactoring: The process of extracting or reorganising code within a project. For example, suppose that you write a loop in a method and then decide it would be better off as a separate, private method within the class. You could select that code and have Eclipse "refactor" it so that is extracted into that new method. You can also refactor by renaming classes, methods and fields, or perhaps moving classes between packages, and have Eclipse update all of the relevant references automatically. Refactoring is far more reliable than doing a search and replace.

REST: Representational State Transfer, a software architecture style for building scalable web services. The latest TM1 API, which will eventually supersede all of the current ones including the Java one, is based on this methodology. It primarily uses HTTP-based methods.

RESTful: A web service architecture / API built on REST standards.

Reverse Domain Notation: Typically used when creating packages (qv). If you want to create a folder (package) named maincode to store all of your primary class definitions, and your corporate website is ww.yourcompany.com, you would typically name the package as com.yourcompany.maincode to ensure that any classes that you create would have a name that is unique to your company. Not all Java libraries use this notation; some of the core libraries use names like java.lang and java.util.

SE: Java Standard Edition. The edition of choice for creating console applications and applications intended to run on notebooks, desktops and similar devices. For applications that are intended to deliver web services, for example, EE (qv) is the more common choice.

Semicolon (; character): Each line of programming statements needs to be terminated by a semi-colon to tell Java that that's the end of that statement, just as happens in TurboIntegrator. Semicolons do not appear after the ending brace ( } character) of a block of code, just after each line of code.

static: A field or method which is defined as part of a class, and which does not require you to create an instance (qv) of the class before you can use it. Instead you can simply specify that you want to execute the .foo method of the Bar class, and it will run directly from the class definition. Do not confuse this with the VBA keyword "Static", which is a completely different concept.

String: A sequence of characters of text. Strings are complex objects in Java (as opposed to VBA where they are regarded as a base data type) and therefore the data type always starts with a capital S. Strings are immutable; once populated the only way they can be changed is by destroying the old object and creating a new one. For that reason if the action is performed repeatedly it's better to use a StringBuilder object (which is designed to update constantly changing string values) and retrieve only the final string from that. Do not confuse strings with the char (character) data type, which holds only a single character and is a primitive type. Strings and arrays of chars can be used interchangeably, at least in terms of the storage of data. (Not that there is usually a need to.)

Symbolic Link: A special type of file that allows another command which calls it to be redirected elsewhere. Microsoft included a number of symbolic links for people migrating to versions of Windows later than XP to account for the fact that some programmers had hard coded in paths that would no longer exist after XP. By using symbolic links the programs would be redirected to the new paths without any impact on the users. Java uses symbolic links to ensure that calls to the JRE (qv) go to the correct place, even if the system has been upgraded resulting in the JRE files moving to a new location.

Throw / throws: If you want to generate your own runtime exception (qv) you use the throw keyword to create a new Exception object and feed the details of the error into it. If you are throwing an exception, or using method calls which might throw an exception, you need to either handle these within a Try/Catch block (qv) or put a "throws" statement in your method declaration to warn the calling procedure that there is the possibility that specific Exception types may be coming back at it.

TI: TurboIntegrator, TM1's Extraction, Translation and Loading tool. If you didn't already know that, I'm not sure you should be reading this.

Trie: With reference to Wikipedia: In computer science, a trie, also called digital tree and sometimes radix tree or prefix tree (as they can be searched by prefixes), is an ordered tree data structure that is used to store a dynamic set or associative array where the keys are usually strings. No, it has nothing to do with the Java API as such but it's used inside the TM1 server and since I mentioned it in relation to something that happened at the Conference, Cubewise (qv) I thought I'd better mention it.

Try/Catch/Finally block: Used in Exception (qv) handling. You can place code that you think may possibly throw an exception when you run it into a block of code (surrounded by {} braces, of course) after the keyword Try. You then have a second block of code after the keyword Catch which handles the Exception. The Catch block is defined to receive an Exception object (typically named e) as an argument, and you can use that to decide what to do with the exception. You can optionally create a Finally block which executes after the code above has been run, and which can be used to "clean up" by closing opened files and the like.

Value Capsule: A long integer value returned by some API functions in both the classic and Java TM1 APIs. These point you to where a value can be found rather than containing the value itself. You then need to use a further function (classic API) or method (Java API) to pull the relevant value out.

Variable: A named location in a computer's memory which holds a value. That value may be either a primitive (qv) value, or a complex object (qv). By giving the value a name you can easily manipulate the value as you need to, changing its fields or calling its methods if it's an object.

While loop: A block of code that executes over and over again for as long as a particular condition remains true. The code that executes must be contained within opening and closing braces that appear after the condition.
Locked