Creating Spreadsheet Documents Using ODFDOM
This tutorial works for ODFDOM version 0.7. The API for
tables has been completely revised (and much improved)
for version 0.8. This tutorial will be updated in the
future to work with the latest version of ODFDOM.
The input for the program in this tutorial is
a text file that
contains weather observations for a 24-hour period. Each line contains
an observation date, time of day, and temperature in degrees Celsius.
The program’s output will be
an OpenDocument spreadsheet file.
Here is a
partial screenshot of what the result will look like.
The program is written to be run from the command line program; you invoke
it with a command like this:
java -jar simple_ods.jar inputfile.txt outputfile.ods
The Structure of a Spreadsheet Document
In order for the following code to make sense, you need to know how a
spreadsheet document is represented in OpenDocument.
An ODF spreadsheet’s
content.xml file
contains an <office:spreadsheet> element.
Each of its <table:table> children represents one worksheet
within the document. Tables and their styles are built in this hierarchy:
-
The
<table:table> may have a style that describes its
borders and page-break properties (this is because tables can appear in
text documents as well as spreadsheets).
-
A series of
<table:table-column> elements
specifies the width of each of the table columns
and the default style for cells
in that column.
-
The column specifications are followed by a series of
<table:table-row> elements that specify
the height of each row.
-
Each row consists of
<table:table-cell>
elements. A cell has attributes that specify:
- Its style name
- A value attribute that tells what type of
data the cell contains (string, numeric, date, time,
formula, etc.)
- The cell’s value in an internal format
The cell may have a <text:p> child that gives
the presentation form of the cell’s value. If none is given,
some applications such as OpenOffice.org will evaluate the
cell value and display it.
Variables
The program starts out by declaring variables that you need to accomplish the
task. First, you need a variable to hold the name of the input file and
output file, and the spreadsheet document.
String inputFileName;
String outputFileName;
OdfSpreadsheetDocument outputDocument;
As described in the
previous tutorial, you also need variables for the main
“access points” to the document:
OdfFileDom contentDom; // the document object model for content.xml
OdfFileDom stylesDom; // the document object model for styles.xml
// the office:automatic-styles element in content.xml
OdfOfficeAutomaticStyles contentAutoStyles;
Unlike the previous tutorial, this spreadsheet does not create any
named styles, so it does not need the stylesOfficeStyles
variable; it is just here for the sake of completeness.
In this program, you will be creating automatic styles. The
end user of the document does not need to see the names of these
styles, and the ODFDOM toolkit automatically generates unique
style names for you. In order for the program to apply those styles
to elements, though, it must keep track of their names:
String columnStyleName;
String rowStyleName;
String headingStyleName;
String noaaTimeStyleName;
String noaaDateStyleName;
String noaaTempStyleName;
These styles correspond to the styles mentioned in the table hierarchy. The last
three styles are prefixed with noaa because the data came from the
National Oceanic and
Atmospheric Administration’s (NOAA) XML feed. It also helps to distinguish
the names from ODF’s date and time format styles, which you will see in
a while.
As mentioned previously, the content.xml file for a spreadsheet document has
all the tables as a child of an
<office:spreadsheet> element. That element wil be stored in:
OdfOfficeSpreadsheet officeSpreadsheet;
This having been done, here is the main() method,
which creates an application and runs it via the run()
method:
public static void main(String[] args) {
SimpleOds app = new SimpleOds();
app.run(args);
}
public void run(String[] args) {
if (args.length == 2) {
inputFileName = args[0];
outputFileName = args[1];
setupOutputDocument();
if (outputDocument != null) {
cleanOutDocument();
addAutomaticStyles();
processInputDocument();
saveOutputDocument();
}
} else {
System.err.println("Usage: SimpleOds infile outfile");
}
}
Creating the Output Document
The setupOutputDocument() method starts by calling
newTextDocument() to create an ODF text
document from a template that is built into the library. Once you
have the document, the method gets the the Document Object Model (a
subclass of Document) for the content.xml and
styles.xml file.
setupOutputDocument() then retrieves the
automatic styles in content.xml and the named styles in
styles.xml (or creates them if they don’t exist yet).
It finishes by retrieving the <office:spreadsheet> element
from the content DOM. All of the headings and paragraphs that make up the
document’s content will be children of this element.
void setupOutputDocument() {
try {
outputDocument =
OdfSpreadsheetDocument.newSpreadsheetDocument();
contentDom = outputDocument.getContentDom();
stylesDom = outputDocument.getStylesDom();
contentAutoStyles = contentDom.getOrCreateAutomaticStyles();
stylesOfficeStyles = outputDocument.getOrCreateDocumentStyles();
officeSpreadsheet = outputDocument.getContentRoot();
} catch (Exception e) {
System.err.println("Unable to create output file.");
System.err.println(e.getMessage());
outputDocument = null;
}
}
Clearing Content from the Output Document
The templates included in the ODFDOM toolkit have content in them; a
newly-created spreadsheet document contains at least one worksheet. The
cleanOutDocument() method gets rid of any existing
worksheets, and is built along the same lines as
described in the
previous tutorial, so we won’t duplicate the code here.
How Table Styles Work
Before continuing with the part of the program that creates the styles, you
need to know a bit about how styles and tables interact.
In the previous tutorial, you created a style by doing these steps:
- Create a style with a name and family.
- Set the display name
- Set the style’s properties.
For automatic styles, the sequence is similar:
- Create a style; the name is generated by the toolkit.
- Retrieve the internally generated name for future use.
- Set the style’s properties.
This sequence works for setting up styles for table columns,
table rows, and table cells that don’t require any formatting
of their data. For cells that need data formatting, you have a
couple of additional steps:
- Create a “data style” that tells how the data
should be formatted.
- Create a style; the name is generated by the toolkit.
- Retrieve the internally generated name for future use.
- Specify that this new style should use the data style created
in step 1.
- Set the style’s properties.
Adding Styles
Let’s this process in action. The
addAutomaticStyles() method starts by setting up
styles for
table columns (2.5cm wide), rows (0.5cm tall), and header
cells (bold and centered).
void addAutomaticStyles() {
OdfStyle style;
// Column style (all columns same width)
style = contentAutoStyles.newStyle(OdfStyleFamily.TableColumn);
columnStyleName = style.getStyleNameAttribute();
style.setProperty(OdfStyleTableColumnProperties.ColumnWidth, "2.5cm");
// Row style
style = contentAutoStyles.newStyle(OdfStyleFamily.TableRow);
rowStyleName = style.getStyleNameAttribute();
style.setProperty(OdfStyleTableRowProperties.RowHeight, "0.5cm");
// bold centered cells (for first row)
style = contentAutoStyles.newStyle(OdfStyleFamily.TableCell);
headingStyleName = style.getStyleNameAttribute();
style.setProperty(OdfStyleParagraphProperties.TextAlign, "center");
setFontWeight(style, "bold");
The last line in the method calls the setFontWeight()
method. This utility code,
described in
the previous tutorial, sets the font weight for western, asian,
and complex scripts.
Now the method creates the data formatting styles and appends them to the
automatic styles.
The format strings for date and time are
the same as those used in Java’s SimpleDateFormat class,
and the format strings for numbers are the same as those used in
Java’s DecimalFormat class.
// Create the date, time, and temperature styles and add them.
// The null in OdfNumberDateStyle means "use default calendar system"
OdfNumberDateStyle dateStyle = new OdfNumberDateStyle(contentDom,
"yyyy-MM-dd", "numberDateStyle", null);
OdfNumberTimeStyle timeStyle = new OdfNumberTimeStyle(contentDom,
"hh:mm:ss", "numberTimeStyle");
OdfNumberStyle numberStyle = new OdfNumberStyle(contentDom,
"#0.00", "numberTemperatureStyle");
contentAutoStyles.appendChild(dateStyle);
contentAutoStyles.appendChild(timeStyle);
contentAutoStyles.appendChild(numberStyle);
The method concludes by creating the styles for the date, time, and temperature
cells, linking them to the data styles with the
setStyleDataStyleNameAttribute() call:
// cell style for Date cells
style = contentAutoStyles.newStyle(OdfStyleFamily.TableCell);
noaaDateStyleName = style.getStyleNameAttribute();
style.setStyleDataStyleNameAttribute("numberDateStyle");
// and for time cells
style = contentAutoStyles.newStyle(OdfStyleFamily.TableCell);
noaaTimeStyleName = style.getStyleNameAttribute();
style.setStyleDataStyleNameAttribute("numberTimeStyle");
// and for the temperatures
style = contentAutoStyles.newStyle(OdfStyleFamily.TableCell);
noaaTempStyleName = style.getStyleNameAttribute();
style.setStyleDataStyleNameAttribute("numberTemperatureStyle");
style.setProperty(OdfStyleParagraphProperties.TextAlign, "right");
}
Processing the Input Document
As before, setting up the styles is the most difficult part of the
program. Once they are set up, processing the input document is relatively simple.
- Create the table object.
- Add the table column descriptions.
- Add a row with the headers.
- For each line in the file:
- Separate the date, time, and temperature
- Create a row with three cells containing that data.
- Insert a blank row.
- Insert a row with a label and a formula for the average
temperature.
Here is the code for steps 1 and 2:
void processInputDocument() {
BufferedReader inReader; // for reading the file
String data; // holds one line of the file
String[] info; // holds the split-up data
OdfTable table;
OdfTableRow row;
OdfTableColumn column;
OdfTableCell cell;
table = new OdfTable(contentDom);
column = table.addStyledTableColumn(columnStyleName);
column = table.addStyledTableColumn(columnStyleName);
column = table.addStyledTableColumn(columnStyleName);
Here is step 3. The
createCell() method
is a utility routine that avoids a lot of duplicated code.
// fill in the header row
row = new OdfTableRow(contentDom);
row.setTableStyleNameAttribute(rowStyleName);
row.appendCell(createCell(headingStyleName, "Date"));
row.appendCell(createCell(headingStyleName, "Time"));
row.appendCell(createCell(headingStyleName, "\u00b0C"));
table.appendRow(row);
And now, the file manipulation and loop. To save space, we
present it here without the enclosing try/catch.
The code is similar for each cell: (a) create the cell,
(b) give it a style name, (c) set the data as either a date, time, or
decimal value, and (d) specify what kind of value the cell contains.
The time of day needs some extra processing. Times in ODF are not really
times of day; they are stored as a duration, in
the ISO 8601 extended format PnYnMnDTnHnMnS. The
convertToOdfTime()
utility method converts from
the hours/minutes/seconds format to the ODF duration format.
inReader = new BufferedReader(
new FileReader(inputFileName));
data = inReader.readLine();
while (data != null) {
info = data.split(",");
/* 0 = date, 1 = time, 2 = temperatue in degrees C */
row = new OdfTableRow(contentDom);
row.setTableStyleNameAttribute(rowStyleName);
cell = new OdfTableCell(contentDom); // (a)
cell.setTableStyleNameAttribute(noaaDateStyleName); // (b)
cell.setOfficeDateValueAttribute(info[0]); // (c)
cell.setOfficeValueTypeAttribute( // (d)
OfficeValueTypeAttribute.Value.DATE.toString());
row.appendCell(cell);
cell = new OdfTableCell(contentDom);
cell.setTableStyleNameAttribute(noaaTimeStyleName);
cell.setOfficeTimeValueAttribute(convertToOdfTime(info[1]));
cell.setOfficeValueTypeAttribute(
OfficeValueTypeAttribute.Value.TIME.toString());
row.appendCell(cell);
cell = new OdfTableCell(contentDom);
cell.setTableStyleNameAttribute(noaaTempStyleName);
cell.setOfficeValueAttribute(Double.parseDouble(info[2]));
cell.setOfficeValueTypeAttribute(
OfficeValueTypeAttribute.Value.FLOAT.toString());
row.appendCell(cell);
table.appendRow(row);
data = inReader.readLine();
}
inReader.close();
After the file is processed, the method adds a blank row and a row with the
label “Avg. Temp” and the formula for the average
temperature. The formula is given in ODF1.1 format.
// insert a blank row
table.appendRow(new OdfTableRow(contentDom));
// the row with the formula
row = new OdfTableRow(contentDom);
row.appendCell(new OdfTableCell(contentDom)); // an empty cell
row.appendCell(createCell(headingStyleName, "Avg. Temp."));
cell = new OdfTableCell(contentDom);
cell.setOfficeValueTypeAttribute(
OfficeValueTypeAttribute.Value.FLOAT.toString());
cell.setTableFormulaAttribute("oooc:=AVERAGE([.C2:.C25]");
row.appendCell(cell);
table.appendRow(row);
officeSpreadsheet.appendChild(table);
}
Saving the Output Document
This is the easiest part of the program: only one line of actual code,
surrounded by error handling.
void saveOutputDocument()
{
try
{
outputDocument.save(outputFileName);
}
catch (Exception e)
{
System.err.println("Unable to save document.");
System.err.println(e.getMessage());
}
}
The Entire Program
You may download the src directory
for this program. This directory comes from a
NetBeans project.