Create and Use a Custom Finder
The MATLAB Report Generator report generation API supports creation of finders that search data containers for specified objects and return the results in reportable form. Finders allow you to separate search logic from report logic in your report generators. Finders also promote reuse of search logic, thereby speeding development of report generators. This example shows how to develop and use a finder to generate a report.
Define a Finder
Creating a finder entails creating a MATLAB class that defines the finder's properties and behavior. The following sections explain the steps needed to create a finder class. The explanation uses a class named GrantFinder
as an example. The code for the class resides in a file, GrantFinder.m
, that accompanies this script. The GrantFinder
class defines a finder capable of finding and formatting grants awarded by the U.S. National Endowment for the Humanities (NEH).
Create a Skeleton Class Definition File
Use the MATLAB Editor (not the Live Editor) to create a skeleton class definition for your finder, for example
Specify Finder Base Class
Specify the Report API's mlreportgen.finder.Finder
class as the base class for your finder.
This base class defines properties that are common to finders, including
Container
: a property used to reference the container to be searched by the finder. For example, theGrantFinder
use this property to store a reference to a grant database that it creates.Properties
: a property used by finder clients to specify property values that an object must have to satisfy a search. For example, this property allows a GrantFinder client to specify the grant properties that a grant must have to be returned as a result of a search of the NEH grant database.
The mlreportgen.finder.Finder
class specifies other properties and methods that your finder class definition must define. This ensures that your finder works with the Report API.
Define a Finder Constructor
Define a function that creates an instance of your finder, for example,
The GrantFinder
constructor uses the MATLAB parseFile
function to read the grant XML file from disk and convert it to a MAXP DOM document. It then passes the DOM document to the mlreportgen.finder.Finder
constructor, which sets the MAXP DOM document as the value of the finder's Container
property. Storing the NEH database as a MAXP DOM document allows the finder to use MATLAB's native API to search the database.
The constructor also calls a reset
function that initializes variables used to search the grant database. The GrantFinder class defines this function. Similarly, your class must define a reset function. The reset
function ensures that a client can use your finder to conduct multiple searches of its container. See Define a reset
Method for more information.
Define a find
Method
Define a method to search the finder container for objects that meet user-specified constraints. The find method must return an array of result objects that contain the objects that it finds. Result objects are objects of base type mlreportgen.finder.Result
. Returning the find results as result objects allows a user of your finder to add the results to a report or report chapter. See Define a Finder Result for more information. Returning the find results as a MATLAB array allows you to use for loop to process search results, for example
The GrantFinder
's find
method illustrates definition of a find
method.
This find method uses getElements
, a search utility function that the GrantFinder
class defines (see Define a Search Utility Method) to search the grant database for grants that meet property value constraints specified by the finder's Properties
property. The getElements
function sets an internal property named Elements
to the result of its search. The result is a list of matlab.io.xml.dom.Element
(matlab.io.xml.dom.Element
).
The find
method then converts this element to an array of result objects of type GrantResult
. It uses the GrantResult
constructor to create a grant result object from the matlab.io.xml.dom.Element
object that contains the grant data.
Define hasNext
and next
Methods
Your finder class definition must define hasNext
and next
methods. On its first invocation, your hasNext
method must create a queue of search results and return true if the queue is not empty. On subsequent invocations the hasNext
method must return true
if the queue is empty, false
otherwise. Your next
method must return the first result in the queue on its first invocation, the next result, on its next invocation, and so on, until the queue is empty.
These methods are intended to allow a client of your finder to use a MATLAB while
loop to search your finder's container, for example,
The GrantFinder
class illustrates a hasNext
method.
This method first checks whether it has already created a search queue as indicated by the finder's IsIterating
property. If the queue already exists and is not empty, this method returns true
. If the queue exists and is empty, this method returns false
. If the queue does not yet exist (i.e., this is the method's first invocation), the hasNext
method creates a result queue as follows. First, it uses its internal getElements
method to get the grants that meet the search criteria specified by the finder's Properties
property. The getElements
method sets an internal finder property named ElementCount
to the number of results found. If ElementCount
is greater than zero, the hasNext
method sets an internal property named NextElementIndex
to 1. The finder's next
method uses this property to save the state of the search queue, that is the next item in the queue. Finally, if the queue is not initially empty, the finder returns true
; otherwise, false
.
The GrantFinder
's next
method operates on the queue created by the hasNext
method.
Define a Search Utility Method
Your finder's find
and hasNext
methods must search your finder's container for objects that satisfy search constraints. You should consider defining a search utility that both methods can use. For example, the GrantFinder
hasNext
and next
methods both delegate searching to an internal utility named getElements
. The getElements
method in turn delegates searching to an XML document search API named XPath
(see XPath Tutorial).
Create an InvalidPropertyNames
Property
Your finder must define a property named InvalidPropertyNames
that specifies object properties that cannot be used to constrain a search. The mlreportgen.finder.Finder
base class uses this property to verify that user-specified search properties specified by your finder's Properties
property are valid. If not, the base class throws an error. In other words, if a client sets your finder's Properties
property to invalid properties, the base class throws an error. In this way, the Report API's base finder handles property validity checking for your finder.
If your finder can use any search object property as a search constraint, it should set the InvalidPropertyNames
property empty. For example, the GrantFinder
can handle any grant property. It therefore sets this property empty:
Define a reset
Method
A finder must be able to support multiple searches to avoid the need to create a finder for every search. For this reason, the Report API's base finder class forces your finder class to define a reset
method that resets variables used by your finder's search logic, for example,
Define a Finder Result
If a suitable definition does not exist, you must create a class to define the result objects returned by your finder. This section shows how to define a finder result object. It uses a class named GrantResult
as an example. The GrantResult
class defines results returned by the GrantFinder
class used as an example in the Define a Finder section. The GrantResult.m
file that accompanies this script contains the code for the GrantResult
class. Defining a finder result entails the following tasks.
Specify the Result Base Class
Define mlreportgen.finder.Result
as the base class for your result class, for example,
Define Object
Property
Define a property named Object
that clients of your result object can use to access the found object that your result object contains. Specify protected
as the SetAccess
value of your finder's Object
property. This ensures that only your result can specify the found object that it contains.
Your result constructor must set the found object as the value of its Object
property. Your result constructor can use the base class constructor to perform this task, for example,
Expose Found Object Properties
Your result's Object
property allows a client to access the found object and therefore its properties. However, accessing the properties can require extra code or specialized knowledge. You may want to expose some or all of the found object's properties as properties of the result object. For example, the GrantResult
class exposes the following subset of a grant's properties.
This saves the client of the grant finder result object from having to extract these properties itself. Your result's constructor should extract the values of the properties to be exposed and set the corresponding result properties to the extracted values, for example,
Note that GrantResult
combines some of the grant properties into a single exposed property. For example, it exposes a grant's InstCity
, InstState
, InstPostalCode
, and InstCountry
properties into a single result property named Location
.
In this example, the constructor uses internal methods to extract the grant properties from the grant object, which is a MAXP DOM Element
object, for example,
Define a getReporter
Method
You must define a getReporter method for your result object that returns a reporter object that reports on the found object that the result object contains. This method allows a client of your finder to report on a result of a find operation simply by adding the result to a Report
, Section
, or Chapter
object. For example,
A report or chapter's append
method knows that a result object must have a getReporter
method that returns a reporter that formats the data the result contains. So if you add a result object to a report or chapter, the append
method invokes the result's getReporter
method to get the result reporter and adds the result reporter to the report or reporter, causing the result data to be formatted and included in the report.
The GrantResult
class definition defines a getReporter
method that returns a customized version of the Report API's mlreportgen.report.BaseTable
reporter. The BaseTable
reporter generates a table with a numbered title. The GrantResult
class customizes the BaseTable
reporter to generate a table of grant properties, for example,
The following code shows how the GrantResult
class customizes the BaseReporter
to generate a numbered grant properties table:
Use a Finder
This script shows how to use a finder to generate a report. This script uses the GrantFinder example used in the Define a Finder section to generate a PDF report on NEH grants to institutions in selected states for the year of 2010. The script performs the following tasks.
Import the Report Generator API
Import the classes included in the MATLAB Report Generator's Report API. Importing the classes allows the script to use unqualified (i.e., abbreviated) names to refer to the classes.
import mlreportgen.report.* import mlreportgen.dom.*
Create a Report Container
Create a PDF container for the report, using the Report API's mlreportgen.report.Report
class. Note that because the script imports the Report API, it can refer to the class by its unqualified name.
rpt = Report("grant","pdf");
Create the Report Title Page
Add a title page to the report, using the Report API's TitlePage
class.
append(rpt,TitlePage( ... "Title","NEH Grants", ... "Subtitle","By State for 2010", ... "Image","neh_logo.jpg", ... "Author","John Doe" ... ));
Create the Report Table of Contents
Add a table of contents, using the Report API's TableOfContents
class.
append(rpt,TableOfContents);
Find the Report Data
Use an array of structures to specify the states to be included in this report. Each structure contains the data for a specific state:
Name
: name of the statePostalCode
: the state's postal (zip) codeGrants
: grants made to institutions in the state. This field is initially emptyNGrants
: number of grants made to institutions in this state (initially empty)
states = struct( ... "Name",{"California","Massachusetts","New York"}, ... "PostalCode",{"CA","MA","NY"}, ... "Grants",cell(1,3), ... "NGrants",cell(1,3) ... );
Use a grant finder to populate the Grants
and NGrants
fields of the state structures. Create the grant finder.
f = GrantFinder;
Loop through the state array. For each state, use the finder's Properties
property to constrain the search for grants awarded to the state. Use these grant properties to constrain the search:
InstState
: Specifies the postal code of the state in which the institution that received the grant is located.YearAwarded
: Specifies the year in which the grant was awarded.
n = numel(states); for i = 1:n f.Properties = [ {"InstState",states(i).PostalCode}, ... {"YearAwarded","2010"}]; states(i).Grants = find(f); states(i).NGrants = numel(states(i).Grants); end
Create the Grant Summary Chapter
Create a grant summary as the first chapter of the report. The grant summary chapter contains a title and a grant summary table. Each row of the table lists the total number of grants and total amount of money awarded to institutions in the state for the year of 2010. States appear in the table in descending order of number of grants. Each state is hyperlinked to the chapter that details the grants awarded to it.
Create the Summary Chapter Container
Start by creating a chapter container.
ch = Chapter("Title","Grant Summary");
Create the Contents of the Grant Summary Table
Create a cell array containing the contents of the table header.
header = {'State','Grants Awarded','Amount Awarded'};
Preallocate a cell array to contain the table body contents. The cell array has Rx3 rows and columns where R is the number of states and 3 is the number of summary items reported for each state.
body = cell(numel(states), 3);
Sort the states array by the number of grants awarded to them, using the MATLAB sort
function. The sort
function returns ind
, an array of indices to the states array. The first index of the ind
array is the index of the state with the most grants, the second, with the second most number of grants, etc.
[~, ind] = sort([states.NGrants],"descend");
Loop through the states by number of grants, filling in the summary information for each state. Use a variable, rowIdx
, as an index to the cell array row corresponding to the current state.
rowIdx = 0;
The following line rearranges the states
array in order of grants received and creates a for
loop that assigns each structure in the sorted states
array to the variable state
on each iteration of the loop.
for state = states(ind)
Update the row index to point to the cell array row corresponding to the current state.
rowIdx = rowIdx+1;
The script enters a hyperlink to the grant details chapter for the state as the first entry in the table for the state, for example,
The following line uses a DOM InternalLink
constructor to create the hyperlink. The InternalLink
constructor takes two arguments, a link target id and the text of the hyperlink. The script uses the current state's postal code as the link target id and the state's name as the link text. Later on, when the script creates the grant details chapter, it inserts a link target in the chapter title whose id is the state's postal code. This completes creation of the hyperlink.
body(rowIdx, 1) = {InternalLink(state.PostalCode,state.Name)};
Assign the total number of grants for this state to the second item in its cell array row.
body(rowIdx, 2) = {state.NGrants};
Compute the total amount awarded to this state.
totalAwarded = 0; for grant = state.Grants totalAwarded = totalAwarded + str2double(grant.AwardAmount); end
Use the cur2str
method to format the total amount as a dollar amount, for example,
and assign the formatted result as the third and final item in the cell array for this state.
formattedCurrency = sprintf('\x24%.2f',totalAwarded); body(rowIdx,3) = {regexprep(formattedCurrency,'\d{1,3}(?=(\d{3})+\>)','$&,')}; end
To create the summary table, pass the header and body cell arrays to the constructor of a mlreportgen.dom.FormalTable
object.
table = FormalTable(header,body);
A formal table is a table that has a header and a body. The FormalTable
constructor takes two arguments: a cell array that specifies the contents of the table's header and a cell array that specifies the contents of its body. The constructor converts the cell array contents to DOM TableRow
and TableEntry
objects that define the table, saving the script from having to create the necessary table objects itself.
Format the Grant Summary Table
At this point, the summary table looks like this:
This is not very readable. The heading has the same format as the body and the columns are not spaced apart.
In the following steps, the script adjusts the header text formatting to look like this:
First the script specifies the width and alignment of the table columns, using a DOM TableColSpecGroup
object. A TableColSpecGroup
object specifies the format of a group of columns. The summary table has only one group of columns so the script needs to create only one TableColSpecGroup
object.
grp = TableColSpecGroup;
The TableColSpecGroup
object lets the script specify the default style of the table's columns. The script specifies 1.5in
as the default width of the columns and center alignment as the default column alignment.
grp.Style = {HAlign("center"),Width("1.5in")};
The script uses a TableColSpec
object to override the default column alignment for the first column.
specs(1) = TableColSpec;
specs(1).Style = {HAlign("left")};
grp.ColSpecs = specs;
grp.Span = 3;
table.ColSpecGroups = grp;
Note The script could use as many as three TableColSpec
objects, one for each table column, to override the group column styles. The first TableColSpec
object applies to the first column, the second to the second column, etc. The script needs to assign only one column spec object to the group because it is overriding the default style only for the first column. However, if it needed to change only the third column, it would have to assign three column spec objects, leaving the Style
property of the first two column spec objects empty.
The default table style crowds the table entries. So the script uses a DOM InnerMargin
format object to create some space above the entries to separate them from the entries in the row above them. An InnerMargin
object creates space (inner margin) between a document object and the object that contains it, for example, between the text in a table entry and the table entry's borders. The InnerMargin
constructor optionally takes four arguments, the left, right, top, bottom inner margins of a document object.
The script use this constructor to create a top inner margin format of 3 points. It then assigns this format to the style of the entries in the summary table's body section.
table.Body.TableEntriesStyle = {InnerMargin("0pt","0pt","3pt","0pt")};
Finally the script format the table header to consist of bold, white text on a gray background:
table.Header.row(1).Style = {Bold,Color("white"),BackgroundColor("gray")};
Add the Summary Chapter to the Report
append(ch,table); append(rpt,ch);
Create the Grant Details Chapters
Loop through the state structures.
for state = states
For each state create a chapter to hold the state's grant details. Insert a link target into the chapter title to serve as a target for the hyperlink in the summary table in the first chapter.
ch = Chapter("Title",{LinkTarget(state.PostalCode),state.Name});
Loop through the grant results for the state.
for grant = state.Grants
For each grant result, add the result to the chapter.
append(ch,grant);
A grant result has a getReporter
method that returns a reporter that creates a table of selected grant properties. The chapter append
method is preconfigured to get a result's reporter and add it the chapter. Thus, adding a grant to a chapter is tantamount to adding the result property table to the chapter, for example,
end append(rpt,ch); end
Close the Report Object
Closing the report object generates the PDF output file (grant.pdf
) that the report object specifies.
close(rpt);
Display the report
rptview(rpt);
Appendix: The NEH Grant Database
The source of the database used in this example is the National Endowment for the Humanities (NEH). The database contains information on NEH grants for the period 2010-2019. It contains about 6000 records in XML format. It is available at NEH Grant Data. This example uses a local copy of the database XML file, NEH_Grants2010s.xml
.
The database consists of a Grants
element that contains a set of Grant
elements each of which contains a set of grant data elements. The following is an extract from the database that illustrates its structure:
See Also
mlreportgen.finder.Finder
| mlreportgen.report.Report
| mlreportgen.report.Chapter
| rptview