Writing a Program to Control OpenOffice.org, Part 2

by Franco Pingiori

In Part 1, we studied the fundamental concepts of OpenOffice.org's software development kit (SDK) and how the SDK can be used to communicate with the OOo programs. We now are ready to write an application. As previously stated, we are going to develop a program that is able to interact with OpenOffice.org's spreadsheet application, Calc. Two reasons are behind this choice. First, solving the problems raised in creating this program will acquaint us with many of the most important aspects of UNO (Universal Network Objects) programming. Second, spreadsheets allow users to build nice reports easily. If we are able to control a spreadsheet application, it can be turned into our personal report generator.

The code that will allow our application to work with Calc can be divided into six sections, each one with its own task:

  1. Connecting to OpenOffice.org

  2. Opening the document

  3. Choosing a worksheet

  4. Modifying the chosen worksheet

  5. Printing the worksheet

  6. Closing the connection

We are going to study the complete source code of a simple program that, regarding the first two items, largely depends on the example located in <OpenOffice.org SDK directory>/examples/DocumentLoader. A Qt based application also is available.

In order to make this article more readable, I refer to seven short code snippets, one more than the above numbered points. For the sake of clarity, I decided to write the numerous #includes and using namespace directives in a separate section, Listing 1.

Listing 1. Header Files and Using Namespace


#include <stdio.h>
#include <cppuhelper/bootstrap.hxx>
#include <osl/file.hxx>
#include <osl/process.h>

#include <com/sun/star/frame/XDesktop.hpp>
#include <com/sun/star/bridge/XUnoUrlResolver.hpp>
#include <com/sun/star/frame/XComponentLoader.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/sheet/XSpreadsheetDocument.hpp>
#include <com/sun/star/sheet/XSpreadsheets.hpp>
#include <com/sun/star/sheet/XSpreadsheet.hpp>
#include <com/sun/star/table/XCell.hpp>
#include <com/sun/star/table/XCellRange.hpp>
#include <com/sun/star/container/XIndexAccess.hpp>
#include <com/sun/star/view/XPrintable.hpp>
#include <com/sun/star/view/PaperOrientation.hpp>
#include <string.h>

using namespace rtl;
using namespace cppu;
using namespace com::sun::star::view;
using namespace com::sun::star::table;
using namespace com::sun::star::container;
using namespace com::sun::star::sheet;
using namespace com::sun::star::uno;
using namespace com::sun::star::lang;
using namespace com::sun::star::beans;
using namespace com::sun::star::bridge;
using namespace com::sun::star::frame;
using namespace com::sun::star::registry;

Every interface--and we use a lot of them--is declared in its own header file, and every service adds its own namespace. The first point, the header file, needs the com.sun.star.bridge.UnoUrlResolver service. Let us study its name, as that can provide us with some interesting information. First of all, the influence of Java is unquestionable: the dot notation and the names--com, sun--show this clearly. And it's no wonder, as Sun is involved in the OpenOffice.org project. Therefore, every C++ software instrument is an adaptation of its Java counterpart. For instance, nested namespace directives replace the Java packages notation. Thus, when we read the name of a service, we can benefit by keeping in mind the typical Java packages structure. Doing so makes the reference guide, <OpenOffice.org SDK directory>/docs/common/ref/com/module-ix.html, much easier to use.

What can we say about the service we need? After com.sun.star, the common root, comes "bridge". This refers to the interprocess bridge we spoke about in the previous article, used to establish the connection with the server. If we want to connect our application to this server, we must find it; in other words, we have to resolve its URL. This is the task of the UnoUrlResolver service. But remember, services can do nothing on their own. They simply describe what must be done, not how it can be done. We do not create the service, but an interface to the service. From a programmer's point-of-view, services are above all a conceptual help. But, the source code only has interfaces. Invoking them, however, could cause us some problems, because we must specify their positions within the nested namespace structure. This way, our code hardly would be readable. That's why we wrote the code in Listing 1; adding the using namespace directives makes our source shorter.

Listing 2. Connecting to the OpenOffice.org Server


//create a OUString to connect to the local host
OUString sConnectionString=OUString::createFromAscii
("uno:socket,host=localhost,port=8100;urp;StarOffice.ServiceManager");

//create an instance of XSimpleRegistry...
Reference<XSimpleRegistry> xSimpleRegistry(createSimpleRegistry());

// ... and connect it to the registry file: prova.rdb, current directory 
OUString sRdbFile=OUString::createFromAscii("./prova.rdb");
xSimpleRegistry->open(sRdbFile,sal_True,sal_False);

//open an  XComponentContext based on the registry file in
// xSimpleRegistry
Reference<XComponentContext> xComponentContext
(bootstrap_InitialComponentContext(xSimpleRegistry));

//build the client-side service factory
Reference<XMultiComponentFactory> xMultiComponentFactoryClient
(xComponentContext->getServiceManager());

//create an XInterface of the service factory, relying on the service string 
//and on the context; then...
OUString sService= OUString::createFromAscii 
( "com.sun.star.bridge.UnoUrlResolver" );
Reference<XInterface> xInterface=
xMultiComponentFactoryClient->createInstanceWithContext
(sService,xComponentContext);

//...query it to build an XUnoUrlResolver interface and...
Reference<XUnoUrlResolver> resolver(xInterface,UNO_QUERY);

//...use its resolve() method to reach the openOffice server and to
//create an XInterface to the server

try
   {    
    xInterface=Reference<XInterface>(resolver->resolve(sConnectionString),UNO_QUERY);
   }
catch(...)
   {
   printf("Error: could not connect to the server");
   exit(1);        
   }

Listing 1 shows us how to proceed. The data to establish the connection is stored in a Unicode character string, a type defined as OUString in UNO. Tthe target is on the local host and uses port 8100. The key element in this excerpt of code is the Reference template, which allows us to create C++ usable data, starting from abstract definitions of services. We here see how to use it to create the service we need. Before using the service factory, however, we must specify the context within which our service will work. That is, the program has to know where to find the registry file we previously talked about, the one that contains the binary description of every data type and service we are going to use.

Opening rdb files is the task of the interface XSimpleRegistry. For the first time, we see the use of the template Reference to instantiate interfaces. In this case, the constructor calls the function createSimpleRegistry to accomplish its work. Once we have the object xSimpleRegistry, its open method allows access to prova.rdb, stored in the current directory. The parameters passed to open tidily specify the name of the rdb file, if this file must be opened in read-only mode and if it must be created if it does not exist. By the way, we create the OUString object we need by using the static method createFromAscii.

The following step binds the data and services stored in the registry file to an interface XComponentContext. Notice the function bootstrap_InitialComponentContext; it "bootstraps an initial component context with service manager upon a given registry" (see the C++ Reference Guide, <OpenOffice.org SDK directory>/docs/cpp/ref/index.html).

As we have a context, we can use a service manager to implement UnoUrlResolver. Of course, what we really need is a service manager interface--XMultiComponentFactory, created by getServiceManager(), a method of XComponentContext. Then, we build an XInterface referring to the service--XInterface is the ancestor class of every interface--calling the method createInstanceWithContext() of XMultiComponentFactory. createInstanceWithContext() has two parameters, the name of the service and the context. XInterface is only a proxy of the service, and we can't use it directly. We instead must use the interface exported by the service. XUnoUrlResolver, whose resolve() method is able to connect to servers, but, at least under Linux, it only works if OpenOffice.org already is running. The following line shows how to create an XUnoUrlResolver interface, resolver, starting from an XInterface (xInterface):


Reference<XUnoUrlResolver> resolver(xInterface,UNO_QUERY);

As previously stated, XInterface refers to the UnoUrlResolver. We query this interface asking it for an XUnoUrlResolver using the Reference template, and as we are going to see, the process is always the same. All we have to do is to invoke Reference by passing two parameters, a reference to the interface we are querying (XInterface, in our example) and the UNO_QUERY enumerated constant. Obviously, the syntax used depends on the programming language depending. That's why the Developer Guide, which particularly refers to Java, gives us different information.

The final lines of Listing 2 try to connect to the server. If the resolve() method succeeds, it returns a server proxy. The code shows how to use this proxy to build an XInterface of the service which is listening on the server. The technique is the same as what we have just seen. The try-catch block allows us to safely close the program should something go wrong.

Opening the document requires the com.sun.star.frame.Desktop service, which is the environment for all the components we can instantiate from frames. The code in Listing 3 largely relies on the techniques we just learned and uses the last interface we created in Listing 2.

Listing 3. Opening the Document


//acquire the server-side context
Reference<XPropertySet> xPropSet( xInterface, UNO_QUERY );
OUString sProperty=OUString::createFromAscii("DefaultContext");
xPropSet->getPropertyValue(sProperty) >>= xComponentContext;

//take the OpenOffice service manager   
Reference<XMultiComponentFactory> xMultiComponentFactoryServer
(xComponentContext->getServiceManager());

//instantiate the component supporting the desired service 
//(load a document)    
sService=OUString::createFromAscii("com.sun.star.frame.Desktop");
xInterface=xMultiComponentFactoryServer->createInstanceWithContext
(sService,xComponentContext);
Reference<XComponentLoader>xComponentLoader
(xInterface,UNO_QUERY);

//declare 3 OUString, to store URL, working directory 
//and name of the document
OUString sDocUrl, sWorkingDir,sName; 

//find the working directory and write it 
osl_getProcessWorkingDir(&sWorkingDir.pData);

//write the name of the document
sName= OUString::createFromAscii("./prova.sxc");

//create the URL 
osl::FileBase::getAbsoluteFileURL( sWorkingDir,sName,sDocUrl);

//set the document type
OUString sType=OUString::createFromAscii("_blank");

//try to open the document
Reference<XComponent> xComponent=
xComponentLoader->loadComponentFromURL
(sDocUrl,sType,0,Sequence<PropertyValue>());

That XInterface is used to find the new context where com.sun.star.frame.Desktop works. In fact, the old context refers to the environment where the client process works. Generally speaking, the server could work in a different environment and in a different host. That's why we use different names for the service managers operating in the client context and in the server one. The new context is a property of the proxy we acquired in the previous point. In order to find the properties of a service, we can query it the usual way and then put the results in an interface, XPropertySet. The following line is an excerpt from Listing 3. It shows how to extract the data from an interface called xPropSet and put it in xComponentContext, the new context interface:


xPropSet->getPropertyValue(sProperty) >>= xComponentContext;

When we defined properties, we said that they are identified by a name-value pair. In our example, we pass the name to the getPropertyValue() method in the sProperty string, and it returns the value. This method is the only one to access properties. Therefore, it must be able to return many types of data, as there are many types of properties. That's why the data type Any and the extraction operator >>= have been defined. Starting from this point, we use the same methodologies that we used in Listing 2.

The service exports three interfaces. We need XComponentLoader, whose method loadComponentFromURL(), in cases of success, returns an XComponent that manages and frees the resources related to the document. The first parameter of the method is the string that contains the URL of the file we want to open. Notice that passing the relative path of the file we want to open could not work, because if the server worked on a remote host, it would not find the document.

Our program processes the file prova.sxc, which is stored in the current directory, from the application point of view. The osl_getProcessWorkingDir() function finds the process working directory, while osl::FileBase::getAbsoluteFileURL() builds the URL of the document. The last parameter of loadComponentFromURL() is a sequence, created by the Sequence template. Sequences are sets of different kinds of variables, and the number of their components is not fixed. We don't need setPropertyValue and getPropertyValue to work with them. Accessing sequences is quite similar to accessing arrays, because we have to use an index. The sequence we are interested in stores property values, PropertyValue. I want to remark that this is the last time we use service factories, because all services and interfaces we are going to implement are in the same branch of the UNO taxonomy.

The procedure we are describing, until now, does not depend on the kind of document we want to open. Therefore, we can use it when working with texts, presentations and so on. Starting here, however, we have to introduce some instruments that have been designed specifically for spreadsheets. OpenOffice.org spreadsheets contain collections of worksheets. We have to choose and then open one of these worksheets in order to modify and print it. The UNO interface system allows us to reach our goal, and Listing 4 shows us how to proceed.

Listing 4. Accessing the Worksheet


//create xSheetDocument
Reference<XSpreadsheetDocument> xSheetDocument (xComponent,UNO_QUERY);

//create an instance of XSpreadsheets, which is a worksheets collection
Reference<XSpreadsheets> xSheets=xSheetDocument->getSheets();

//create a class to interact with single worksheets;

//the single worksheets are referenced by an XIndexAccess interface
Reference<XIndexAccess> xIndex (xSheets,UNO_QUERY);

//take the first worksheet (index=0)...
Any any=xIndex->getByIndex(0);  

//...then create an instance of Xspreadsheet, able to manage 
//single worksheets; 
Reference<XSpreadsheet> xSheet;

//finally, assign the first worksheet to xSheet 
any >>= xSheet;  

First of all, we query our XComponent to create the interface XSpreadsheetDocument, whose getSheets() method returns XSpreadheets. The task of the latter is to manage collections of worksheets. We then need XSpreadsheet, which controls a single worksheet. XSpreadheets contains one XSpreadheet for each worksheet in the document. Each XSpreadheet is identified by its index, as with an array element. Choosing the right XSpreadheet requires the creation of the XIndexAccess container. Containers are data structures similar to sequences. All we have to do is pass the index of the worksheet we want to work with to the method getByIndex() of our container. Naturally, getByIndex() returns an Any data type, thus we have to use the operator >>=.

From XSpreadsheet we create an XCellRange, as shown in Listing 5.

Listing 5. Modifying the Chosen Worksheet


//create an XCellRange interface to interact with cells ranges
Reference<XCellRange> xCellRange(xSheet,UNO_QUERY); 

//create an XCell interface to interact with single cells
Reference<XCell> xCell; 

//take the cell located in column 5, row 6
int column=5;
int row=6;
xCell=xCellRange->getCellByPosition(column,row);

//write a number in the cell
xCell->setValue(1960);

//change cell
column=6;
row=5;
xCell=xCellRange->getCellByPosition(column,row);

//write a string in the cell
sString=OUString::createFromAscii('stringa');
xCell->setFormula(sString); 

XCellRange is able to manage a range of cells, and its getCellByPosition() method finally returns an XCell interface. XCell has two methods of writing data in a cell, setValue() for numbers and setFormula() for anything else.

The printing procedure, shown in Listing 6, is based on XPrintable and starts from XSheetDocument (see Listing 6).

Listing 6. Printing the Worksheet


//open an XPrintable interface, linked to xSheetDocument
Reference<XPrintable> xPrintable(xSheetDocument,UNO_QUERY);  

//load the printer setting
Sequence<PropertyValue> pPrinter=xPrintable->getPrinter();

//try to modify the PaperOrientation property
OUString orient=OUString::createFromAscii("PaperOrientation");
int k=0;
//check the names until PaperOrientation
do
  {
  if(orient.compareTo(pPrinter[k].Name)==0)
    {
    pPrinter[k].Value <<= PaperOrientation_LANDSCAPE;
    }
    k++;
  }
while(orient.compareTo(pPrinter[k].Name)!=0);
   

//assign properties to the printer
xPrintable->setPrinter(pPrinter);

//print
xPrintable->print(pPrinter);

This interface stores the printer configuration in its sequence of properties. The code modifies the paper orientation property, whose name is PaperOrientation. We meet here another extraction operator, <<=, defined to assign an Any value to a variable.

The print() method starts the printing process. It is an asynchronous process, therefore its execution could take a little while.

Finally, Listing 7 shows the code to close the document and the connection to the server.

Listing 7. Closing the Connection


//try to close xComponent, until dispose() succeeds
char b=0;
while(b==0)
  {
  try
    {
    xComponent->dispose();
    }
  catch(...)
    {
     b=1;
     }
  }

//delete the client-side service factory
Reference<XComponent> 
::query(xMultiComponentFactoryClient)->dispose();

We must delete the XComponent we got when loading prova.sxc as well as the client-side service factory, in this order. The key-method is dispose, but it is not defined for XMultiComponentFactory interfaces. The following line solves the problem.


Reference<XComponent> ::query(xMultiComponentFactoryClient)->dispose();

We start from an instance of XMultiComponentFactory called XMultiComponentFactoryClient. The query() function tries to build an XComponent interface based on XMultiComponentFactoryClient. If the operation succeeds, dispose is invoked. Please, do not delete the server-side service factory unless you want to close the OpenOffice.org server.

Notice that dispose fails if printing processes are in progress. This creates a problem, though, because the method returns nothing. Therefore, we are not able to control whether it works. Listing 7 provides a possible solution, though: call dispose within a try-catch block until it succeeds.

Writing the Makefile

We have to know where the header files and the libraries we need are stored, in order to give all the necessary information to the compiler and linker. Let's begin with the header files. They are stored in the following directories:

  • <OpenOffice sdk directory>/include: starting from this directory, we can find general-purpose data and functions declarations, including semaphores and threads.

  • Regarding the interfaces and services we use, we create their header files with cppumaker as we need them. In other words, we choose where to store them. For example, we could write the header files directory tree starting from the source code directory.

We have to link the following dynamic libraries: libstlport_gcc.so, usually written in /usr/lib during the installation process of stlport; libsal.so, libsalhelpergcc3.so, libcppu.so and libcppuhelpergcc3.so, all stored in <OpenOffice directory>/program. What are these libraries for? We could say that libsal.so is the base of the whole UNO system (see Figure 1); sal is short for system abstraction layer. This library contains some useful runtime functions, but it does not refer to any UNO component. libsalhelpergcc3.so helps libsal.so. libcppu.so (C++ UNO) is the core of the C++ system of libraries. Its functions create interfaces, manage bridges and so on. Finally, libcppuhelpergcc3.so manages the bootstrap of UNO.

Writing a Program to Control OpenOffice.org, Part 2

Figure 1. Overview of the Base Shared Libraries (C++)

One more word about libraries: people using the 3.3.x releases of GCC have to replace the file libgcc_s.so.1 in the OpenOffice.org directory with the one shipped with their compiler. Otherwise, the exception handling causes a segmentation fault at runtime. The issue is thoroughly treated by Stephan Bergmann here.

Finally, some flags and options are available for the compiler and linker. The following lines show how to compile test1.cpp to build the executable test1, assuming that the header files tree written by cppumaker starts from the source directory:


gcc -c -O -fpic -fno-rtti -I. -I/usr/include -I/$(OO_SDK_HOME)/include
-DUNX -DGCC -DLINUX -DCPPU_ENV=gcc3 -o test1.o test1.cpp

gcc -Wl -export-dynamic -L/OOSDK/linux/lib -L/$(OFFICE_HOME)/program -o
test1 test1.o -lcppuhelpergcc3 -lcppu -lsalhelpergcc3 -lsal -lstlport_gcc.

Load Disqus comments