Table of Contents with iReport

1

Hello,

First I want to thank to the persons that make this technology available. I work with JasperReports for about one year now an I thought it was time to give back some of my findings. Be aware, it could be, that I have missed some information and that my findings may not be accurate in every detail.

As I could see, there are some questions and a few solutions about "Table of Contents". I have to point out that features like table-of-contents (with leader points, indentations, ...), indices, flow text and so on, are "document" functionalities. A reporting application has core functionalities in other fields. So it is quite logical, that iReport has absolutely no built-in support for such features, although it may me desirable to discuss such features in future.

Here are some points from the questions I saw in the posts:

  • TOC entries are already known at the beginning (e.g. passed as a parameter to the main report) or TOC entries are not known at the beginning and have to be collected at report fill time.
  • Page numbers are not known at the beginning of the report and have to be collected at report fill time.
  • TOC should appear as first page or after the title page or at the end of the report.

As a short summary I have found that there is only one possible way to create a TOC, it works the same way as the one given in the demo samples:

  • You need some piece of software that collects the TOC entries and page numbers at report fill time.
  • The TOC can only be placed at the end of the report, in the Summary band or (?) in a group footer band of a group that has only one run per report. In the first case there are no headers/footers and also no page numbers, as the Summary band does not have them by definition. This will be useful, when moving the toc later to another location or when using "page x of y" constructs.
  • If the toc should not be placed at the end, you will have to use the approach from the demo sample. Maybe - I will try out soon - it is possible to create a scriptlet that does the page moving from the end to some other place when the report has finished rendering. This would make the toc moving independent from the surrounding application code.
  • Placing the toc before the end of the report will not work, even when the toc entries (and anchor names, ...) were known at the beginning of the report.
  • This leads to the fact, that it does not help, if the toc entries are known before the report is run. In other words: Passing the toc entries e.g. as a parameter to the main report will not help you in creating a toc with page numbers. In again other words this implies that you dont need to change your data preparing java code to create a toc as you can collect all usually necessary information for the toc during fill time.
  • Having to collect all the toc information during fill time has, by the way, the advantage that you can be absolutely sure, not to miss toc entries or to create too many toc entries. In some cases not all data may be used in a report. Collecting the toc entries at fill time ensures that you have the exact number of toc entries.

This solution with pre-defined toc entries I first tried did not work:

  • I prepared a List<Map> with all the toc entries + anchor keys (but without page numbers) for hyperlinking and passed it as a parameter "tocListing" to the main report. The map contains tocEntry and tocKey. The tocEntry holds the "title" and the tocKey holds a unique String identifier.
  • I created a TocCollector class with methods String addEntry(<tocKey>, <pageNumber>) and Integer getPageNumber(<tocKey>). The constructor takes the $P{tocListing} as parameter and has the job to assign page numbers to the tocListing.
  • The class is initialized in a parameters' "tocCollector" default expression, so we have a running instance of a TocCollector that can be accessed as $P{tocCollector} at any time.
  • I created a group that runs once per report and put in the group header band a table with two columns, $F{tocEntry} and $P{tocCollector}.getPageNumber($F{tocKey}).
  • To collect the page numbers during the detail band run, I put $P{tocCollector}.addTocEntry("section-" + $V{REPORT_COUNT}, $V{PAGE_NUMBER}) to the anchor expression of the fields I wanted to collect.

So far, the solution was on a good way, with the exception that the placement of the toc at the beginning returned always null from the getPageNumber(..) method because the page numbers were not yet collected. What I wanted, was the following:

  • At "now" time the report should run and create first the toc entries from the tocListing, then the report, calling tocCollector.addTocEntry(..) with the appropriate page number wherever necessary.
  • At "Report" time all page number fields in the toc table at the beginning of the report should be evaluated, using getPageNumber(<tocKey>). Note: This makes the assumption that the engine would be able to evaluate the method parameter <tocKey> at "now" time and to evaluate the method call at "report" time, buffering meanwhile the environment of the "now" run. The engine may probably not support such a complex requirement.

After having configured this way, it turned out, that the getPageNumber(<tocKey>) method was always called before the calls to addTocEntry(<tocKey>, <pageNumber>) method, even when the pageNumber field was set to "Report" (the setting is ignored?). In a way this may be seen as expected behaviour, as the call occurs in a table and this may not support distinct evaluation time settings.

The solution that worked, looks like the demo sample solution:

It seems that the statement I read, that said more or less "with iReport you can create Tocs in various ways, just use your ideas" is not that true as I hoped it to be. I would rather say: "There is exactly one way to create a toc, it is described in the samples, you need quite an effort to create it and the result is halfway o.k.".

So, again: The only solution for a toc works in the way described in the demo samples. There is only one addition:

  • I saw that all propagated solutions use a scriptlet that is derived from JRDefaultScriptlet. This is not necessary as long as you do not need: a) directly access fields, variables etc. of the report, or b) you do not want to override one or more methods from JRDefaultScriptlet.

To make it short:

  • I changed the above class and added a new method: getDataSource() that returns a JRRewindableDataSource. The latter just returns a newly created JRMapCollectionDataSource.
  • The getPageNumber() method is not more necessary.
  • The class has an empty constructor and is created (like above) as default expression for a parameter named "tocCollector" like described above.
  • To each field that should be collected for the toc I set addTocEntry(...) as anchor expression (see code below to understand this placement).
  • The table in the summary band that will hold the toc gets its own "tocDatasource" with fields tocKey, tocText and pageNumber. The table datasource expression is $P{tocCollector}.getDataSource().
  • The page number column text field expression is changed to $F{pageNumber}.
  • It finally became a slight variant of the solution in the demo/samples - from which I initially thought: «I don't believe it's so much work».

I hope I could add some information, maybe some things are not clear, I would appreciate to give more precise information as soon as I find the time. The code of the class (having some more fields and still containing the now obsolete getPageNumber method) is below:

Code:
package xxxxxxxxxxxxxx.jasper;
 
import java.util.Collection;
import java.util.List;
import java.util.Map;
 
import javolution.util.FastList;
import javolution.util.FastMap;
import net.sf.jasperreports.engine.JRRewindableDataSource;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;
 
/**
 * This class can be used to collect page numbers during report fill time.
 * Each page number is associated to a String key. During collection you
 * add key/page number pairs and when finished you can get the collected
 * data as JRDataSource.
 */
public class JRSimpleTocCreator {
	public final static String MODULE = JRSimpleTocCreator.class.getName();
 
	private List<Map<String, Object>> tocEntries = FastList.newInstance();
 
	public JRSimpleTocCreator() {
		super();
	}
 
	/**
	 * Returns a JRDataSource for usage as datasource expression. The datasource contains all
	 * TOC entries that were collected at this time.
	 * @return
	 */
	public JRRewindableDataSource getDataSource() {
		@SuppressWarnings({ "unchecked", "rawtypes" })
		Collection<Map<String, ?>> resultColl = (Collection) tocEntries;
		return new JRMapCollectionDataSource(resultColl);
	}
 
	/**
	 * Returns the page number for the given key.
	 * @param tocKey
	 * @return
	 * @deprecated
	 */
	public Integer getPageNumber(String tocKey) {
		for (Map<String, Object> tocEntry : this.tocEntries) {
			if (tocKey.equals(tocEntry.get("tocKey"))) {
				return (Integer) tocEntry.get("pageNumber");
			}
		}
		return null;
	}
 
	/**
	 * Adds a record to the list of tocEntries.
	 * @param tocKey A key that uniquely identifies an anchor name of a report field.
	 * @param tocText A String expression that holds the title text for the toc entry.
	 * @param pageNumber An Integer value, holding the page number.
	 * @param entryLevel A (not used) Integer value for later use to describe the level of the entry. 
	 * @return
	 */
	public String addToc(String tocKey, String tocText, Integer pageNumber, Integer entryLevel) {
		tocEntries.add(createTocEntry(tocKey, tocText, pageNumber, entryLevel));
		return tocKey;
	}
 
	private Map<String, Object> createTocEntry(String tocKey, String tocText, Integer pageNumber, Integer entryLevel) {
		Map<String, Object> tocEntry = FastMap.newInstance();
		tocEntry.put("tocKey", tocKey);
		tocEntry.put("tocText", tocText);
		tocEntry.put("tocLevel", pageNumber);
		tocEntry.put("pageNumber", pageNumber);
		return tocEntry;
	}
 
}


Post Edited by akuehn at 12/22/2011 19:27
akuehn's picture
6
Joined: Dec 22 2011 - 7:10am
Last seen: 5 years 9 months ago

3 Answers:

0

Impressive!

I find it difficult how to apply in my situation.

Like a picture a sample explains often more than 1000 words.

Could you provide a very basic sample including an .JRXML report which runs in iReport.

mulcamd's picture
92
Joined: Mar 16 2011 - 11:47am
Last seen: 4 months 5 days ago
0

Hello,

I am searching for table of conents from last 2 days but did not find any good solution.

After reading your article I got some basic idea about implementation.

Can you please provide some JRXML report sample.

Thanks,

Mayank

jainmayank33's picture
Joined: Mar 27 2013 - 10:54pm
Last seen: 4 years 5 months ago
0

I've found another solution to create a simple table of contents which doesn't require to add any external libs or classes:

http://rolandtapken.de/blog/2013-04/jasperreports-append-report-toc-without-scriplets

cybso42's picture
Joined: Apr 5 2013 - 9:12am
Last seen: 4 years 5 months ago
Feedback
randomness