-
Notifications
You must be signed in to change notification settings - Fork 6
Tutorial
This tutorial is extracted from https://github.com/jferard/fastods/tree/master/fastods-examples/src/main/java/com/github/jferard/fastods/examples. See the classes in this directory for the full code.
Writing a full documentation would be a considerable work, because every time you change the library, you have to rewrite the doc.
My idea is to provide a set of examples of the features of FastODS.
Before you start, add the following dependency to your POM (Maven users):
<dependency>
<groupId>com.github.jferard</groupId>
<artifactId>fastods</artifactId>
<version>0.6.1</version>
</dependency>
For other build tools, look at: https://search.maven.org/artifact/com.github.jferard/fastods/0.6.1/jar
Let's start with the famous "Hello, World!" example.
As stated in the javadoc, "An OdsFactory is the entry point for creating ODS documents." Every time you want to create an ODS document, you'll start with something like that:
final OdsFactory odsFactory = OdsFactory.create(Logger.getLogger("hello-world"), Locale.US);
Now, you can create an ODS writer. You have the choice: either you give the filename now, or you keep it for the end. Let's create an anonymous writer: the content will live in memory until we save it to a file:
final AnonymousOdsFileWriter writer = odsFactory.createWriter();
The writer owns a document that we need to create a spreadsheet:
final OdsDocument document = writer.document();
Okay, let's go. We create a new table (a new sheet indeed) named "hello-world":
final Table table = document.addTable("hello-world");
We get the first row:
final TableRow row = table.getRow(0);
And the first cell of the first row:
final TableCell cell = row.getOrCreateCell(0);
Note that we could have chained the calls:
TableCell cell = document.addTable("hello-world").getRow(0).getOrCreateCell(0)
Finally, we put the famous sentence in this cell A1
cell.setStringValue("Hello, world!");
And save the file.
writer.saveAs(new File("generated_files", "a_hello_world_example.ods"));
With a mvn clean verify
at the root of the project, you can check the result in the
fastods-examples/generated-files
directory.
Now, we want to write values in other cells that the A1 cell.
We start with the (now) usual boilerplate code to get a document:
final OdsFactory odsFactory = OdsFactory.create(Logger.getLogger("accessing"), Locale.US);
final AnonymousOdsFileWriter writer = odsFactory.createWriter();
final OdsDocument document = writer.document();
You know how to create a table:
final Table table1 = document.addTable("direct-access");
Get the first row and the first cell of the first row:
TableRow row = table1.getRow(0);
TableCell cell = row.getOrCreateCell(0);
And set a value.
cell.setStringValue("A1");
Note that the access is a write only access. You can't read the content of a cell. You can have direct access to any cell, but it has a cost. If the row doesn't exist yet in the list of rows, it will create all rows between the last row and the actual row.
row = table1.getRow(6);
You can have direct access to cells
cell = row.getOrCreateCell(1);
cell.setStringValue("B7");
With a TableHelper, direct access might be easier to write...
final TableHelper tableHelper = TableHelper.create();
...but note that the cell is referred by row then column (as matrices in maths). To access the cell "F2", you'll use:
cell = tableHelper.getCell(table1, 1, 5);
cell.setStringValue("F2");
You can use an address, but there is the cost of parsing that address and a risk of malformed address:
try {
cell = tableHelper.getCell(table1, "E3");
cell.setStringValue("E3");
} catch (final ParseException e) {
/* this won't happen here! */
}
To be (almost) complete, there is another way to write a value to a cell:
try {
tableHelper.setCellValue(table1, "D4", CellValue.fromObject("D4"));
} catch (final ParseException e) {
/* this won't happen here! */
}
Direct access may be useful, but FastODS was designed for a relative access Create a new table:
final Table table2 = document.addTable("relative-access");
We want ten rows of data
for (int r = 0; r < 10; r++) {
The Table object has an internal row index (that is updated by the getRow
method).
Just call nextRow
to make the index advance by one (you have to call nextRow
before you write data, to get the first row):
final TableRow tableRow = table2.nextRow();
And then create a "walker" for this row:
final TableCellWalker cellWalker = tableRow.getWalker();
Now, we want nine columns for each row:
for (int c = 0; c < 9; c++) {
Add the value to each cell
cellWalker.setStringValue(String.valueOf((char) (c + 'A')) + String.valueOf(r + 1));
And then push one cell right.
cellWalker.next();
}
}
And save the file.
writer.saveAs(new File("generated_files", "b_accessing_example.ods"));
Note 1: There is a slight inconsistency between table.newtRow
(before using
the row) and cellWalker.next
(after using the cell). Maybe I'll fix it before
version 1.0...
Note 2: We will see how to merge cells in the Advanced part of this tutorial.
Four elements define the content of a cell:
- the value
- the type (string, float, boolean, ...)
- the style (font, background color, border, ...)
- the format, or data style (number of digits for a float, date format for a date, ...)
So far, we just created cells of type string, with neither style nor data style. Now, we will create cells with a more varied content.
Now, we will create cells of different types. First, a table:
Table table = document.addTable("types");
We add a header:
TableRow tableRow = table.nextRow();
TableCellWalker cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Type");
cellWalker.next();
cellWalker.setStringValue("Example");
The first row contains a boolean:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Boolean");
cellWalker.next();
cellWalker.setBooleanValue(true);
The second row contains a currency:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Currency");
cellWalker.next();
cellWalker.setCurrencyValue(10.5, "USD");
The third row contains a date:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Date");
cellWalker.next();
cellWalker.setDateValue(new GregorianCalendar(2014, 9, 17, 9, 0, 0));
The fourth row contains a float:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Float");
cellWalker.next();
cellWalker.setFloatValue(3.14159);
The fifth row contains a percentage:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Percentage");
cellWalker.next();
cellWalker.setPercentageValue(0.545);
The sixth row contains...
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("String");
cellWalker.next();
cellWalker.setStringValue("A String");
The seventh row contains a time (that mean a duration):
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Time");
cellWalker.next();
cellWalker.setTimeValue(3600);
The eighth row contains nothing
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("Void");
cellWalker.next();
cellWalker.setVoidValue();
FastODS can guess types, based on Java object types. It's useful when we try to auto
import typed data, e.g. from a ResultSet
. We can use this ability to reduce the
boilerplate code.
Let's define two lists:
final List<String> A = Arrays
.asList("Type", "Boolean", "Currency", "Date", "Float", "Percentage", "String",
"Void");
final List<Object> B = Arrays.<Object>asList("Type guess example", true,
new CurrencyValue(10.5f, "USD"), new GregorianCalendar(2014, 9, 17, 9, 0, 0),
3.14159, new PercentageValue(0.545f), "A String", null);
As you can see, some types are not guessable: is 0.545
a float or a percentage? For
FastODS, it is a float. What Java type will map a currency value? We have to use specific
types...
We skip a row for readability:
table.nextRow();
Now, we can use setValue
to take advantage of the type guess:
for (int r = 0; r < A.size(); r++) {
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setStringValue(A.get(r));
cellWalker.next();
cellWalker.setCellValue(CellValue.fromObject(B.get(r)));
}
Let's try to add some shapes and colors. First, we have to create a style for the header:
final TableCellStyle grayStyle = TableCellStyle.builder("gray")
.backgroundColor(SimpleColor.GRAY64).fontWeightBold().build();
The functions calls are chained in a fluent style.
We create a table and get the first cell:
table = document.addTable("styles");
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
Now, we add a value and set the style
cellWalker.setStringValue("A1");
cellWalker.setStyle(grayStyle);
In LO, you'll see a new style named "gray" in the "Styles" window. That's because
TableCellStyle
s are visible by default. We can make a style hidden by adding a
hidden()
call:
final TableCellStyle hiddenGrayStyle = TableCellStyle.builder("hiddenGray")
.backgroundColor(SimpleColor.GRAY64).fontWeightBold().hidden().build();
cellWalker.next();
cellWalker.setStringValue("A2");
cellWalker.setStyle(grayStyle);
The "gray2" style is not present in the Style window of LO. This distinction between "visible" and "hidden" styles matches the distinction between common and automatic styles in the OpenDocument specification (3.15.3):
Note: Common and automatic styles behave differently in OpenDocument editing consumers. Common styles are presented to the user as a named set of formatting properties. The formatting properties of an automatic style are presented to a user as properties of the object to which the style is applied.
This distinction is sometimes hard to handle. FastODS tries to make things easy:
each style is, by default, either visible or hidden (depending on the kind of the style),
but you can always override the default choice. For instance, TableCellStyle
s,
are visible by default, but TableRowStyle
s and data styles are hidden by default.
In most of the cases, you can simply ignore the distinction.
Let's continue with a new style:
final TableCellStyle rotateStyle = TableCellStyle.builder("rotate")
.fontColor(SimpleColor.RED).textRotating(Angle.deg(37)).build();
cellWalker.next();
cellWalker.setStringValue("A3");
cellWalker.setStyle(rotateStyle);
You can explore the TableCellStyle
to create the style you need. A last example:
final TableCellStyle borderStyle = TableCellStyle.builder("border").fontName(LOFonts.DEJAVU_SANS).fontSize(SimpleLength.pt(24))
.borderAll(SimpleLength.mm(2), SimpleColor.BLUE, BorderAttribute.Style.OUTSET).build();
cellWalker.next();
cellWalker.setStringValue("A4");
cellWalker.setStyle(borderStyle);
I think you get it now.
What do we see? Yes, the last cell is ugly. But it is also partially hidden because the height of the row was not adapted. You have to adapt it yourself. Let's try with another row:
final TableRowStyle tallRowStyle = TableRowStyle.builder("tall-row").rowHeight(SimpleLength.cm(3)).
build();
tableRow = table.nextRow();
tableRow.setStyle(tallRowStyle);
cellWalker = tableRow.getWalker();
cellWalker.setStringValue("B1");
cellWalker.setStyle(borderStyle);
You have to set the height of the row manually. There's nothing like an Optimal height/width in the OpenDocument specification, and FastODS won't provide those features. (Maybe one day I'll write a tool to compute the width/height of a text.) You can also add a column style:
final TableColumnStyle wideColumn = TableColumnStyle.builder("wide-col")
.columnWidth(SimpleLength.cm(9)).build();
table.setColumnStyle(0, wideColumn);
Data styles are what we call "formats": is your date in plain text or in US format? how many digits have your number? are the negative numbers in red?
A cell may have a style, and this style may have a data style. The OpenDocument specification states that data style can't be attached directly to a cell, but must be embedded in a cell. That's not easy to handle for FastODS, but should not be too complicated for you. As usual, we create a table and get the first cell:
table = document.addTable("data styles");
We'll place a float with the standard format, and a float with a custom format side by side
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
Standard format:
cellWalker.setFloatValue(123456.789);
And now create a custom data style:
final DataStyle floatDataStyle = new FloatStyleBuilder("float-datastyle", Locale.US)
.decimalPlaces(8).groupThousands(true).build();
cellWalker.next();
cellWalker.setFloatValue(123456.789);
cellWalker.setDataStyle(floatDataStyle);
We can do the same with dates:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
A date with the standard format:
final Calendar cal = new GregorianCalendar(2018, 1, 1, 0, 0, 0);
cellWalker.setDateValue(cal);
And a custom format:
final DataStyle dateDataStyle = new DateStyleBuilder("date-datastyle", Locale.US)
.dateFormat(new DateStyleFormat(DateStyleFormat.DAY, DateStyleFormat.DOT,
DateStyleFormat.MONTH, DateStyleFormat.DOT, DateStyleFormat.YEAR)).visible()
.build();
cellWalker.next();
cellWalker.setDateValue(cal);
cellWalker.setDataStyle(dateDataStyle);
A last try with a time (duration):
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setTimeValue(10000000);
And:
final DataStyle timeDataStyle = new TimeStyleBuilder("time-datastyle", Locale.US)
.timeFormat(new DateStyleFormat(DateStyleFormat.text("Hour: "),
DateStyleFormat.LONG_HOURS)).visible().build();
cellWalker.next();
cellWalker.setTimeValue(10000000);
cellWalker.setDataStyle(timeDataStyle);
Obvioulsy, you can combine a style and a data style:
tableRow = table.nextRow();
cellWalker = tableRow.getWalker();
cellWalker.setTimeValue(10000000);
cellWalker.setStyle(rotateStyle);
cellWalker.setDataStyle(timeDataStyle);
Setting the data style for every cell may become cumbersome. Happily, you can decide of the default data styles at the creation of the document.
First, create a DataStyles
object (note the "s") with a builder:
final DataStylesBuilder dsb = DataStylesBuilder.create(Locale.US);
dsb.floatStyleBuilder().decimalPlaces(0);
dsb.dateStyleBuilder().dateFormat(
new DateStyleFormat(DateStyleFormat.LONG_DAY, DateStyleFormat.SLASH,
DateStyleFormat.LONG_MONTH, DateStyleFormat.SLASH,
DateStyleFormat.LONG_YEAR));
You can use the other data style builders if you want, and then build all the data style in one shot:
final DataStyles ds = dsb.build();
Now, create the factory
final OdsFactory odsFactory = OdsFactory.create(Logger.getLogger("cells2"), Locale.US);
and pass the created "data styles" to the factory:
odsFactory.dataStyles(ds);
We can continue as usual:
final AnonymousOdsFileWriter writer = odsFactory.dataStyles(ds).createWriter();
final OdsDocument document = writer.document();
And create the same cells as above:
final Table table = document.addTable("data styles");
final TableRow tableRow = table.nextRow();
final TableCellWalker cellWalker = tableRow.getWalker();
cellWalker.setFloatValue(123456.789);
final Calendar cal = new GregorianCalendar(2018, 1, 1, 0, 0, 0);
cellWalker.next();
cellWalker.setDateValue(cal);
And save the file.
writer.saveAs(new File("generated_files", "c_value_type2.ods"));
We know how to access to cells and how to format those cells. We still have to format the pages. Let's start with a new table:
Table table = document.addTable("format-page");
TableRow row = table.getRow(0);
TableCellWalker walker = row.getWalker();
walker.setStringValue("Text");
We will add a footer and a header.
First, we build the three parts of a simple header:
final Text leftHeaderContent = Text.content("left header");
final Text centerHeaderContent = Text.builder().par().span("center header, page ")
.span(Text.TEXT_PAGE_NUMBER).build();
final Text rightHeaderContent = Text.content("right header");
Then we build the header itself:
final Header header = PageSection.regionBuilder().region(PageSectionContent.Region.LEFT)
.text(leftHeaderContent).region(PageSectionContent.Region.CENTER)
.text(centerHeaderContent).region(PageSectionContent.Region.RIGHT)
.text(rightHeaderContent).allMargins(SimpleLength.cm(2))
.minHeight(SimpleLength.cm(5)).buildHeader();
For the footer, let's use the one part format:
final Footer footer = PageSection.simpleBuilder().text(Text.content("footer"))
.buildFooter();
We now insert the header and the footer in a page style:
PageStyle pageStyle = PageStyle.builder("page-style").header(header).footer(footer)
.build();
And add the page style into the table style:
TableStyle tableStyle = TableStyle.builder("table-style").pageStyle(pageStyle)
.build();
And set this table style:
table.setStyle(tableStyle);
Create another table:
table = document.addTable("format-page2");
row = table.getRow(0);
walker = row.getWalker();
walker.setStringValue("Text");
We can create a very simple header:
final Header minimalHeader = PageSection.simpleBuilder().content("minimal header").buildHeader();
Or a complex footer:
final TextBuilder textBuilder = Text.builder();
par()
means a new paragraph, span
a new portion of text:
textBuilder.par().span("complex");
Both can be used in one call:
textBuilder.parContent("footer");
Text can be styled:
textBuilder.par().styledSpan("date is:",
TextProperties.builder().fontWeightBold().buildHiddenStyle("footer1"));
In one call:
textBuilder.parStyledContent(Text.TEXT_DATE,
TextProperties.builder().fontSize(SimpleLength.pt(25)).fontWeightBold().buildHiddenStyle("footer2")
);
And build the text:
final Text footerContent = textBuilder.build();
final Footer complexFooter = PageSection.simpleBuilder().text(footerContent).buildFooter();
As above:
pageStyle = PageStyle.builder("page-style2").header(minimalHeader).footer(complexFooter).build();
tableStyle = TableStyle.builder("table-style2").pageStyle(pageStyle)
.build();
table.setStyle(tableStyle);
If you know what you are doing, you can play with LO settings, for instance:
table.setSettings("View1", "ZoomValue", "150");
For more doc, see:
Let's start with a new page:
final Table table = document.addTable("advanced");
final TableRow row = table.getRow(0);
final TableCellWalker walker = row.getWalker();
And save the file.
writer.saveAs(new File("generated_files", "a_hello_world_example.ods"));