BrowseJasperReportTemplate.java

/*
** Module   : BrowseJasperReportTemplate.java
** Abstract : Template for a browse Jasper report design, used by a browse.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 SVL 20180207 Created initial version.
** 002 SVL 20181004 Support for arbitrary fonts and colors.
** 003 OM  20181005 Improve reports in small page space.
**     SVL 20181006 Implemented arbitrary colors and fonts for column labels.
** 004 SVL 20200514 Added center column alignment.
** 005 RNC 20250131 If there are too many columns to be displayed in the given page width,
*                   prepare the cells for the process of splitting the pages instead of just
*                   shrinking the cells.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.reporting;

import com.goldencode.ast.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.xml.*;
import com.goldencode.util.*;
import net.sf.jasperreports.engine.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.*;
import java.util.*;

import static com.goldencode.p2j.reporting.BrowseJasperDataSource.*;
import static com.goldencode.p2j.xml.XmlAst.*;

/**
 * Template for a Jasper report design, used by a browse. First, user creates a Jasper report
 * design. This design is called "template" because basing on this design a design applicable to
 * the given browse is created (design should have the matching number of columns, same width,
 * alignment and data type of columns). The following transformations is used to transform a
 * design template into the design for the given browse:
 * <ul>
 *    <li>Only the first column in the design template is taken into consideration. It is
 *    duplicated to form other columns.</li>
 *    <li>Column headers and details bands are widened to the page width (excluding margins)</li>
 *    <li>Widths of columns are set in the design in the same proportion as in the browse.</li>
 *    <li>Text alignment of a column (including column label) is set as in the browse.</li>
 *    <li>Label of a column is set as in the browse.</li>
 * </ul>
 * Transformations are performed in two steps: {@link #prepare(String)} function which loads
 * template into XML tree and performs preparation steps, and
 * {@link #generateReport(BrowseWidget)} function which transforms the XML tree into report
 * applicable to the specific browse and compiles the report.
 */
class BrowseJasperReportTemplate
{
   /** Default column label bgcolor. */
   private static final String DEFAULT_COLUMN_LABEL_BGCOLOR = "#ADADAD";

   /** Root node of the prepared template. */
   private XmlAst templateRoot;

   /** Sub-tree representing template for a column header. */
   private XmlAst columnHeaderTemplate;

   /** Sub-tree representing template for a split table header. */
   private XmlAst headersRootTemplate;

   /** Sub-tree representing template for a split table details. */
   private XmlAst detailsRootTemplate;

   /** Sub-tree representing template for a column data area. */
   private XmlAst columnDataTemplate;

   /** Name of the design template file. */
   private String designTemplateFile;
   
   /** Page width specified in the report (assumed to be Portrait, width &lt; height). */
   private int templatePageWidth;
   
   /** Page height specified in the report (assumed to be Portrait, width &lt; height). */
   private int templatePageHeight;
   
   /** The left margin of the page, as read from the template file. */
   private int leftMargin;
   
   /** The right margin of the page, as read from the template file. */
   private int rightMargin;
   
   /** The top margin of the page, as read from the template file. */
   private int topMargin;
   
   /** The bottom margin of the page, as read from the template file. */
   private int bottomMargin;
   
   /**
    * Load given design template into XML tree and perform preparation steps, which in the future
    * allow to quickly transform this tree into a report suitable for the specific browse.
    *
    * @param  designTemplateFile
    *         Name of the design template file.
    *
    * @throws JasperReportException
    *         if design template file could not be read or parsed or has invalid structure.
    */
   void prepare(String designTemplateFile)
   throws JasperReportException
   {
      InputStream stream =
            FwdJasperExtensionRegistry.getRepositoryService().getInputStream(designTemplateFile);

      Document dom;
      try
      {
         dom = XmlHelper.parse(stream);
      }
      catch (Exception e)
      {
         throw new JasperReportException("Could not parse browse report template file",
                                         designTemplateFile, e);
      }
      templateRoot = domToAst(dom);
      this.designTemplateFile = designTemplateFile;

      XmlAst jasperReportsRoot = findChild(templateRoot, "jasperReport");
      cleanUUID(jasperReportsRoot);
      
      // get page width
      templatePageWidth = getAttributeInt(jasperReportsRoot, "pageWidth", 0);
      leftMargin = getAttributeInt(jasperReportsRoot, "leftMargin", 0);
      rightMargin = getAttributeInt(jasperReportsRoot, "rightMargin", 0);
      
      // get page height
      templatePageHeight = getAttributeInt(jasperReportsRoot, "pageHeight", 0);
      topMargin = getAttributeInt(jasperReportsRoot, "topMargin", 0);
      bottomMargin = getAttributeInt(jasperReportsRoot, "bottomMargin", 0);
      
      // Remove FIELD nodes
      List<XmlAst> fields = findChildren(jasperReportsRoot, "field");
      fields.forEach(AnnotatedAst::remove);
      
      // Handle column headers (columnHeader/band nodes)
      XmlAst columnHeadersRoot = findChildPath(jasperReportsRoot, "columnHeader/band");
      List<XmlAst> headers = findChildren(columnHeadersRoot, "textField");

      // Create template for column headers
      columnHeaderTemplate = (XmlAst) headers.get(0).duplicateFresh();

      // Remove original header nodes
      headers.forEach(AnnotatedAst::remove);

      // Handle column data (detail/band nodes)
      XmlAst columnsDataRoot = findChildPath(jasperReportsRoot, "detail/band");
      List<XmlAst> columns = findChildren(columnsDataRoot, "textField");

      // Create template for column data
      columnDataTemplate = (XmlAst) columns.get(0).duplicateFresh();

      // Remove original column data nodes
      columns.forEach(AnnotatedAst::remove);

      // just in case the table does not fit on page width
      headersRootTemplate = (XmlAst) findChildPath(jasperReportsRoot, "columnHeader").duplicateFresh();
      detailsRootTemplate = (XmlAst) findChildPath(jasperReportsRoot, "detail").duplicateFresh();
   }

   /**
    * Transform the XML tree representing prepared template ({@link #templateRoot} field) into the
    * report applicable to the specific browse and compile it.
    * 
    * What to do when "Bands are larger than one page":
    * * use landscape page,
    * * https://community.jaspersoft.com/wiki/bands-larger-one-page
    *
    * @param  browse
    *         Browse for which the report is generated.
    *
    * @return compiled report applicable to the specific browse.
    *
    * @throws JasperReportException
    *         if there was an error during creation of the browse report or the report could not
    *         be compiled.
    */
   JasperReport generateReport(BrowseWidget browse)
   throws JasperReportException
   {
      XmlAst resultRoot = (XmlAst) templateRoot.duplicateFresh();
      XmlAst fieldsRoot = findChild(resultRoot, "jasperReport");
      XmlAst fieldsAnchor = findChild(fieldsRoot, "queryString");
      
      // calc total columns width
      List<BrowseColumnWidget> visibleColumns = browse.getVisibleColumns();
      int colNo = visibleColumns.size();
      if (colNo == 0)
      {
         throw new JasperReportException("Browse has no visible columns", designTemplateFile);
      }
      
      // calculate report columns' widths: widths should be in the same proportion as the browse
      // columns' widths
      int totalBrowseColumnsWidth = 0;
      int[] columnWidths = new int[colNo];
      for (int i = 0; i < colNo; i++)
      {
         BrowseColumnWidget column = visibleColumns.get(i);
         int columnWidth = column.getWidthPixels().toJavaIntegerType();
         columnWidths[i] = columnWidth;
         totalBrowseColumnsWidth += columnWidth;
      }

      boolean longBand = false;
      int pageWidth = templatePageWidth;
      int pageHeight = templatePageHeight;
      if (pageWidth > 0)
      {
         if (pageHeight <= 0)
         {
            // auto-fix to An paper:
            pageHeight = (int) (pageWidth * Math.sqrt(2));
         }
         
         if (totalBrowseColumnsWidth + leftMargin + rightMargin > pageWidth) 
         {
            // well, the report does not fit. Assuming this is portrait, try landscape:
            if (totalBrowseColumnsWidth + leftMargin + rightMargin > pageHeight)
            {
               // The columns will not fit even in landscape. Prepare for splitting the pages.
               longBand = true;
            }
            int tmp = pageWidth;
            pageWidth = pageHeight;
            pageHeight = tmp;

            XmlAst footerNode = findChildPath(resultRoot, "jasperReport/pageFooter/band");
            List<XmlAst> footerReportElements = new ArrayList<>();
            for (XmlAst footerElement : findChildren(footerNode, "textField"))
            {
               footerReportElements.add(findChild(footerElement, "reportElement"));
            }

            // re-center the footer because of the landscape mode
            setAttribute(footerReportElements.get(0), "width", String.valueOf(pageWidth));
            setAttribute(footerReportElements.get(1), "width", String.valueOf(pageWidth));

            // special case when we have a single column that is larger than the usable width
            if (colNo == 1)
            {
               longBand = false;
               columnWidths[0] = pageWidth - leftMargin - rightMargin;
               totalBrowseColumnsWidth = columnWidths[0];
            }

            if (longBand)
            {
               // compensate for the longer format (Page I of N, Rows A to B). After splitting,
               // there will be only one footer element instead of 2
               int oldXCoordinate = Integer.parseInt(getAttribute(footerReportElements.get(0), "x"));
               setAttribute(footerReportElements.get(0), "x", String.valueOf(oldXCoordinate - 18));
            }
         }
      }
      else
      {
         // template does not care about width (like SSV, XLS or XLSX)
         pageWidth = totalBrowseColumnsWidth + leftMargin + rightMargin;
         pageHeight = 10000; // quite large; it will be ignored, anyway
      }

      int usableWidth = pageWidth - leftMargin - rightMargin;

      List<Integer> splits = new ArrayList<>();
      int counter = 0;
      int nextSplit = -1;
      if (longBand)
      {
         int offset = 0;
         int i;
         for (i = 0; i < colNo - 1; i++)
         {
            if (columnWidths[i] > usableWidth)
            {
               // shrink column width only to usable width, there will be only one column on this page
               columnWidths[i] = usableWidth;
               if (i > 0 && columnWidths[i - 1] != usableWidth)
               {
                  // if columnWidths[i - 1] == usableWidth, it means the last column occupied the whole
                  // page. In this case, avoid adding duplicate splits
                  // Furthermore, there is no point of setting the last column width one more time if we
                  // already know that it uses the whole page width
                  splits.add(i);
                  columnWidths[i - 1] += usableWidth - offset;
               }
               splits.add(i + 1);
               offset = 0;
               continue;
            }

            if (offset + columnWidths[i] <= usableWidth)
            {
               // add the whole column to the page
               offset += columnWidths[i];
            }
            else
            {
               // the column doesn't fit anymore, so "move it" to the next page
               columnWidths[i - 1] += usableWidth - offset;
               splits.add(i);
               i--;
               offset = 0;
            }
         }

         if (offset + columnWidths[i] > usableWidth)
         {
            if (columnWidths[i] > usableWidth)
            {
               columnWidths[i] = usableWidth;
            }

            if (columnWidths[i - 1] != usableWidth)
            {
               columnWidths[i - 1] += usableWidth - offset;
               splits.add(i);
            }
         }

         totalBrowseColumnsWidth = usableWidth;
         nextSplit = splits.get(0);
      }
      
      int tableX = (usableWidth - totalBrowseColumnsWidth) / 2;
      
      // center the title:
      XmlAst titleNode = findChildPath(resultRoot,
                                       "jasperReport/title/band/textField/reportElement");
      if (titleNode != null)
      {
         setAttribute(titleNode, "x", "0");
         setAttribute(titleNode, "width", Integer.toString(usableWidth));
      }
      
      // now set the page sizes to [fieldsRoot]
      setAttribute(fieldsRoot, "pageWidth", Integer.toString(pageWidth));
      setAttribute(fieldsRoot, "pageHeight", Integer.toString(pageHeight));
      setAttribute(fieldsRoot, "columnWidth", Integer.toString(usableWidth));
      
      XmlAst columnHeadersRoot = findChildPath(resultRoot, "jasperReport/columnHeader/band");
      XmlAst columnDataRoot = findChildPath(resultRoot, "jasperReport/detail/band");
      
      int xOffset = tableX;
      int splitSize = splits.size();
      for (int i = 0; i < colNo; i++)
      {
         BrowseColumnWidget column = visibleColumns.get(i);
         int columnWidth = columnWidths[i];
         
         // Create nodes for the field and its attributes
         String columnVarName = "column" + column.config().ordinal;
         XmlAst node = createFieldNode(columnVarName, fieldsRoot, fieldsAnchor);
         node = createFieldNode(columnVarName + ".forecolor", fieldsRoot, node);
         node = createFieldNode(columnVarName + ".fontName", fieldsRoot, node);
         node = createFieldNode(columnVarName + ".fontSize", fieldsRoot, node);
         node = createFieldNode(columnVarName + ".isBold", fieldsRoot, node);
         node = createFieldNode(columnVarName + ".isItalic", fieldsRoot, node);
         fieldsAnchor = createFieldNode(columnVarName + ".isUnderline", fieldsRoot, node);

         BrowseTextAlignment alignment = Browse.getTextAlign(column.config());

         // Create column header
         XmlAst columnHeaderNode = (XmlAst) columnHeaderTemplate.duplicateFresh();
         columnHeadersRoot.graft(columnHeaderNode);

         // Set label text
         String[] labelLines = Browse.getLabelVar(column.config());
         StringBuilder sb = new StringBuilder();
         for (int j = 0; j < labelLines.length; j++)
         {
            if (j != 0)
            {
               sb.append("\\n"); // do not add the NL char, use the 2-char escape instead 
            }
            sb.append(labelLines[j]);
         }

         node = findChild(columnHeaderNode, "textFieldExpression");
         node = (XmlAst) node.getImmediateChild(CDATA_SECTION_NODE, null);
         node = (XmlAst) node.getImmediateChild(CONTENT, null);
         node.setText("\"" + sb.toString() + "\"");

         XmlAst headerReportElement = findChild(columnHeaderNode, "reportElement");

         // label foreground color
         node = null;
         String fgColor = getElementColor(browse,
                                          column.config().ehLabelFgColor,
                                          column.config().labelFgColor,
                                          browse.config().ehLabelFgColor,
                                          browse.config().labelFgColor);

         if (fgColor == null)
         {
            fgColor = getElementColor(browse,
                                      column.config().ehFgColor,
                                      column.config().columnFgColor,
                                      browse.config().ehFgColor,
                                      browse.config().fgcolor);
         }

         if (fgColor != null)
         {
            node = createPropertyNode("net.sf.jasperreports.style.forecolor",
                                      fgColor,
                                      headerReportElement,
                                      node);
         }

         // label bg color
         String bgColor = getElementColor(browse,
                                          column.config().ehLabelBgColor,
                                          column.config().labelBgColor,
                                          browse.config().ehLabelBgColor,
                                          browse.config().labelBgColor);
         if (bgColor == null)
         {
            bgColor = DEFAULT_COLUMN_LABEL_BGCOLOR;
         }

         node = createPropertyNode("net.sf.jasperreports.style.backcolor",
                                   bgColor,
                                   headerReportElement,
                                   node);

         // font attributes
         FontDetails font = getColumnFont(column, true);
         if (isFontFamilyAvailable(font))
         {
            node = createPropertyNode("net.sf.jasperreports.style.fontName",
                                      font.fontName,
                                      headerReportElement,
                                      node);
         }
         node = createPropertyNode("net.sf.jasperreports.style.fontSize",
                                   Integer.toString(font.pointSize),
                                   headerReportElement,
                                   node);
         node = createPropertyNode("net.sf.jasperreports.style.isBold",
                                   Boolean.toString(font.style.isBold()),
                                   headerReportElement,
                                   node);
         node = createPropertyNode("net.sf.jasperreports.style.isItalic",
                                   Boolean.toString(font.style.isItalic()),
                                   headerReportElement,
                                   node);
         node = createPropertyNode("net.sf.jasperreports.style.isUnderline",
                                   Boolean.toString(font.style.isUnderline()),
                                   headerReportElement,
                                   node);

         setPositionAndWidth(columnHeaderNode, xOffset, columnWidth);
         setTextAlignment(columnHeaderNode, alignment);

         // Create column data
         XmlAst columnDataNode = (XmlAst) columnDataTemplate.duplicateFresh();
         columnDataRoot.graft(columnDataNode);

         String content = String.format("$F{%s}", columnVarName);
         node = findChild(columnDataNode, "textFieldExpression");
         node = (XmlAst) node.getImmediateChild(CDATA_SECTION_NODE, null);
         node = (XmlAst) node.getImmediateChild(CONTENT, null);
         node.setText(content);

         setPositionAndWidth(columnDataNode, xOffset, columnWidth);
         setTextAlignment(columnDataNode, alignment);

         // process attribute names
         node = findChild(columnDataNode, "reportElement");
         List<XmlAst> nodes = findChildren(node, "propertyExpression");
         for (XmlAst propNode : nodes)
         {
            XmlAst n = (XmlAst) propNode.getImmediateChild(CDATA_SECTION_NODE, null);
            n = (XmlAst) n.getImmediateChild(CONTENT, null);
            n.setText(n.getText().replace("dummy", columnVarName));
         }

         if (longBand && nextSplit == i + 1)
         {
            nextSplit = ++counter == splitSize ? -1 : splits.get(counter);
            xOffset = tableX;
         }
         else
         {
            xOffset += columnWidth;
         }
      }

      ByteArrayInputStream inputStream;
      try
      {
         String namespaceURI = getAttribute(fieldsRoot, "xmlns");
         Document doc = XmlAst.astToDom(resultRoot, namespaceURI);
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         XmlHelper.write(doc, outputStream);
         //DEBUG: XmlHelper.write(doc, "/tmp/generated-report.jrxml");
         inputStream = new ByteArrayInputStream(outputStream.toByteArray());
      }
      catch (ParserConfigurationException | IOException e)
      {
         throw new JasperReportException("Cannot create DOM from XML", designTemplateFile, e);
      }

      try
      {
         JasperReport report = JasperCompileManager.compileReport(inputStream);
         if (longBand)
         {
            // attach the required properties to the report for splitting the pages
            report.setProperty("colNo", String.valueOf(colNo));
            report.setProperty("splitsPerPage", String.valueOf(splitSize));
         }

         return report;
      }
      catch (JRException e)
      {
         throw new JasperReportException("Cannot compile generated browse report",
                                         designTemplateFile, e);
      }
   }

   /**
    * Set position and width of the single <code>reportElement</code> element under the given
    * parent node.
    *
    * @param   parent
    *          Parent node.
    * @param   xPos
    *          Element X position.
    * @param   columnWidth
    *          Element width.
    */
   private void setPositionAndWidth(XmlAst parent, int xPos, int columnWidth)
   {
      XmlAst element = findChild(parent, "reportElement");
      setAttribute(element, "width", Integer.toString(columnWidth));
      setAttribute(element, "x",     Integer.toString(xPos));
   }

   /**
    * Set text alignment of the single <code>textElement</code> element under the given
    * parent node.
    *
    * @param parent
    *        Parent node.
    * @param alignment
    *        Text alignment.
    */
   private void setTextAlignment(XmlAst parent, BrowseTextAlignment alignment)
   {
      XmlAst element = findChild(parent, "textElement");

      String alignmentStr = null;
      switch (alignment)
      {
         case LEFT:
            alignmentStr = "Left";
            break;
         case RIGHT:
            alignmentStr = "Right";
            break;
         case CENTER:
            alignmentStr = "Center";
            break;
      }
      setAttribute(element, "textAlignment", alignmentStr);
   }

   /**
    * Find the single available path under the given parent node, and return the last node in this
    * path.
    *
    * @param  parent
    *         Parent node.
    * @param  path
    *         "/"-separated down path, e.g. "jasperReport/columnHeader/band". Elements of the path
    *         are XML tags.
    *
    * @return the last node in the path, if such path exists, or <code>null</code> if there is no
    *         path or there are multiple matching paths.
    */
   private XmlAst findChildPath(XmlAst parent, String path)
   {
      String[] tags = path.split("/");

      XmlAst res = parent;
      for (String tag : tags)
      {
         res = findChild(res, tag);
         if (res == null)
         {
            break;
         }
      }

      return res;
   }

   /**
    * Find the single available child with the specified XML tag under the given parent node.
    *
    * @param  parent
    *         Parent node.
    * @param  tag
    *         XML tag.
    *
    * @return the single available child with the specified XML tag.
    *
    * @throws JasperReportXmlSearchException
    *         if there is no available node, or there are multiple matching nodes.
    */
   private XmlAst findChild(XmlAst parent, String tag)
   {
      return findChild(parent, tag, false);
   }

   /**
    * Find the single available child with the specified XML tag under the given parent node.
    *
    * @param  parent
    *         Parent node.
    * @param  tag
    *         XML tag.
    *
    * @return the single available child with the specified XML tag. If <code>noError</code> is
    *         <code>true</code>: returns <code>null</code> if there is no available node, or
    *         there are multiple matching nodes.
    *
    * @throws JasperReportXmlSearchException
    *         if <code>noError</code> is <code>false</code>: there is no available node, or
    *         there are multiple matching nodes.
    */
   private XmlAst findChild(XmlAst parent, String tag, boolean noError)
   {
      return findChild(parent, ELEMENT_NODE, tag, noError);
   }

   /**
    * Find the single available child with the specified node type and text under the given parent
    * node.
    *
    * @param  parent
    *         Parent node.
    * @param  nodeType
    *         Node type (see constants in {@link XmlTokenTypes}).
    * @param  nodeText
    *         Node text (e.g. XML tag or attribute name).
    *
    * @return the single available child with the specified node type and text. If
    *         <code>noError</code> is <code>true</code>: returns <code>null</code> if there are no
    *         available node, or there are multiple matching nodes.
    *
    * @throws JasperReportXmlSearchException
    *         if <code>noError</code> is <code>false</code>: there are no available node, or
    *         there are multiple matching nodes.
    */
   private XmlAst findChild(XmlAst parent, int nodeType, String nodeText, boolean noError)
   {
      List<XmlAst> children = findChildren(parent, nodeType, nodeText, true);
      if (children == null || children.size() != 1)
      {
         if (noError)
         {
            return null;
         }
         else
         {
            throw new JasperReportXmlSearchException(parent, nodeText, designTemplateFile);
         }
      }

      return children.get(0);
   }

   /**
    * Find the child nodes with the specified XML tag under the given parent node.
    *
    * @param  parent
    *         Parent node.
    * @param  tag
    *         XML tag.
    *
    * @return available child nodes with the specified XML tag.
    *
    * @throws JasperReportXmlSearchException
    *         there are no available node.
    */
   private List<XmlAst> findChildren(XmlAst parent, String tag)
   {
      return findChildren(parent, ELEMENT_NODE, tag);
   }

   /**
    * Find the child nodes with the specified node type and text under the given parent node.
    *
    * @param  parent
    *         Parent node.
    * @param  nodeType
    *         Type of the target nodes (see constants in {@link XmlTokenTypes}).
    * @param  nodeText
    *         Node text (e.g. XML tag or attribute name).
    *
    * @return available child nodes with the specified node type and text.
    *
    * @throws JasperReportXmlSearchException
    *         there are no available node.
    */
   private List<XmlAst> findChildren(XmlAst parent, int nodeType, String nodeText)
   {
      return findChildren(parent, nodeType, nodeText, false);
   }

   /**
    * Find the child nodes with the specified node type and text under the given parent node.
    *
    * @param  parent
    *         Parent node.
    * @param  nodeType
    *         Type of the target nodes (see constants in {@link XmlTokenTypes}).
    * @param  nodeText
    *         Node text (e.g. XML tag or attribute name).
    *
    * @return available child nodes with the specified node type and text. If
    *         <code>noError</code> is <code>true</code>: returns <code>null</code> if there are no
    *         available nodes.
    *
    * @throws JasperReportXmlSearchException
    *         if <code>noError</code> is <code>false</code>: there are no available nodes.
    */
   private List<XmlAst> findChildren(XmlAst parent,
                                     int nodeType,
                                     String nodeText,
                                     boolean noError)
   {
      List<XmlAst> children = null;
      XmlAst child = null;
      while (true)
      {
         child = (XmlAst) parent.getImmediateChild(nodeType, child);
         if (child == null)
         {
            break;
         }

         if (Objects.equals(nodeText, child.getText()))
         {
            if (children == null)
            {
               children = new ArrayList<>();
            }
            children.add(child);
         }
      }

      if (children == null && !noError)
      {
         throw new JasperReportXmlSearchException(parent, nodeText, designTemplateFile);
      }

      return children;
   }

   /**
    * Remove all "uuid" attributes in the given tree.
    *
    * @param root
    *        Root of the tree.
    */
   private void cleanUUID(XmlAst root)
   {
      removeAttribute(root, "uuid");

      XmlAst child = null;
      while (true)
      {
         child = (XmlAst) root.getImmediateChild(ELEMENT_NODE, child);
         if (child == null)
         {
            return;
         }

         cleanUUID(child);
      }
   }

   /**
    * Create FIELD node for Jasper report template.
    *
    * @param  name
    *         Field name.
    * @param  parent
    *         Parent node.
    * @param  anchor
    *         Anchor node: created node will be positioned after the anchor node.
    *
    * @return created FIELD node.
    */
   private XmlAst createFieldNode(String name, XmlAst parent, XmlAst anchor)
   {
      XmlAst fieldNode = new XmlAst(ELEMENT_NODE, "field");
      setAttribute(fieldNode, "name", name);
      setAttribute(fieldNode, "class", "java.lang.String");
      parent.graftAt(fieldNode, anchor.getIndexPos() + 1);
      return fieldNode;
   }

   /**
    * Create PROPERTY node for Jasper report template.
    *
    * @param  name
    *         Property name.
    * @param  value
    *         Property value.
    * @param  parent
    *         Parent node.
    * @param  anchor
    *         Anchor node: created node will be positioned after the anchor node. Can be
    *         <code>null</code>.
    *
    * @return created PROPERTY node.
    */
   private XmlAst createPropertyNode(String name, String value, XmlAst parent, XmlAst anchor)
   {
      XmlAst node = new XmlAst(ELEMENT_NODE, "property");
      setAttribute(node, "name", name);
      setAttribute(node, "value", value);
      if (anchor == null)
      {
         parent.graft(node);
      }
      else
      {
         parent.graftAt(node, anchor.getIndexPos() + 1);
      }
      return node;
   }

   /**
    * Get value of the specified integer attribute of the specified node.
    *
    * @param  node
    *         Parent node.
    * @param  name
    *         Attribute name.
    * @param  defaultValue
    *         Default value of the attribute.
    *
    * @return value of the attribute or the default value if there is no such attribute.
    */
   private int getAttributeInt(XmlAst node, String name, int defaultValue)
   {
      String val = getAttribute(node, name);
      if (val != null)
      {
         try
         {
            return Integer.valueOf(val);
         }
         catch (NumberFormatException e)
         {
            return defaultValue;
         }
      }
      else
      {
         return defaultValue;
      }
   }

   /**
    * Get value of the specified attribute of the specified node.
    *
    * @param  node
    *         Parent node.
    * @param  name
    *         Attribute name.
    *
    * @return value of the attribute or <code>null</code> if there is no such attribute.
    */
   private String getAttribute(XmlAst node, String name)
   {
      // Structure of an attributed node:
      //
      // <node> [ELEMENT_NODE]
      //    attributes [ATTR_SET]
      //       <name> [ATTRIBUTE_NODE]
      //          #text [TEXT_NODE]
      //             <value> [CONTENT]


      XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
      if (attrSet == null)
      {
         return null;
      }

      XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
      if (attrNode == null)
      {
         return null;
      }

      XmlAst textNode = (XmlAst) attrNode.getImmediateChild(TEXT_NODE, null);
      if (textNode == null)
      {
         return null;
      }

      XmlAst contentNode = (XmlAst) textNode.getImmediateChild(CONTENT, null);
      if (contentNode == null)
      {
         return null;
      }
      else
      {
         return contentNode.getText();
      }
   }

   /**
    * Delete the specified attribute of the specified node.
    *
    * @param  node
    *         Parent node.
    * @param  name
    *         Attribute name.
    */
   private void removeAttribute(XmlAst node, String name)
   {
      // Structure of an attributed node:
      //
      // <node> [ELEMENT_NODE]
      //    attributes [ATTR_SET]
      //       <name> [ATTRIBUTE_NODE]
      //          #text [TEXT_NODE]
      //             <value> [CONTENT]


      XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
      if (attrSet == null)
      {
         return;
      }

      XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
      if (attrNode != null)
      {
         attrNode.remove();
      }
   }

   /**
    * Set the value of the specified attribute.
    *
    * @param node
    *        Parent node.
    * @param name
    *        Attribute name.
    * @param value
    *        Attribute value to set.
    */
   private void setAttribute(XmlAst node, String name, String value)
   {
      // Structure of an attributed node:
      //
      // <node> [ELEMENT_NODE]
      //    attributes [ATTR_SET]
      //       <name> [ATTRIBUTE_NODE]
      //          #text [TEXT_NODE]
      //             <value> [CONTENT]


      XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
      if (attrSet == null)
      {
         attrSet = new XmlAst(ATTR_SET, "attributes");
         node.graft(attrSet);
      }

      XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
      if (attrNode == null)
      {
         attrNode = new XmlAst(ATTRIBUTE_NODE, name);
         attrSet.graft(attrNode);
      }

      XmlAst textNode = (XmlAst) attrNode.getImmediateChild(TEXT_NODE, null);
      if (textNode == null)
      {
         textNode = new XmlAst(TEXT_NODE, "#text");
         attrNode.graft(textNode);
      }

      XmlAst contentNode = (XmlAst) textNode.getImmediateChild(CONTENT, null);
      if (contentNode == null)
      {
         contentNode = new XmlAst(CONTENT, value);
         textNode.graft(contentNode);
      }
      else
      {
         contentNode.setText(value);
      }
   }
}